封面：アスナ（Pixiv ID：29328488）介绍emoji 即表情符号，来自日语词汇“絵文字”（假名为“えもじ”，读音即 emoji），最初由栗田穰崇创作，目前已被纳入 Unicode 编码并得到广泛支持。⭐Emojipedia 收录了目前所有的 emoji。🎈尽管如此，emoji 在部分老设备、旧系统的支持性并不好，甚至可能显示为类似“□”的无法显示字符。这当然是不好的。为了优化展现，目前常用的做法是渲染时用图片替换 emoji 字符。相关的项目有 GitHub 正在使用的 g-emoji-element 以及 Twitter 的 twemoji 等。本文将要介绍的就是引入 twemoji 来优化展现。🍻使用方法引入 twemoji.jstwemoji 官方默认提供 MaxCDN 作为加载源，但该 CDN 在国内似乎可用性并不好。因此，笔者还是一贯地选择 jsDelivr。当然，你也可以从官方仓库下载并制作你自己的源。twemoji.js 从 NPM 拉取，使用： 将上述代码插入到网页中适当的位置，即可引入 twemoji。此时 window 下会有 twemoji 对象以便调用。💡调用 parse 方法twemoji.parse() 方法的详细用法在 README 中有很科学💎的说明，所以笔者在这里只介绍整体替换的思路。这个方法接受两个参数：第一个参数是解析的对象，直接使用 document.body 即可；第二个参数是一个 Object，包含一些选项：{ callback: Function, // default the common replacer attributes: Function, // default returns {} base: string, // default MaxCDN ext: string, // default ".png" className: string, // default "emoji" size: string|number, // default "72x72" folder: string // in case it's specified // it replaces .size info, if any }callback 是每次替换的回调，返回值将作为 的 src 属性，即对应替换图片的 URL。attributes 是另一个回调，返回值应该是一个 Object，这个 Object 将被整合到 img 的 attributes，例如返回 {test: 'test'} 就会使 多出一个 test="test" 的属性。base 是 emoji 替换图片的加载源，默认是 MaxCDN，稍后笔者将会给出 jsDelivr 的对应参数值。⚠️ext 指定 emoji 替换图片的扩展名，默认是 .png，也可以使用 .svg 或其他类型（需要 size 或 folder 配合）。className 指定 的 class 属性，默认是 emoji，如果与现有 class 有冲突则需要改变。size 指定“图片尺寸”，实质上是指定一个目录名。不建议修改。folder 指定 emoji 替换图片的目录名，将会覆盖 size 的设置。⚠️ 使用 jsDelivr 时，emoji 替换图片不能直接从 NPM 拉取，而是要从 GitHub 拉取，应该把 base 设置为 https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/（末尾的 / 不可省略，原因后述）。如果没有指定 callback，组装 URL 的规则默认是 {base}{size|folder}/{icon}{ext}，其中 icon 作为 callback 的第一个参数传入。所以，当使用默认 callback 时，如果 base 的末尾缺少 /，会直接导致 URL 拼接非法。同理，ext 也一定要以 . 开头。👌综上，解析的代码如下：twemoji.parse(document.body, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/' });handsome 主题handsome 是 Typecho 主题，所以首先需要确保 Typecho 已经开启 emoji 支持，即数据库编码已经调整至 utf8mb4，否则可能会出现数据损坏的严重后果。调整的方法可以参考：使 Typecho 支持 emoji 的显示😂😂😂 以及 如何让Typecho支持emoji的存储。调整完毕后，可以发送几条包含 emoji 的评论测试配置是否有效。handsome 主题配置中可以直接加入 twemoji 的设定：第一步，依次展开 开发者设置 -> 自定义输出body 尾部的HTML代码，在末尾加入： 第二步，依次展开 开发者设置 -> 自定义 JavaScript，同样在末尾加入：$(function() { twemoji.parse(document.body, { attributes() {return {nogallery: 'nogallery'};}, base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/' }); });第三步，依次展开 开发者设置 -> 自定义 CSS，还是在末尾加入：img.emoji { height: 1em !important; width: 1em !important; margin: 0 .05em 0 .1em !important; vertical-align: -0.1em !important; border-radius: 0 !important; border: 0 !important; display: inline-block !important; padding: 0 !important; }为了处理样式冲突，不得不加了很多 !important（话说这也太多了吧😂）如果你还开启了 PJAX，那么在 PJAX -> PJAX回调函数 末尾也需要加入第二步中的代码。这样，我们就成功引入了 twemoji，emoji 在旧设备、旧系统也能很漂亮了~💯
封面：一緒に爆発しろ❤（Pixiv ID：46262787）我目前使用 GitHub repo 作为 OSS，配合 jsDelivr 实现访问速度优化，这在之前已经说过：[post cid="147" cover="https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu/blog/github-jsdelivr-oss/thumb.png"/]但是我目前的方案要求我在更改文件后重新发一个 release，由于是不同的 release，jsDelivr 会认为数据是不互通的，相当于我整体刷新了缓存，初次访问会变慢。而且每次推上去之后还要发版……虽说也就是鼠标点两下，但果然还是好麻烦。干脆整个 GitHub Actions，让它在我 push 之后自动发版，顺便把整个 repo 全请求一遍来 force cache。关于 GitHub Actions 的教程，官方有详细的文档（也有部分中文版本，没仔细看），这里 link 一个 Workflow 的写法：Workflow syntax for GitHub Actions - GitHub Help这个 Workflow 是 GitHub Actions 的概念，直译是“工作流”，.github/workflows/ 下的每个 .yml 文件都描述一个 Workflow。Workflow 有一个 on 属性，规定了这个 Workflow 的触发事件。可以是对特定分支或 tag 的 push、pull request，也可以设置为定时任务。Workflow 下还有 Job 的概念，一个 Workflow 下一般有多个 Job。再之后就不具体展开了，官方文档中比我讲的清楚。下面开始着手整这个 Workflow。# Auto release and force cache name: Auto release and force cache # Controls when the action will run. Triggers the workflow only on push # events but only for the master branch on: push: branches: [ master ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Auto release - name: Auto release uses: marvinpinto/action-automatic-releases@latest with: repo_token: ${{ secrets.GITHUB_TOKEN }} automatic_release_tag: ${{ github.run_id }} prerelease: false title: ${{ github.run_id }} # Force cache - name: Force cache run: | function scan() { for e in `ls $1` do object=$1"/"$e if [ -d $object ] then scan $object else curl -s -o /dev/null --retry 5 "https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu@master"${object:1} url="https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu@${{ github.run_id }}"${object:1} code=`curl -s -o /dev/null --retry 5 -w %{http_code} $url` echo "Force cache "$url" [ "$code" ]" if [ $code != "200" ] then url="https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu@master"${object:1} echo " - Try master "$url" [ `curl -s -o /dev/null --retry 5 -w %{http_code} $url` ]" url="https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu@${{ github.run_id }}"${object:1} echo " - Retry "$url" [ `curl -s -o /dev/null --retry 5 -w %{http_code} $url` ]" fi url="https://cdn.jsdelivr.net/gh/wuxianucw/ucw-desu"${object:1} echo " -> "$url" [ `curl -s -o /dev/null --retry 5 -w %{http_code} $url` ]" fi done } scan .整好了。下面我们来细品一下。name 是 Workflow 的名字，我整了个“自动发版并强制缓存”，这个可以自定义，按需即可。on 设置了触发的条件，push 到 master 分支时触发。只有一个名为 build 的 Job，在 Ubuntu 最新版环境下运行。首先必须 checkout，以便后续操作；然后使用现成的轮子 marvinpinto/action-automatic-releases，它支持自动发版或替换已存在的同名版，发了一个 tag 为当前 Workflow run_id 的版（此时会有邮件提示）；之后就是 Force cache，因为我比较懒，所以直接往里面塞 shell 脚本了。此时 commit，立刻就会触发。成功的效果大概是像这样的：没有做中文路径支持，如果是中文路径则并不能有效刷新（但这并不影响 build passing）。同时由于以 . 开头的文件或目录默认是隐藏的，也不会进行刷新。由于 jsDelivr 的神秘特性，有时可能出现暂时无法更新的情况，这时使用 repo@master 方式访问以及一点点耐心可能会有所帮助。
MathJax 目前已经推出了全新的 v3 版本，使用现代化的编程技巧重写了整个代码，包括 TS、ES6、Promises 等，同时全面采用 jsDelivr 作为默认 CDN 取代原先的 cdn.mathjax.org，在速度上有较大提升（国内尤为明显）。咕咕咕了很久之后我终于下定决心将其更新到了 v3 版本。这篇笔记就来讲解一下从 v2 升级到 v3 的一些快捷方法和注意事项。[scode type="blue"]这篇笔记针对 Typecho 的 handsome 主题编写，对于其他程序/主题的 MathJax 更新可能仅仅只有启发作用。[/scode]生成 v3 配置MathJax 有一个配置选项，在 v2 系列版本中书写在 我的写法是： 并没有什么明显的区别。插入部署完毕后，来到前端页面 F12 并在 Console 中运行 MathJax.version，就可以看到 3.x.x 的返回值咯 ~参考MathJax Configuration ConverterTypesetting and Converting Mathematics — MathJax 3.0 documentationmathjax/MathJax-demos-web: A repository with examples using mathjax-v3
前言这几天初步了解了一下 PHP-Parser，它是一个由 PHP 语言编写的 PHP 解析器，支持将残缺的 PHP 代码转换为 AST、对 AST 进行操作以及将 AST 转换为格式友好的 PHP 源代码。功能可以说是十分强大且灵活。前文提到的 AST 即抽象语法树，是源代码语法结构的一种抽象表示，可以认为 AST 与其对应的 PHP 源代码是等价的。项目地址：nikic/PHP-Parser，十分感谢 nikic 大佬以及其他 Contributors 为我们提供了如此方便强大的库。使用 composer 安装：php composer.phar require nikic/php-parser生成 AST我们需要利用 AST 便利的特性，当然就需要先把原生 PHP 代码转换为一棵 AST。use PhpParser\Error; use PhpParser\NodeDumper; use PhpParser\ParserFactory; $code = <<<'CODE' <?php function test() { var_dump(0xaa); } test(); CODE; $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); try { $ast = $parser->parse($code); } catch (Error $error) { echo "Parse error: {$error->getMessage()}\n"; return; } $dumper = new NodeDumper; echo $dumper->dump($ast) . "\n";这就是一个最简单的例子，它可以生成 $code 中代码所对应的 AST，同时通过 NodeDumper，我们可以轻松直观地看到它的结构。操作 ASTPHP-Parser 提供了 NodeVisitor 和 NodeFinder 两种遍历节点的接口，后者是前者的一种简洁用法，本质上它们都需要使用 NodeTraverser。这里我们以适用范围更广的 NodeVisitor 举例。AST 就像是一把精巧的手术刀，许多大规模改动源代码的问题都可以通过它简便地解决。操作 AST 时就像是在对源代码动手术，以节点为单位批量解决许多看似难以解决的问题。例如，我们需要把代码中所有的十六进制数字换成十进制书写，便于维护，在源代码很大时这个工作将会很困难，但利用 PHP-Parser 即可轻松处理。我们用 NodeVisitor 处理一下：use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PhpParser\Node\Scalar\LNumber; $traverser = new NodeTraverser(); $traverser->addVisitor(new class extends NodeVisitorAbstract { public function leaveNode(Node $node) { if ($node instanceof LNumber) { // 十六进制数字修复为十进制 return new LNumber($node->value,array('kind'=>LNumber::KIND_DEC)); } } }); $ast = $traverser->traverse($ast);对于源代码中的数字，PHP-Parser 将其解析为 LNumber 节点，我们重新创建一个 kind 为十进制、值与原先相同的 LNumber 节点即可。leaveNode 方法将在 NodeVisitor 离开节点时执行，返回值将改变该节点的值。对于 Expression 节点，返回常量 NodeTraverser::REMOVE_NODE 可以移除该节点。生成代码PHP-Parser 在操作完毕后，一般需要将抽象的数据结构 AST 转换回具体的源代码，此时可以使用 PrettyPrinter。经过 PrettyPrinter 处理后，可以得到美化（当然也可以是自定义格式）后的源代码。$prettyPrinter = new PhpParser\PrettyPrinter\Standard; $newCode = $prettyPrinter->prettyPrintFile($ast);
引言这几天需要安装一个控制cookie_flag的NGINX模块，但是很遗憾我并没有找到可供直接使用的RPM包，无奈之下只好选择编译安装。搜索引擎提供的教程大多需要重编译NGINX，但我只想添加一个模块，并不想如此大费周折，且另一方面我的生产环境实在是不便于乱搞，所以在折腾完毕之后写下这篇笔记以备不时之需。开始官方文档中对这个插件的说明：Cookie-Flag我是CentOS 7下的NGINX 1.16.1，并没有找到仓库为我直接提供RPM包。（有是有一个，不过那个仓库是付费订阅？？？每月10刀？？？）于是决定自行编译安装。（下列步骤均在root下完成，你可能需要先su一下；行首的#代表命令提示符）首先拿到这个模块的源代码：# git clone https://github.com/AirisX/nginx_cookie_flag_module.git然后根据目前部署的NGINX版本下载对应版本的NGINX源代码（完整列表）：# wget http://nginx.org/download/nginx-1.16.1.tar.gz # tar zxf nginx-1.16.1.tar.gz这样一来，事前准备就全部完成啦~编译这时开始编译我们想要加载的新模块，首先进入解压好的NGINX源代码目录：# cd nginx-1.16.1 # ls查看一下当前运行的NGINX的编译选项：# nginx -V复制一下configure arguments后面的内容，在下一步中将其粘贴到命令的末尾，以免出现问题。下面执行一次configure：# ./configure --add-dynamic-module=../nginx_cookie_flag_module/ [上一步复制的内容]例如我此时需要执行：# ./configure --add-dynamic-module=../nginx_cookie_flag_module/ --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'等待configure执行完毕后，开始编译：# make modules如果一切正常，编译结果将会输出在objs文件夹中：# ls objs/其中ngx_http_cookie_flag_filter_module.so即为我们需要的编译完成的动态模块。把它复制到模块目录下：# cp objs/ngx_http_cookie_flag_filter_module.so /usr/lib64/nginx/modules/完成。加载在NGINX全局配置文件中添加：load_module modules/ngx_http_cookie_flag_filter_module.so;重启NGINX即可。参考NGINX Docs | Cookie-FlagAirisX/nginx_cookie_flag_module: Module for Nginx which allows to set the flags "HttpOnly", "secure" and "SameSite" for cookies.NGINX编译安装动态模块(不需重新编译nginx)-Linux运维日志
最近给博客增加了船新的雀魂表情包，由于从导出表情到重新配置OwO.json都是完全由自己来完成，不像上次扩展贴吧表情的时候有现成的东西可以直接拿来用，所以记录一下以便之后需要时留作参考。 ::majsoul:001:: 并没有尝试一组表情中能否多种格式图片并存，这次统一采用了png格式，避免给自己找锅。本次新增的一组表情共计151张，需要在OwO.json中对它们一一进行声明。手写是不太可能的，因为是大量的重复工作，懒惰如我显然是宁可写一个Generator也不会去一条条加。gen.py的内容如下：#!/usr/bin/python3 OWO_NAME='雀魂' OWO_SLUG='majsoul' PICTURE_COUNT=151 f=open("OwO.json","w",encoding='utf-8') f.write(''' "%s": { "name": "%s", "type": "image", "container": [ '''%(OWO_NAME,OWO_SLUG)) for i in range(1,PICTURE_COUNT+1): f.write(''' { "icon": "%03d", "text": "%03d" }'''%(i,i)) if i<PICTURE_COUNT: f.write(',') f.write('\n') f.write(''' ] }''') f.close()其中OWO_NAME为这组表情的显示名称，OWO_SLUG为表情的内部别名，PICTURE_COUNT为图片总数。若需要生成其他表情包声明，改动这些值即可。执行后将在当前目录下生成一个OwO.json，内容是需要插入到完整OwO.json中的这个表情包的声明。 ::majsoul:029:: [scode type="yellow"]这里生成的并不是完整的json文件，需要手动将其插入完整的OwO.json中。[/scode]如果执行正确，将会生成形如下方代码块中所示的一段文本： "雀魂": { "name": "majsoul", "type": "image", "container": [ { "icon": "001", "text": "001" }, { "icon": "002", "text": "002" } ] }为方便起见，我只列出了PICTURE_COUNT=2的情况。插入位置如下图所示，需要视情况在后方补一个半角逗号。应用json检测工具检测通过后即可部署。由于上述代码是针对Python3编写的，考虑到有时并不方便获取Python3执行环境，补充一个JavaScript版本：(function(){ 'use strict'; const OWO_NAME="雀魂"; const OWO_SLUG="majsoul"; const PICTURE_COUNT=151; const html_encode=str=>{ let s=""; if (str.length==0)return""; s=str.replace(/&/g,"&"); s=s.replace(/</g,"<"); s=s.replace(/>/g,">"); s=s.replace(/ /g," "); s=s.replace(/\'/g,"'"); s=s.replace(/\"/g,"""); s=s.replace(/\n/g," "); return s; } let output=` "${OWO_NAME}": { "name": "${OWO_SLUG}", "type": "image", "container": [ `; for(let i=1,num;i<=PICTURE_COUNT;i++){ num=(Array(3).join('0')+i).slice(-3); output+=` { "icon": "${num}", "text": "${num}" }`; if(i<PICTURE_COUNT)output+=','; output+='\n'; } output+=` ] }`; document.write(`${html_encode(output)}`); })();在浏览器中新建标签页，按下F12键，在开发人员工具的Console选项卡中粘贴并执行上述代码即可生成在网页上，复制后插入即可。
TinyPNG是一个优秀的图片压缩服务，通过它，你可以轻松地减少图片的加载体积。例如本文的头图（来源于TinyPNG官网首页，通过技术手段对该页面元素重排后屏幕截图获得），压缩前大小为259045字节，压缩后仅79658字节，为原来的30.75%。（数据来自TinyPNG）上次讲到过可以通过引入lazyload.js来提升图片加载时的用户体验，但是并没有对速度起到实质性的优化，且门槛较高（需要引入js并进行配置，部分读者可能知识储备不足）。但使用TinyPNG压缩图片则是很简单的事情，进入官网上传即可。图片的尺寸得到了优化，加载的速度就能够得以提升。按照本文头图的压缩率，使用压缩后的图片就能在原先加载一张图片的时间里加载三张图片，对速度的提升是显著的。（当然，不一定每张图片都能够有如此高的压缩率）更加令人振奋的是，TinyPNG的图片压缩对图片质量的损害极低。我用肉眼对比压缩前后，没有找到明显的模糊/失真，十分适合拿来预处理博客上引用的图片。于是本文所有引用的图片均已经过TinyPNG预处理。打开TinyPNG官网，可以看到非常简明的操作界面：把png文件或者jpeg文件丢进去就好了，支持一次性上传多张，会全部压缩好以后生成一个zip统一下载，也可以自己一张张下。不过，毕竟是免费的服务，不仅对单张上传图片的大小有5MB的限制，而且一次性最多允许上传20张。如果需要处理更多，则需要刷新页面再来。我压缩图片的起因是最近给博客增加了雀魂表情包，必须控制加载体积。那套表情包一共有一百五十来张图片，很难想象一点一点上传多么难熬。而且因为网络原因，常常出现上传20张，实际只处理了10张不到的情况。所幸官方提供了api，下次有类似需求时可以不必如此大费周折了。点击导航栏上的DEVELOPER API转到api接入申请页面，很快就能生成一个自己的key，官方的文档也很全，甚至还有WordPress插件（考虑闲着的时候写个Typecho的？我自己没需求，果然还是算了）。不过，仍然有月上传量500张的限制。钞能力即可突破限制，但是我没有[heimu]（疯狂暗示）[/heimu]，500张/月基本够用。直接POST首页的接口也是一种思路，得到错误信息如下：{"error":"unauthorized","message":"Please use api.tinify.com."}看来没那么简单？答案是否定的，就那么简单。过程略。综上，TinyPNG是一个优秀的图片压缩服务。满足日常需求使用官方api足矣，当然他们主页使用的接口也极容易突破，可作为免key的api接口使用，有兴趣者可以自行研究。
BUG简述近期更新了handsome 5.2.0，后台写文章时发现编辑器工具栏上贴心地增加了增强功能的快捷按钮，于是稍微试了试。今天的主角就是我们的“调用其他文章”功能。方便起见，在本地搭建typecho+handsome测试环境。复现如下：emm，原来你管这叫随机……修复方法众所周知出锅需要补。根据常识，对增强功能的解析在libs/Content.php中完成，于是定位到该文件相关位置：自postContent方法开始跟踪，相关代码如下： /** * 输入内容之前做一些有趣的替换+输出文章内容 * * @param $obj * @param $status * @return string */ public static function postContent($obj,$status){ $content = $obj->content; $options = mget(); //... //中略 //... $content = Content::parseContentPublic($content); } return trim($content); }注意到解析外部文章调用逻辑存在于parseContentPublic方法中，定位到该方法继续跟踪，相关代码如下： /** * 一些公用的解析，文章、评论、时光机公用的，与用户状态无关 * @param $content * @return null|string|string[] */ public static function parseContentPublic($content){ $options = mget(); //... //中略 //... //调用其他文章页面的摘要 if ( strpos( $content, '[[post')!== false) {//提高效率，避免每篇文章都要解析 $pattern = self::get_shortcode_regex(array('post')); $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content); } //... //后略 //...显然，具体内容替换已经交由Content::quoteOtherPostCallback()处理，定位至该方法，相关代码如下： /** * 一篇文章中引用另一篇文章正则替换回调函数 * @param $matches * @return bool|string */ public static function quoteOtherPostCallback($matches){ $options = mget(); // 不解析类似 [[post]]] 双重括号的代码 if ( $matches[1] == '[' && $matches[6] == ']' ) { return substr($matches[0], 1, -1); } //对$matches[3]的url如果被解析器解析成链接，这些需要反解析回来 $matches[3] = preg_replace("/(.*?)<\/a>/",'$1',$matches[3]); $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表 $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数 //这里需要对id做一个判断，避免空值出现错误 $cid = @$attrs["cid"]; $url = @$attrs['url']; $cover = @$attrs['cover'];//封面 $targetTitle = "";//标题 $targetUrl = "";//链接 $targetSummary ="";//简介文字 $targetImgSrc = "";//封面图片地址 if (!empty($cid)){ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $posts = $db->fetchAll($db ->select()->from($prefix.'contents') ->orWhere('cid = ?',$cid) ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')); //这里需要对id正确性进行一个判断，避免查找文章失败 if (count($posts) == 0){ $targetTitle = "文章不存在，或文章是加密、私密文章"; }else{ $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]); if ($cover == ""){ $thumbArray = $db->fetchAll($db ->select()->from($prefix.'fields') ->orWhere('cid = ?',$cid) ->where('name = ? ', 'thumb')); $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value']); }else{ $targetImgSrc = $cover; } $targetSummary = Content::excerpt(Markdown::convert($result['text']),60); $targetTitle = $result['title']; $targetUrl = $result['permalink']; } }else if (empty($cid) && $url !=""){ $targetUrl = $url; $targetSummary = @$attrs['intro']; $targetTitle = @$attrs['title']; $targetImgSrc = $cover; }else{ $targetTitle = "文章不存在，请检查文章CID"; } $imageHtml = ""; $noImageCss = ""; if (trim($targetImgSrc) != ""){ $imageHtml = ' '; }else{ $noImageCss = 'style="margin-left: 10px;"'; } return <<<EOF {$imageHtml} {$targetTitle} {$targetSummary} EOF; }该方法中头图又由Content::whenSwitchHeaderImgSrc()获取，跟踪： /** * 处理具体的头图显示逻辑：当有头图时候，显示随机图片还是第一个附件还是一张图片还是thumb字段 * @param int $index * @param $howToThumb 显示缩略图的方式，0，1，2，3 * @param $attach 文章的第一个附件 * @param $content 文章内容 * @param $thumbField thumb字段 * @return string */ public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField){ $options = mget(); $randomNum = unserialize(INDEX_IMAGE_ARRAY); // 随机缩略图路径 $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$index] . '.png';//如果有文章置顶，这里可能会导致index not undefined $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i'; $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i'; $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i'; if ($howToThumb == '0'){ return $random; }elseif ($howToThumb == '1' || $howToThumb == '2'){ if (!empty($thumbField)){ return $thumbField; }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){ return $attach->url; }else{ if (preg_match_all($pattern, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMD, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }else{//文章中没有图片 if ($howToThumb == '1'){ return ''; }else{ return $random; } } return $thumb; } }elseif ($howToThumb == '3'){ if (!empty($thumbField)){ return $thumbField; }else{ return $random; } } }由于无论几次调用，传入的$index始终为0（仅在文章列表显示逻辑中该参数发挥作用，具体暂未作分析），而随机头图的逻辑是将整个随机头图集随机乱序从中取索引值为$index的一项进行输出，且乱序操作仅在开始前进行一次，故此时当前文章大头图（如果随机）、文章内引用的其他文章展现头图（如果随机）均为一致项目。个人认为展现效果较差，尤其是当这篇文章是随机头图，而引用处又距离大头图较近，将产生单调感。出于稳定性和维护方便考虑不操作$index参数，而是考虑在whenSwitchHeaderImgSrc方法中新建static变量，每次随机调用时将该变量+1，从而达到显示不同的随机头图效果。先放出代码，然后进行必要解释。 /** * 处理具体的头图显示逻辑：当有头图时候，显示随机图片还是第一个附件还是一张图片还是thumb字段 * @param int $index * @param $howToThumb 显示缩略图的方式，0，1，2，3 * @param $attach 文章的第一个附件 * @param $content 文章内容 * @param $thumbField thumb字段 * @param $quote 是否在处理调用外部文章 0没有 1正在处理 2复位 * @return string */ public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField,$quote=0){ static $used=1;//(ucw)解决引用其他文章随机头图重复问题 //(ucw)附注：存在潜在bug，随机头图集的size需要足够大 if($quote==2){$used=1;return '';} $options = mget(); $randomNum = unserialize(INDEX_IMAGE_ARRAY); // 随机缩略图路径 $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$quote?$used:$index] . '.png';//如果有文章置顶，这里可能会导致index not undefined $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i'; $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i'; $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i'; if ($howToThumb == '0'){ if($quote)$used++; return $random; }elseif ($howToThumb == '1' || $howToThumb == '2'){ if (!empty($thumbField)){ return $thumbField; }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){ return $attach->url; }else{ if (preg_match_all($pattern, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMD, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }else{//文章中没有图片 if ($howToThumb == '1'){ return ''; }else{ if($quote)$used++; return $random; } } return $thumb; } }elseif ($howToThumb == '3'){ if (!empty($thumbField)){ return $thumbField; }else{ if($quote)$used++; return $random; } } }我新建了一个integer型参数$quote，作为是否正在处理引用的标志。static变量$used标记目前可用（未重复）的首个随机头图索引，当且仅当需要返回随机结果且正在处理引用情况时，使$used++。$used初始化为1防止与当前文章的随机头图重复。这样做有一个潜在bug：当随机头图的数量小于或等于引用文章的数目时，数组的对应位置不存在，此时会出现显示问题。（由于php的变量声明真的很随意，这只是一个warning级别的错误，最终输出的url是一个不存在的地址，前台访问时会404导致图片无法显示）我忽视这个bug的原因是我为自己配置了44张随机头图（具体来源信息参见左侧边栏“友情链接->总览”），而我不太可能一次性引用44篇文章。修复这个潜在bug也非常简单，只需要将$used对随机头图数目取模。上述的修复方法有些绕弯子，甚至增加了参数。但我考虑到实装阅读模式（在我的blog，它是“启动魔眼”）后，postContent方法不止调用一次，替换后的内容应该保证对同一次生成的内容是一致的，不应该出现阅读模式下头图与常规模式下有差异的情况，且这样会需要更大的随机头图集，容易带来不可预料的结果，所以我额外编写了重置$used变量的逻辑，在每次调用parseContentPublic方法时： /** * 一些公用的解析，文章、评论、时光机公用的，与用户状态无关 * @param $content * @return null|string|string[] */ public static function parseContentPublic($content){ $options = mget(); //... //中略 //... //调用其他文章页面的摘要 if ( strpos( $content, '[[post')!== false) {//提高效率，避免每篇文章都要解析 Content::whenSwitchHeaderImgSrc(0,0,0,0,0,2);//* 这里 $pattern = self::get_shortcode_regex(array('post')); $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content); } //... //后略 //...首先进行复位，在几乎不影响效率的前提下保证内容的稳定性。同时，调用时加上最后一个参数： /** * 一篇文章中引用另一篇文章正则替换回调函数 * @param $matches * @return bool|string */ public static function quoteOtherPostCallback($matches){ $options = mget(); // 不解析类似 [[post]]] 双重括号的代码 if ( $matches[1] == '[' && $matches[6] == ']' ) { return substr($matches[0], 1, -1); } //对$matches[3]的url如果被解析器解析成链接，这些需要反解析回来 $matches[3] = preg_replace("/(.*?)<\/a>/",'$1',$matches[3]); $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表 $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数 //这里需要对id做一个判断，避免空值出现错误 $cid = @$attrs["cid"]; $url = @$attrs['url']; $cover = @$attrs['cover'];//封面 $targetTitle = "";//标题 $targetUrl = "";//链接 $targetSummary ="";//简介文字 $targetImgSrc = "";//封面图片地址 if (!empty($cid)){ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $posts = $db->fetchAll($db ->select()->from($prefix.'contents') ->orWhere('cid = ?',$cid) ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')); //这里需要对id正确性进行一个判断，避免查找文章失败 if (count($posts) == 0){ $targetTitle = "文章不存在，或文章是加密、私密文章"; }else{ $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]); if ($cover == ""){ $thumbArray = $db->fetchAll($db ->select()->from($prefix.'fields') ->orWhere('cid = ?',$cid) ->where('name = ? ', 'thumb')); $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value'],1); // * 这里！ 小小的“,1”作用极大 }else{ $targetImgSrc = $cover; } $targetSummary = Content::excerpt(Markdown::convert($result['text']),60); $targetTitle = $result['title']; $targetUrl = $result['permalink']; } }else if (empty($cid) && $url !=""){ $targetUrl = $url; $targetSummary = @$attrs['intro']; $targetTitle = @$attrs['title']; $targetImgSrc = $cover; }else{ $targetTitle = "文章不存在，请检查文章CID"; } $imageHtml = ""; $noImageCss = ""; if (trim($targetImgSrc) != ""){ $imageHtml = ' '; }else{ $noImageCss = 'style="margin-left: 10px;"'; } return <<<EOF {$imageHtml} {$targetTitle} {$targetSummary} EOF; }至此，修复工作全部完成。最终效果
主题现已更新至handsome 5.2.0。于是鸽子久违地需要动工了。以上就是这个项目更新的背景信息。5.2.0版本的bug的确存在，目前我能发现的就是阅读模式下几个显示和输出上的问题了，下面将问题清单列出以便后续查阅。[collapse status="false" title="目前能够发现的bug"]阅读模式下文章发布日期存在格式问题；阅读模式下本应显示文章发布日期处显示了当前日期；阅读模式内容中将自动替换a标签target="_blank"。（特性）[/collapse]好的，下面进入正题。[post cid="28" /]前情参见↑目前已完成更新，由于主题更新了阅读模式，导致原本的黑幕在阅读模式中无法渲染，于是稍作变更适配了一下；[heimu]同时更新了控制台彩蛋~pc端f12即可体验；[/heimu]终于优化了展现效果，现在给黑幕加上了渐变（基于transition实现）。关于[heimu]控制台彩蛋[/heimu]~
你是否困扰于既想写代码又想看视频呢？你是否曾经一边编辑枯燥的文档一边幻想着同时小窗播放视频呢？虽然一心不能二用，但在全屏编辑时打开一个不被遮挡的小窗口上上b站也确实惊人地闲适。为了更方便地偷税我的身心健康，我翻出八百年不动的易语言稍微构造了一个实现我愿望的程序。Main这个程序仅由一个置顶窗口组成，我将其入口链接固定为https://www.bilibili.com以便观看番剧与其他视频。因为简洁，所以快捷键也很重要：全局热键： Ctrl+Alt+T 显示/隐藏窗口边框(用于调整窗口大小及位置) 浏览框热键： F5 刷新 Ctrl+F5 刷新(无缓存) Esc 停止加载 Alt+[←] 后退 Alt+[→] 前进简单的使用说明：就如同一般的浏览器一样，只是极为简洁。窗口是置顶的，所以可以在全屏编辑的同时观看视频（事实上我编辑这篇文章时也在这么干，这是雪未来2019演唱会），但相应的可能会对内容造成遮挡，这时需要你手动调节。这时我有一个疑问，不知道你发现没有，为什么我老婆这么可爱？（逃Update Log2019.03.26 完成首个Release版本MiniBili 1.0，但极为原始，热键及配置支持不足2019.03.27 更新MiniBili 1.1，修复热键故障并增加简单配置支持，具体包括：设置独立缓存目录（重启程序后登录不会丢失），增加窗口位置配置文件，实现在关闭时自动记忆窗口的位置，下次启动时将自动适配到关闭时的位置2019.03.28 完善1.1版本，增加起始页面自定义支持（需手动修改runtime\config\costum.ini），修复了读取窗口位置时的逻辑漏洞，提供了更友好的最小化展现方式2019.03.30 继续完善1.1版本，版本号更新到1.1.2.5，修复Esc键不能正确传递给网页的故障，变更配置文件名称为page.ini，增加记忆关闭网址功能，设置StartPage=LastPage即可从上次关闭时的网页开始。此版本暂不发布，将会积累更新至1.3再行发布关于继续更新的计划：很可能鸽着，但根据定理懒是第一生产力，也可能会为了方便自己做进一步优化。PS：全局快捷键Ctrl+Alt+T致敬了Linux启动终端的快捷键。Download[button color="dark" icon="fa fa-download" url="https://pan.baidu.com/s/1aQC-uRMBh1L2uRuzZVMtoA"]立即下载[/button]提取密码：9qce当前版本：1.1.2.1SHA256：E6A1C8E82D69B7C7A7EF2D1D1A4EF941A230AFF5B8F09BD538DC77FC37F5E631