自新世界 #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)