跳到主要内容

· 阅读需 2 分钟
wen

前言

Docusaurus 是一个由 Facebook 开发的开源项目,用于构建静态网站。

它是一个基于 React 的静态网站生成器,可以帮助您快速创建网站。

Docusaurus 还提供了一个易于使用的博客插件,可以帮助您创建博客。

Docusaurus 默认不提供 RSS 导航菜单,在本文中,我们将介绍如何为 Docusaurus 博客添加 RSS 导航菜单。

怎么给 Docusaurus 博客 添加 RSS 导航菜单

docusaurus.config.js
module.exports = {
...
themeConfig: {
...
navbar: {
items: [
// blog rss
{
href: '/blog/rss.xml',
label: 'RSS',
position: 'right',
target: '_blank', // Open the link in a new tab/window
},
],
...

补充说明

BTW, 如果用 to 选项的话,点击 RSS 链接会报页面无法找到的 404 错误,

因为 Docusaurus 会把 to 选项的值当作一个页面的路径,而不是一个链接。

docusaurus.config.js
module.exports = {
...
themeConfig: {
...
navbar: {
items: [
// blog rss
{
to: '/blog/rss.xml',
label: 'RSS',
position: 'right',
},
],
...

· 阅读需 2 分钟
wen

问题

有 2 个数组,互相可能有重复的元素,如何合并这两个数组并去重?

比如有两个数组:

type User struct {
ID int // ID 作为唯一标识 (ID相同则认为是同一个元素)
Name string
}

old := []User{
{ID: 1, Name: "a"}, // only in old
{ID: 2, Name: "b"}, // 重复
}

new := []User{
{ID: 2, Name: "c"}, // 重复
{ID: 3, Name: "d"}, // only in new
}

合并后的结果应该是:

c := []User{
{ID: 1, Name: "a"}, // only in old
{ID: 2, Name: "c"}, // 重复 (保留 new 中的)
{ID: 3, Name: "d"}, // only in new
}

解决方案(一般化)

package main

import "fmt"

type User struct {
ID int
Name string
}

// contains Check if an element exists in a slice.
// keyFunc is used to uniquely identify the elements.
func contains(slice []any, item any, keyFunc func(any) any) bool {
for _, element := range slice {
if keyFunc(element) == keyFunc(item) {
return true
}
}
return false
}

// mergeSlices Merges two slices and removes duplicates.
//
// keyFunc is used to uniquely identify the elements.
// if an element exists in both old and new, the element in new takes precedence.
// old and new are assumed to have no duplicate elements.
// The order is not guaranteed.
func MergeSlices(old, new []any, keyFunc func(any) any) []any {
var merged []any

// copy new to merged
merged = append(merged, new...)

for _, item := range old {
if !contains(merged, item, keyFunc) {
merged = append(merged, item)
}
}

return merged
}

func main() {
old := []any{
User{ID: 1, Name: "a"},
User{ID: 2, Name: "b"}, // 重複
}
new := []any{
User{ID: 2, Name: "c"}, // 重複
User{ID: 3, Name: "d"},
}

mergedUsers := MergeSlices(old, new, func(item any) any {
return item.(User).ID
})
fmt.Printf("Merged Users:%+v", mergedUsers) // Merged Users:[ {ID:1 Name:a} {ID:2 Name:c} {ID:3 Name:d}]
}

· 阅读需 3 分钟
wen

背景和需求

我在写博客或者周报的时候,一般会这样管理我的分支:

  • 1). 创建带日期的分支,比如 blog/2024-01-28, weekly/2024-01-28
  • 2). 然后在这个分支上写好博客或者周报。
  • 3). 创建 PR,合并到 main 分支。
  • 4). 合并到 main 分支后,会触发 CD 自动部署。

步骤 3) 很容易忘记,所以我想通过 GitHub Actions 来自动化这个过程。

方案

创建一个 Github Actions 的 Workflow,每天定时检查分支名,

发现有当天的日期的分支 weekly/xxxx-xx-xx 的时候,自动创建 PR 并合并到 main 分支。

实现

准备工作

  1. 在 Github 中创建一个 Personal Access TokenPAT),用于创建和合并 PR。(BTW,到期时间最大只能设置 2 年) 1.1. 设置 PAT 的仓库权限: Repository accessOnly select repositories → 选择你的仓库。 1.2. 设置 PAT 的仓库访问权限: PermissionsRepository permissionsimg
  2. 在仓库中创建一个 secret,用于存放 Personal Access Token。

创建 Workflow

在仓库中创建 .github/workflows/merge-pr.yml 文件,内容如下:

name: Merge Weekly Posts Branches
on:
schedule:
- cron: "00 00 * * *"
workflow_dispatch:

jobs:
merge_weekly_branch:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Git
run: |
git config --global user.email "[email protected]"
git config --global user.name "GitHub Actions"

- name: Create and merge PR
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Personal Access Token
run: |
# Get the current date in the format 'YYYY-MM-DD'
current_date=$(date +%Y-%m-%d)

# Construct the branch name
branch_name="weekly/${current_date}"
# Fetch the branch to ensure it exists locally
git fetch origin ${branch_name}

# Check if the branch exists
if [[ -n "$(git ls-remote origin $branch_name)" ]]; then
git checkout ${branch_name}
# Create a pull request using GitHub CLI
gh pr create --base main --head $branch_name --title "weekly post PR" --body "This is a PR for weekly post changes."

# Wait for a few seconds to allow GitHub to process the PR creation
sleep 15
# Merge the pull request using GitHub CLI
gh pr merge --squash
else
echo "Weekly branch '${branch_name}' does not exist."
fi

· 阅读需 1 分钟
wen

Problem

I was trying to run a Makefile in VSCode and I got the following error:

Makefile:4: *** missing separator.  Stop.

Solution

The problem is that the Makefile is using tabs instead of spaces. To fix this, you can:

  1. Open the Makefile in VSCode
  2. Open the command palette with Ctrl + Shift + P or View > Command Palette. If you are using a Mac, you can use Cmd + Shift + P
  3. Search for Convert Indentation to Tabs

If you want to prevent this from happening in the future, you can:

  1. Open settgings in VSCode with Ctrl + , or File > Preferences > Settings. If you are using a Mac, you can use Cmd + ,
  2. Search for Insert Spaces
  3. Uncheck the box for Editor: Insert Spaces

img

· 阅读需 4 分钟
wen

问题

如何计算字符串中的(所见)字符数是一个常见的问题。

在 Golang 中,我们可以使用utf8.RuneCountInString()函数来计算字符串中的字符数。

中文这样的多字节字符也可以正确计算。

示例

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
str := "abc世界"
fmt.Println(utf8.RuneCountInString(str)) // 5
}

但是如果字符串中包含 👉🏻 这样的 emoji 字符,那么有些情况下这个函数就无法正确计算了。

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
emojiWorld := "🌍"
fmt.Println(utf8.RuneCountInString(emojiWorld)) // 1 ✅ 没有问题

emojiHand := "👉"
fmt.Println(utf8.RuneCountInString(emojiHand)) // 1 ✅ 没有问题

emojiHandBlack := "👉🏿"
fmt.Println(utf8.RuneCountInString(emojiHandBlack)) // 2 ❌ 有问题 期望是 1。 同一种 emoji,但是不同的皮肤颜色的字符数不一样

emojiOne := "1️⃣"
fmt.Println(utf8.RuneCountInString(emojiOne)) // 3 ❌ 有问题 期望是 1。
}

emoji 可以从这里emojipedia复制。

原因

这是因为有些 emoji 是多个 unicode 字符 (Code Points) 组合而成的,而 utf8.RuneCountInString() 函数只会计算 unicode 字符的数量。

术语描述
Bytes(字节)计算数据存储的最小单元,通常是 8 位二进制。
Code Units(编码单元)在编码方案中,用于表示一个字符的固定大小的单元。在 UTF-8 中,一个 Code Unit 是 8 位,而在 UTF-16 中,是 16 位。
Code Points(码点)在 Unicode 标准中,每个字符都被分配一个唯一的代码点,是一个用来标识字符的数字。例如,拉丁字母"A"的代码点是 U+0041。
Grapheme Clusters(字符簇)表示语言中可感知的最小字符单元,通常是一个或多个 Code Points 组成的序列。例如,字母加重音符可能是一个 Grapheme Cluster。

比如 1️⃣ 这个 emoji,它是由 3 个 Code Points 组成的,分别是:

img

Rendered by Markdown Table

提示

Emoji 的 Code Points 可以在这里emojipedia 查看。

解决方案

所以我们需要计算的是 Grapheme Clusters(字符簇)的数量,而不是 Code Points 的数量。

使用第三方库 rivo/uniseg

package main

import (
"fmt"

"github.com/rivo/uniseg"
)

func main() {
emojiWorld := "🌍"
fmt.Println(uniseg.GraphemeClusterCount(emojiWorld)) // 1 ✅ 没有问题

emojiHand := "👉"
fmt.Println(uniseg.GraphemeClusterCount(emojiHand)) // 1 ✅ 没有问题

emojiHandBlack := "👉🏿"
fmt.Println(uniseg.GraphemeClusterCount(emojiHandBlack)) // 1 ✅ 没有问题

emojiOne := "1️⃣"
fmt.Println(uniseg.GraphemeClusterCount(emojiOne)) // 1 ✅ 没有问题
}

补充

其实不只是 emoji,还有一些泰语,阿拉伯语的字符也是由多个 unicode 字符组成的。

Reference

Go で文字数をカウントする 在 Go 中计算字符数

文字数をカウントする 7 つの方法

Go: Unicode と rune 型

· 阅读需 1 分钟
wen

背景

labstack/gommon 的代码中 看到了这个库 valyala/fasttemplate, 于是就去 调查了一下。

提示

labstack 是 Echo Web Framework 的 Organization。

什么是 fasttemplate

fasttemplate 是一个高效的 Go 模板引擎,比 Go 标准库的模板引擎 text/template 快很多,

而且比 strings.Replace, strings.Replacerfmt.Fprintf 都要快。

img

具体可以看一下 fasttemplate 的 benchmark

fasttemplate 的使用

基础用法

    template := "http://{{host}}/?q={{query}}&foo={{bar}}{{bar}}"
t := fasttemplate.New(template, "{{", "}}")
s := t.ExecuteString(map[string]interface{}{
"host": "google.com",
"query": url.QueryEscape("hello=world"),
"bar": "foobar",
})
fmt.Printf("%s", s)

// Output:
// http://google.com/?q=hello%3Dworld&foo=foobarfoobar

高阶用法

    template := "Hello, [user]! You won [prize]!!! [foobar]"
t, err := fasttemplate.NewTemplate(template, "[", "]")
if err != nil {
log.Fatalf("unexpected error when parsing template: %s", err)
}
s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
switch tag {
case "user":
return w.Write([]byte("John"))
case "prize":
return w.Write([]byte("$100500"))
default:
return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag)))
}
})
fmt.Printf("%s", s)

// Output:
// Hello, John! You won $100500!!! [unknown tag "foobar"]

· 阅读需 3 分钟
wen

背景&目的

项目中使用的第三方服务(Shopify)的 API 隔几个月就会发布新版本。

为了及时对应 API 变化,我们希望在该服务的 API 的 RSS 有更新时,能够及时收到通知。

但是 Lark(飞书)并没有提供 RSS 订阅的功能,所以我们需要通过其他方式实现。

实现方法

通过 Zapier 将 RSS 新文章通知发送到 Lark(飞书)Webhook。

Zapier 是一个在线的自动化工作流程工具,可以将不同的应用程序连接起来,实现自动化工作流程。

实现步骤

0. 前提条件

  • Lark(飞书)中创建一个接受通知的 Group,并设置 Webhook,用于接收通知。

1. Zapier 中创建一个 Zap

分 2 步:

  • 1️⃣ 添加 RSS by Zapier, 填入 Shopify API RSS 地址, 用于获取 RSS 新文章通知。(图 1)

  • 2️⃣ 添加 Code by Zapier, 通过编写代码将 RSS 新文章发送到 Lark(飞书)Webhook。(图 2)

    • 如果是付费用户的话,可以使用 Webhook by Zapier 替代 Code by Zapier,省得写代码。
    • 代码语言支持 JavaScript 和 Python,这里用的是 JavaScript。

图 1 图 1 图 2 图 2

图 2 中的设置如下:

  • 1️⃣ 添加变量 Webhook,用于存放 Lark(飞书)Webhook 地址。
  • 2️⃣ 添加变量 Data,用于存放 RSS 新文章通知。 具体存放什么东西可以自己选择,这里存放的是 TitleLink等。
  • 3️⃣ 代码如下:
// 获取设置的变量
let Webhook = inputData.Webhook.trim();
let Method = "POST";

// 发送给 Webhook 的 JSON 对象 (注意格式要符合 Lark(飞书)Webhook 的要求)
let JSONObject = {
msg_type: "text",
content: { text: inputData.Data },
};

// creates the Method, Headers, and Body of the HTTP POST Request
let Options = {
method: Method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(JSONObject),
};

const Request = await fetch(Webhook, Options); // HTTP POST Request
const Response = await Request.json(); // HTTP POST Response

output = { Response, Request, Webhook, Method };

References

Make a HTTP POST Request to Fire a Webhook with Headers and Parameters via a Zap Code Step

· 阅读需 2 分钟
wen

0. Manim 是什么?

Manim 是一个用于创建数学动画的库,它是由 3Blue1BrownGrant Sanderson @X开发的。

Manim 的目标是提供一个用于创建数学动画的简单、快速且强大的工具。

它是一个用 Python 编写的开源项目,它的代码托管在 GitHub 上。

1. 最简单快速的安装方法: 利用 GitHub Codespaces

1). 打开(或者 fork)我配置好的仓库 wifecooky/manim-devcontainer,

1️⃣ 点击右上角的 Code 按钮, 2️⃣ 选择 Open with Codespaces,等待构建完成后,就可以在浏览器中使用 Manim 了。

img

2). 测试一下

1️⃣ : 打开 example_scenes.py 文件。

2️⃣ : 在 terminal 运行 manim -ql example_scenes.py SquareToCircle

3️⃣ : 等待运行完成,就可以在 media/videos 目录下看到生成的视频了。

img

2. 本地最快速的安装方法: 利用 Docker

0). 安装 Docker

已经安装过的请跳过这一步。

1). 拉取 Manim 镜像

docker pull manimcommunity/manim

2). 使用 Docker 启动本地 JupyterLab

docker run -it -p 8888:8888 manimcommunity/manim jupyter lab --ip=0.0.0.0

3). 打开 JupyterLab 链接并创建一个新的 Notebook

img

img

4). 测试一下

1️⃣ : 在 Notebook 中运行以下代码

%%manim -qm -v WARNING SquareToCircle

class SquareToCircle(Scene):
def construct(self):
square = Square()
circle = Circle()
circle.set_fill(PINK, opacity=0.5)
self.play(Create(square))
self.play(Transform(square, circle))
self.wait()
提示

%%manim 是 Manim 的 Jupyter magic 命令,用于链接 Binder 来运行 Manim。

-qm 参数表示 quality medium,即生成的视频质量为中等。

-v WARNING 参数表示只显示警告信息。

img

Reference

Manim easy Installation for all operating systems (Windows, Linux, Mac OS)

docker manimcommunity/manim

· 阅读需 2 分钟
wen

问题

Docusaurus(3.0) 的文档侧边栏默认是按文件的创建日期的升序排列的。

这样的排序方式对于我管理和查找文档来说并不是很方便。

我更喜欢按照日期倒序排列,这样我就可以更容易地找到最新的文档。

img

解决方案

修改 docusaurus.config.js 文件,如下所示:

docusaurus.config.js
+// Reverse the sidebar items ordering (including nested category items)
+function reverseSidebarItems(items) {
+ // Reverse items in categories
+ const result = items.map((item) => {
+ if (item.type === 'category') {
+ return {...item, items: reverseSidebarItems(item.items)};
+ }
+ return item;
+ });
+ // Reverse items at current level
+ result.reverse();
+ return result;
+}

/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'thewang',
...
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
routeBasePath: 'weekly',
sidebarPath: require.resolve('./sidebars.js'),
showLastUpdateTime: true,
showLastUpdateAuthor: true,
sidebarCollapsed: false,
+ async sidebarItemsGenerator({defaultSidebarItemsGenerator, ...args}) {
+ const sidebarItems = await defaultSidebarItemsGenerator(args);
+ return reverseSidebarItems(sidebarItems);
+ return sidebarItems;
+ },
},
...

References

Customize the sidebar items generator

· 阅读需 1 分钟
wen

问题

当你在同一台电脑上使用多个 Git 用户的时候,你可能会遇到在 commit push 之后才发现自己没有切换到正确的用户的问题。

为了避免这种情况,我们可以在终端显示当前的 Git 用户信息。

img

解决方案

终端的显示信息(Shell Prompt) ,我推荐 Starship 配置来实现。

安装 Starship 可以参考官方文档, 这里就不再赘述。

配置

安装完后在 Starship 的配置文件 ~/.config/starship.toml 中添加以下配置, 格式可以按照自己的喜好修改 format = 部分 。

~/.config/starship.toml
format = """
...
${custom.git_username}\
...

[custom.git_username]
command = "git config user.name"
when = "[ -d .git ] && echo .git || git rev-parse --git-dir > /dev/null 2>&1"
format = ' [$symbol($output)@git]($style) '