自新世界 #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 { function createGenerator<Theme extends object = object>(config?: UserConfig<Theme>, defaults?: UserConfigDefaults<Theme>): UnoGenerator<Theme>createGenerator } from "unocss";
import import unoConfigunoConfig from "uno.config"; // 导入当前项目的 UnoCSS 配置
 
export async function function generateUno(content: string, layer?: string): Promise<string | undefined>
Generate UnoCSS style from HTML string
@paramcontent HTML string@returnsCSS string
generateUno
(content: string
HTML string
@paramcontent HTML string
content
: string, layer: stringlayer: string = 'default') {
const const generator: UnoGenerator<object>generator = createGenerator<object>(config?: UserConfig<object> | undefined, defaults?: UserConfigDefaults<object> | undefined): UnoGenerator<object>createGenerator(import unoConfigunoConfig); const const style: GenerateResult<Set<string>>style = await const generator: UnoGenerator<object>generator.UnoGenerator<object>.generate(input: string | Set<string> | CountableSet<string> | string[], options?: GenerateOptions<false>): Promise<GenerateResult<Set<string>>> (+1 overload)generate(content: string
HTML string
@paramcontent HTML string
content
);
// 返回的内容为不同 layer 的 CSS 字符串,这里我们只用到了 default layer return const style: GenerateResult<Set<string>>style.GenerateResult<Set<string>>.getLayer: (name?: string) => string | undefinedgetLayer(layer: stringlayer); }

接着,在使用到的地方就可以调用我们的函数来生成 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 仓库的 拉取请求列表 中查看。
本表单无 JavaScript,请勿重复提交。

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


This site does not support dark mode by Dark Reader, please turn it off before visiting.
(Contrast, brightness, etc. of light mode are not affected)