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: string
userId: string;
knowledgebaseId: string
knowledgebaseId: string;
name: string
name: string;
exportDate: string
exportDate: string;
exportVersion: number
exportVersion: number;
documentRemToExportId: string
documentRemToExportId: string;
docs: Doc[]
docs: type Doc = /*unresolved*/ any
Doc[];
}
整個 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.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(): void
remParse() {
const const self: any
self = this;
const self: any
self.parser = function (local function) parser(doc: string, file: unknown): any
parser 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): any
parser(doc: string
doc: string, file: unknown
file: unknown) {
return transformDoc(...);
}
}
let let processor: any
processor = function unified(): Processor<undefined, undefined, undefined, undefined, undefined>
Create a new 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.use(function remParse(): void
remParse).- 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) K
K 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: number
a: number,
b: string
b: string,
}
type type FeatSet = {
c: number;
d: string;
}
FeatSet = {
c: number
c: number,
d: string
d: 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: MyType
dataAll: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)
MyType = {
a: number
a: 1,
b: string
b: "2",
c: number
c: 3,
d: string
d: "4"
}
const const dataNone: MyType
dataNone: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)
MyType = {
a: number
a: 1,
b: string
b: "2",
}
const dataPartial: type MyType = (Base & FeatSet) | (Base & NoneOf<FeatSet>)
MyType = { // should throw a: number
a: 1,
b: string
b: "2",
c: number
c: 3,
}
///////////
type type ExpandToAllOrNoneHelper<T, U> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : T
ExpandToAllOrNoneHelper<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) F
F, ...infer function (type parameter) R
R]
? type ExpandToAllOrNoneHelper<T, U> = U extends [infer F, ...infer R] ? ExpandToAllOrNoneHelper<AllOrNone<T, F>, R> : T
ExpandToAllOrNoneHelper<type AllOrNone<B, S> = (B & S) | (B & NoneOf<S>)
AllOrNone<function (type parameter) T in type ExpandToAllOrNoneHelper<T, U>
T, function (type parameter) F
F>, function (type parameter) R
R>
: 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> : T
AllOrNones<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> : T
ExpandToAllOrNoneHelper<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: boolean
e: 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> : T
AllOrNones<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: number
a: 11.4,
b: string
b: "514",
e: boolean
e: true,
f: "aaa" | "bbb"
f: "aaa"
}
const dataWithIncompleteFeatSet: type MyTypeB = (AllOrNone<Base, FeatSet> & AnotherFeatSet) | (AllOrNone<Base, FeatSet> & NoneOf<AnotherFeatSet>)
MyTypeB = { // should throw a: number
a: 11.4,
b: string
b: "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: number
a: 11.4,
b: string
b: "514",
e: true
e: true,
f: "233", // should throw}
如何在服務端(Astro SSR/SSG, etc.)使 UnoCSS 對動態字串生效
RemNote 中可以設定文字和背景的顏色,可以是預設的顏色,也可以是自定義的顏色。因此我對應生成了 Tailwind 的 class 名。
但是,原子化 CSS 框架 UnoCSS 作用的原理是使用正規表示式掃描原始碼來生成規則。為了防止最終的結果 diverge,所以不會掃描產出結果。這意味著在 Astro 中,即使是 SSG 模式下也無法處理 JS 動態生成的字串。
例
---
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>
我在玩什麼
巴別塔聖歌
巴別塔聖歌 game 2023-09-06
高塔上的各個民族語言不通由來已久,傳說某個旅人某日將破除隔閡,將他們團結起來。走進這座別樣而又迷人的巴別塔,仔細觀察、聆聽,才能破譯古代文字。 《Chants of Sennaar》 尋覓往昔的隻言片語,揭示過去的秘密 自從創世之初,高塔各族便已分隔,彼此不再有言語往來,只是據說某日,某位旅人獲得了智慧,能夠推倒高牆,恢復天道。走進引人入勝的世界,探索繽紛詩意的設定,感受巴別塔傳說的奇妙,幫人們憶起往事。置身龐大的迷宮之中,走過無窮無盡的階梯,揭示苦澀的真相,揭曉迷人世界的秘密。在這裡,你會發現:古老的語言既是鎖頭,也是鑰匙。 踏上別有天地的旅途,破譯有趣的古代文字 - 探索美妙而又迷人的世界,體驗紮實的敘事,以不同的形式領略巴別塔的神話 - 仔細觀察周圍環境,破解謎題,揭開身邊的秘密 - 利用潛行和智慧巧勝衛兵,穿過常人不可入內的禁區 - 破譯古代文字,令高塔各族恢復聯絡與交流
玩到了第三層。有些地方很關鍵(比如第二層第一次出現雙語對照文字的時候),如果錯過了就會卡關。


第二層的地圖把我搞得暈頭轉向的。

破譯的另一大難點是畫得很抽象。
這兩天由於 Pulasan,降溫很厲害,搞得我感冒了。恍惚間讓我回憶起去年(還是前年)的秋天玩《2077》的時候。
我在看什麼
網路煉獄:揭發N號房
網路煉獄:揭發N號房 movie 2022
一個可以匿名使用的線上聊天室,一個性犯罪的溫床。揭發2019年震撼韓國的網路聊天室惡行。
分數都是給事件內的人物的。作為作品,表現手法比較欠缺。即使事件很大一部分都發生在網路上,但也沒能看到一些像《網路迷蹤》那樣新穎的鏡頭設定,很多都是乾巴巴的打字。
在技術上也有很多露餡的地方。比如,P 上去的右鍵選單,레드팀用來釣魚的背景的程式碼是原型鏈相關的操作。
評論