自新世界 #0x06:生活在 AST 上

rehype-remnote

写了一个可以从 RemNote 导出的 .rem 文件中生成 hast。hast 是 rehype 所用的抽象语法树,实际写下来感觉还是挺方便的。

.rem 文件结构

新的 .rem 文件本质是一个 .tar.gz 压缩包(以前则是 .zip),解压后有一个 rem.json 文件,里面包含了所有笔记的内容。应该是为了节省空间,所以这个 JSON 中很多字段的名称都很难以分辨。于是写了一个测试各种样式的页面,导出看效果,根据效果修改代码。

文件的结构大概是这样:

type 
type Workspace = {
    userId: string;
    knowledgebaseId: string;
    name: string;
    exportDate: string;
    exportVersion: number;
    documentRemToExportId: string;
    docs: Doc[];
}
Workspace
= {
userId: stringuserId: string; knowledgebaseId: stringknowledgebaseId: string; name: stringname: string; exportDate: stringexportDate: string; exportVersion: numberexportVersion: number; documentRemToExportId: stringdocumentRemToExportId: string; docs: Doc[]docs: type Doc = /*unresolved*/ anyDoc[]; }

整个 docs 的结构是扁平而有序的,通过 _id 来找到对应的内容。对于单个 Doc,主要的内容在 .key 中。.key 中存储的内容是 (string|object)[]。对于一张卡片,如果有涉及到闪卡的部分,可能会把背面的内容放在其他地方。

一些特殊的 block 类型会存储在几个特殊的 block 中,比如

type type RemTypes = "Daily Document" | "Document" | "Automatically Sort" | "Tags" | "Template Slot" | "Header" | "Website" | "Link" | "Quote" | "Image" | "Code" | "Card Item" | "List Item"RemTypes = "Daily Document"
| "Document"
| "Automatically Sort"
| "Tags"
| "Template Slot"
| "Header"
| "Website"
| "Link"
| "Quote"
| "Image"
| "Code"
| "Card Item"
| "List Item"

如何使用

可以参考 Rem.astro 文件。

  • 如果你只需要最后的 HTML,那么可以调用 rem2Html
  • 如果你需要将 rem.json 的内容提交到版本管理系统中,那么建议你先调用 hydrate 来生成一个精简的、树状的 JSON。
  • 如果你想要走完整个 unified 的 pipeline,得有一个 parser 来初始化 unist:
import { const unified: Processor<undefined, undefined, undefined, undefined, undefined>
Create a new processor.
@example This example shows how a new processor can be created (from `remark`) and linked to **stdin**(4) and **stdout**(4). ```js import process from 'node:process' import concatStream from 'concat-stream' import {remark} from 'remark' process.stdin.pipe( concatStream(function (buf) { process.stdout.write(String(remark().processSync(buf))) }) ) ```@returns New *unfrozen* processor (`processor`). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.
unified
, type type Parser<Tree extends import("unist").Node = Node> = (document: string, file: VFile) => Tree
A **parser** handles the parsing of text to a syntax tree. It is used in the parse phase and is called with a `string` and {@linkcode VFile } of the document to parse. It must return the syntax tree representation of the given file ( {@linkcode Node } ).
Parser
} from "unified";
function function remParse(): voidremParse() { const const self: anyself = this; const self: anyself.parser = function (local function) parser(doc: string, file: unknown): anyparser as type Parser<Tree extends import("unist").Node = Node> = (document: string, file: VFile) => Tree
A **parser** handles the parsing of text to a syntax tree. It is used in the parse phase and is called with a `string` and {@linkcode VFile } of the document to parse. It must return the syntax tree representation of the given file ( {@linkcode Node } ).
Parser
;
function function (local function) parser(doc: string, file: unknown): anyparser(doc: stringdoc: string, file: unknownfile: unknown) { return transformDoc(...); } } let let processor: anyprocessor = function unified(): Processor<undefined, undefined, undefined, undefined, undefined>
Create a new processor.
@example This example shows how a new processor can be created (from `remark`) and linked to **stdin**(4) and **stdout**(4). ```js import process from 'node:process' import concatStream from 'concat-stream' import {remark} from 'remark' process.stdin.pipe( concatStream(function (buf) { process.stdout.write(String(remark().processSync(buf))) }) ) ```@returns New *unfrozen* processor (`processor`). This processor is configured to work the same as its ancestor. When the descendant processor is configured in the future it does not affect the ancestral processor.
unified
().
Processor<undefined, undefined, undefined, undefined, undefined>.use<[], undefined, undefined>(plugin: Plugin<[], undefined, undefined>, ...parameters: [] | [boolean]): Processor<undefined, undefined, undefined, undefined, undefined> (+2 overloads)
Configure the processor to use a plugin, a list of usable values, or a preset. If the processor is already using a plugin, the previous plugin configuration is changed based on the options that are passed in. In other words, the plugin is not added a second time. > **Note**: `use` cannot be called on *frozen* processors. > Call the processor first to create a new unfrozen processor.
@example There are many ways to pass plugins to `.use()`. This example gives an overview: ```js import {unified} from 'unified' unified() // Plugin with options: .use(pluginA, {x: true, y: true}) // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): .use(pluginA, {y: false, z: true}) // Plugins: .use([pluginB, pluginC]) // Two plugins, the second with options: .use([pluginD, [pluginE, {}]]) // Preset with plugins and settings: .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) // Settings only: .use({settings: {position: false}}) ```@template{Array<unknown>} [Parameters=[]]@template{Node | string | undefined} [Input=undefined]@template[Output=Input]@overload@overload@overload@paramvalue Usable value.@paramparameters Parameters, when a plugin is given as a usable value.@returnsCurrent processor.
use
(function remParse(): voidremParse).
  • apply
  • arguments
  • bind
  • call
  • caller
  • compiler
  • data
  • freeze
  • length
  • name
  • parse
  • parser
  • process
  • processSync
  • prototype
  • run
  • runSync
  • stringify
  • toString
  • use
  • attachers
  • Compiler
  • copy
  • freezeIndex
  • frozen
  • namespace
  • Parser
  • transformers

如果你想要直接传 string 的话,可以跳过 process 的步骤,自行调用相应的 stage。


| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|

        +--------+                     +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
        +--------+          |          +----------+
                            X
                            |
                     +--------------+
                     | Transformers |
                     +--------------+

rehype-remnote/style 提供了一些基础的样式,可以导入使用。

TypeScript 类型体操之一个 Record 要么有一组 kv 的全部,要么全部没有

处理 rem.json 的时候发现很多时候结构体中的部分字段要么同时出现,要么同时不出现,遂有了下面的一些类型体操。你也可以在操场上玩耍

type type NoneOf<S> = { [K in keyof S]?: undefined; }NoneOf<function (type parameter) S in type NoneOf<S>S> = {[function (type parameter) KK in keyof function (type parameter) S in type NoneOf<S>S]?: never};
 
type type AllOrNone<B, S> = (B & S) | (B & NoneOf<S>)AllOrNone<function (type parameter) B in type AllOrNone<B, S>B, function (type parameter) S in type AllOrNone<B, S>S> = function (type parameter) B in type AllOrNone<B, S>B & function (type parameter) S in type AllOrNone<B, S>S | function (type parameter) B in type AllOrNone<B, S>B & type NoneOf<S> = { [K in keyof S]?: undefined; }NoneOf<function (type parameter) S in type AllOrNone<B, S>S>;
 
/////////////
 
type 
type Base = {
    a: number;
    b: string;
}
Base
= {
a: numbera: number, b: stringb: string, } type
type FeatSet = {
    c: number;
    d: string;
}
FeatSet
= {
c: numberc: number, d: stringd: string, } type type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)MyType = type AllOrNone<B, S> = (B & S) | (B & NoneOf<S>)AllOrNone<
type Base = {
    a: number;
    b: string;
}
Base
,
type FeatSet = {
    c: number;
    d: string;
}
FeatSet
>
const const dataAll: MyTypedataAll: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)MyType = { a: numbera: 1, b: stringb: "2", c: numberc: 3, d: stringd: "4" } const const dataNone: MyTypedataNone: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)MyType = { a: numbera: 1, b: stringb: "2", } const dataPartial: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)MyType = { // should throw
Type '{ a: number; b: string; c: number; }' is not assignable to type 'MyType'. Type '{ a: number; b: string; c: number; }' is not assignable to type 'Base & FeatSet'. Property 'd' is missing in type '{ a: number; b: string; c: number; }' but required in type 'FeatSet'.
a: numbera: 1, b: stringb: "2", c: numberc: 3, } /////////// type type ExpandToAllOrNoneHelper<T, U> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : TExpandToAllOrNoneHelper<function (type parameter) T in type ExpandToAllOrNoneHelper<T, U>T, function (type parameter) U in type ExpandToAllOrNoneHelper<T, U>U> = function (type parameter) U in type ExpandToAllOrNoneHelper<T, U>U extends [infer function (type parameter) FF, ...infer function (type parameter) RR] ? type ExpandToAllOrNoneHelper<T, U> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : TExpandToAllOrNoneHelper<type AllOrNone<B, S> = (B & S) | (B & NoneOf<S>)AllOrNone<function (type parameter) T in type ExpandToAllOrNoneHelper<T, U>T, function (type parameter) FF>, function (type parameter) RR> : function (type parameter) T in type ExpandToAllOrNoneHelper<T, U>T; type type AllOrNones<T, U extends any[]> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : TAllOrNones<function (type parameter) T in type AllOrNones<T, U extends any[]>T, function (type parameter) U in type AllOrNones<T, U extends any[]>U extends any[]> = type ExpandToAllOrNoneHelper<T, U> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : TExpandToAllOrNoneHelper<function (type parameter) T in type AllOrNones<T, U extends any[]>T, function (type parameter) U in type AllOrNones<T, U extends any[]>U>; type
type AnotherFeatSet = {
    e: boolean;
    f: "aaa" | "bbb";
}
AnotherFeatSet
= {
e: booleane: boolean, f: "aaa" | "bbb"f: "aaa" | "bbb" } type type MyTypeB = (AllOrNone<Base, FeatSet> & AnotherFeatSet) | (AllOrNone<Base, FeatSet> & NoneOf<AnotherFeatSet>)MyTypeB = type AllOrNones<T, U extends any[]> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : TAllOrNones<
type Base = {
    a: number;
    b: string;
}
Base
, [
type FeatSet = {
    c: number;
    d: string;
}
FeatSet
,
type AnotherFeatSet = {
    e: boolean;
    f: "aaa" | "bbb";
}
AnotherFeatSet
]>;
const const dataWithAnotherFeatSet: AllOrNone<AllOrNone<Base, FeatSet>, AnotherFeatSet>dataWithAnotherFeatSet: type MyTypeB = (AllOrNone<Base, FeatSet> & AnotherFeatSet) | (AllOrNone<Base, FeatSet> & NoneOf<AnotherFeatSet>)MyTypeB = { a: numbera: 11.4, b: stringb: "514", e: booleane: true, f: "aaa" | "bbb"f: "aaa" } const dataWithIncompleteFeatSet: type MyTypeB = (AllOrNone<Base, FeatSet> & AnotherFeatSet) | (AllOrNone<Base, FeatSet> & NoneOf<AnotherFeatSet>)MyTypeB = { // should throw
Type '{ a: number; b: string; f: "aaa"; }' is not assignable to type 'AllOrNone<AllOrNone<Base, FeatSet>, AnotherFeatSet>'. Type '{ a: number; b: string; f: "aaa"; }' is not assignable to type '(Base & FeatSet & AnotherFeatSet) | (Base & NoneOf<FeatSet> & AnotherFeatSet)'. Type '{ a: number; b: string; f: "aaa"; }' is not assignable to type 'Base & NoneOf<FeatSet> & AnotherFeatSet'. Property 'e' is missing in type '{ a: number; b: string; f: "aaa"; }' but required in type 'AnotherFeatSet'.
a: numbera: 11.4, b: stringb: "514", f: "aaa" | "bbb"f: "aaa", } const const dataWithIncompatibleFeatSet: AllOrNone<AllOrNone<Base, FeatSet>, AnotherFeatSet>dataWithIncompatibleFeatSet: type MyTypeB = (AllOrNone<Base, FeatSet> & AnotherFeatSet) | (AllOrNone<Base, FeatSet> & NoneOf<AnotherFeatSet>)MyTypeB = { a: numbera: 11.4, b: stringb: "514", e: truee: true, f: "233", // should throw
Type '"233"' is not assignable to type '"aaa" | "bbb" | undefined'.
}

如何在服务端(Astro SSR/SSG, etc.)使 UnoCSS 对动态字符串生效

RemNote 中可以设置文字和背景的颜色,可以是预设的颜色,也可以是自定义的颜色。因此我对应生成了 Tailwind 的 class 名。

但是,原子化 CSS 框架 UnoCSS 作用的原理是使用正则表达式扫描源码来生成规则。为了防止最终的结果 diverge,所以不会扫描产出结果。这意味着在 Astro 中,即使是 SSG 模式下也无法处理 JS 动态生成的字符串。

Bad.astro
---
const r = `<span class="text-red-600">红色</span>`;
const y = 'PHNwYW4gY2xhc3M9InRleHQteWVsbG93LTYwMCI+eWVsbG93PC9zcGFuPg==';
---
<div set:html={r} />
<div set:html={atob(y)} />
<div class="text-blue-600">蓝色</div>

(其中 atob(y) 的结果是 <span class="text-yellow-600">yellow</span>

渲染结果
红色
黄色
蓝色

解决方案

unocss 提供了一个 createGenerator 方法,可以用来生成 CSS 字符串。

import { createGenerator } from "unocss";
import unoConfig from "uno.config"; // 导入当前项目的 UnoCSS 配置
 
// ---cut-start---
/**
 * Generate UnoCSS style from HTML string
 * @param content HTML string
 * @returns CSS string
 */
// ---cut-end---
export async function generateUno(content: string, layer: string = 'default') {
  const generator = createGenerator(unoConfig);
  const style = await generator.generate(content);
  // 返回的内容为不同 layer 的 CSS 字符串,这里我们只用到了 default layer
  return style.getLayer(layer);
}

接着,在使用到的地方就可以调用我们的函数来生成 CSS 字符串了。

---
import { generateUno } from '@/scripts/uno';

const r = `<span class="text-red-600">红色</span>`;
const y = atob('PHNwYW4gY2xhc3M9InRleHQteWVsbG93LTYwMCI+eWVsbG93PC9zcGFuPg==');

const style = await generateUno(y);

export type Props = {};
---

<style is:global is:inline is:raw set:html={style} />

<div set:html={r} />
<div set:html={y} />
<div class="text-blue-600">蓝色</div>
渲染结果
红色
yellow
蓝色

我在玩什么

巴别塔圣歌

巴别塔圣歌 game 2023-09-06
Chants of Sennaar by RUNDISC
类型冒险 平台PC / PS4 / Xbox One / Nintendo Switch / XSX

高塔上的各个民族语言不通由来已久,传说某个旅人某日将破除隔阂,将他们团结起来。走进这座别样而又迷人的巴别塔,仔细观察、聆听,才能破译古代文字。 《Chants of Sennaar》 寻觅往昔的只言片语,揭示过去的秘密 自从创世之初,高塔各族便已分隔,彼此不再有言语往来,只是据说某日,某位旅人获得了智慧,能够推倒高墙,恢复天道。走进引人入胜的世界,探索缤纷诗意的设定,感受巴别塔传说的奇妙,帮人们忆起往事。置身庞大的迷宫之中,走过无穷无尽的阶梯,揭示苦涩的真相,揭晓迷人世界的秘密。在这里,你会发现:古老的语言既是锁头,也是钥匙。 踏上别有天地的旅途,破译有趣的古代文字 - 探索美妙而又迷人的世界,体验扎实的叙事,以不同的形式领略巴别塔的神话 - 仔细观察周围环境,破解谜题,揭开身边的秘密 - 利用潜行和智慧巧胜卫兵,穿过常人不可入内的禁区 - 破译古代文字,令高塔各族恢复联系与交流

玩到了第三层。有些地方很关键(比如第二层第一次出现双语对照文本的时候),如果错过了就会卡关。

第一层
第一层
第二层
第二层

第二层的地图把我搞得晕头转向的。

第三层
第三层

破译的另一大难点是画得很抽象。


这两天由于 Pulasan,降温很厉害,搞得我感冒了。恍惚间让我回忆起去年(还是前年)的秋天玩《2077》的时候。

我在看什么

网络炼狱:揭发N号房

网络炼狱:揭发N号房 movie 2022
사이버 지옥: n번방을 무너뜨려라 网路炼狱:揭发N号房 网络地狱:N 号房现形记 Cyber Hell: Exposing an Internet Horror by 崔真星
类型Documentary

一个可以匿名使用的在线聊天室,一个性犯罪的温床。揭发2019年震撼韩国的网络聊天室恶行。

分数都是给事件内的人物的。作为作品,表现手法比较欠缺。即使事件很大一部分都发生在网络上,但也没能看到一些像《网络迷踪》那样新颖的镜头设置,很多都是干巴巴的打字。

在技术上也有很多露馅的地方。比如,P 上去的右键菜单,레드팀用来钓鱼的背景的代码是原型链相关的操作。

评论

评论将在审核后显示,阁下可以在本博客的 Github 仓库的 拉取请求列表 中查看。提交成功后会自动跳转。

本站不支持 Dark Reader 的暗色模式,请对本站关闭后再访问。
(亮色模式的对比度、亮度等选项不受影响)