自新世界 #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 的暗色模式,请对本站关闭后再访问。
(亮色模式的对比度、亮度等选项不受影响)