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 上去的右键菜单,레드팀用来钓鱼的背景的代码是原型链相关的操作。
评论