@morlay/dsh-client-ui-primitives 0.0.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/cordis.patch.yml +3 -0
- package/dist/client.cjs +13715 -0
- package/dist/client.d.cts +10883 -0
- package/dist/client.d.mts +10883 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +5 -0
- package/dist/reference-BLcMVsqa.mjs +406 -0
- package/dist/reference-Do8Uwujy.d.cts +29 -0
- package/dist/reference-Do8Uwujy.d.mts +29 -0
- package/package.json +66 -0
- package/src/client/index.ts +20 -0
- package/src/client/markdown-labels.ts +10 -0
- package/src/client/markdown-text.ts +6 -0
- package/src/client/styling/css.ts +27 -0
- package/src/client/styling/index.ts +6 -0
- package/src/client/styling/styled.tsx +56 -0
- package/src/client/styling/styling.ts +137 -0
- package/src/client/styling/token.ts +275 -0
- package/src/client/styling/toolkit.ts +37 -0
- package/src/client/theme.generated.ts +724 -0
- package/src/client/theme.ts +5 -0
- package/src/index.ts +4 -0
- package/src/reference-markdown.tsx +136 -0
- package/src/reference.ts +552 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// 引用 → 官方 markdown 渲染器的 chip。
|
|
2
|
+
//
|
|
3
|
+
// 解析与渲染分离:reference.ts 负责解析成统一结构的引用,这里只做「引用 → 官方渲染器
|
|
4
|
+
// 认得的形态」这一步转换,渲染本身完全交给官方 MarkdownText(不 fork 渲染器)。
|
|
5
|
+
//
|
|
6
|
+
// 转换规则:本地引用(file / skill / dsh-session …,见 isLocalReference)的原文切片
|
|
7
|
+
// 换成 inline code——官方唯一的 chip 钩子是 fileMentions,它只认 inline code,产出
|
|
8
|
+
// icon + 文本的 chip,点击走 actions。http(s) / mailto 这类外部地址保持 link 原文,
|
|
9
|
+
// 官方渲染器照常出锚点。
|
|
10
|
+
//
|
|
11
|
+
// inline code 里的文本是**省略 protocol 的显示文本**(图标已经表示协议:file 走文件图标,
|
|
12
|
+
// label 有就用 label);判定不靠文本形态,而是本次转换产出的 value → Reference 映射,
|
|
13
|
+
// 因此 `code-review` 这种没有路径特征的 skill 也能稳定成 chip、而普通 inline code 不会。
|
|
14
|
+
|
|
15
|
+
import { memo, useMemo } from "react";
|
|
16
|
+
import { MarkdownText } from "./client/markdown-text.ts";
|
|
17
|
+
import type {
|
|
18
|
+
MarkdownFileMentions,
|
|
19
|
+
MarkdownLabels,
|
|
20
|
+
MarkdownPathImages,
|
|
21
|
+
} from "./client/markdown-text.ts";
|
|
22
|
+
import {
|
|
23
|
+
findReferences,
|
|
24
|
+
formatReference,
|
|
25
|
+
isLocalReference,
|
|
26
|
+
parseReferenceToken,
|
|
27
|
+
} from "./reference.ts";
|
|
28
|
+
import type { Reference } from "./reference.ts";
|
|
29
|
+
|
|
30
|
+
// chip 的点击目标:file → openFile(path),skill → openSkill(name)。
|
|
31
|
+
// 没有 actions 时 chip 照旧渲染,只是点击无效果。
|
|
32
|
+
export interface ReferenceActions {
|
|
33
|
+
readonly openFile: (path: string) => void;
|
|
34
|
+
readonly openSkill: (name: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReferenceMarkdownProps {
|
|
38
|
+
readonly text: string;
|
|
39
|
+
readonly labels: MarkdownLabels;
|
|
40
|
+
readonly actions?: ReferenceActions | undefined;
|
|
41
|
+
readonly streaming?: boolean | undefined;
|
|
42
|
+
// 透传给官方渲染器:图片目标是本地路径时的改写词表(替换 MarkdownText 时不丢这个能力)。
|
|
43
|
+
readonly pathImages?: MarkdownPathImages | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 把引用接进官方 fileMentions 钩子:value 是 chip 上的显示文本,title 是完整引用。
|
|
47
|
+
// 不是本次转换产出的引用(普通 inline code)返回 undefined,渲染器按普通 code 处理。
|
|
48
|
+
export function referenceMentions(
|
|
49
|
+
actions?: ReferenceActions,
|
|
50
|
+
tokens?: ReadonlyMap<string, Reference>,
|
|
51
|
+
): MarkdownFileMentions {
|
|
52
|
+
return {
|
|
53
|
+
resolve: (value) => {
|
|
54
|
+
const reference = tokens?.get(value) ?? parseReferenceToken(value);
|
|
55
|
+
if (reference === undefined) return undefined;
|
|
56
|
+
return {
|
|
57
|
+
label: value,
|
|
58
|
+
title: formatReference(reference),
|
|
59
|
+
open: () => {
|
|
60
|
+
openReference(reference, actions);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 引用感知的 markdown 渲染。text 每帧变化时只重跑一次「引用 → 显示文本」的转换(命中引用才
|
|
68
|
+
// 改文本),解析与渲染的增量缓存仍由官方 MarkdownText 负责。
|
|
69
|
+
export const ReferenceMarkdown = memo(function ReferenceMarkdown({
|
|
70
|
+
text,
|
|
71
|
+
labels,
|
|
72
|
+
actions,
|
|
73
|
+
streaming = false,
|
|
74
|
+
pathImages,
|
|
75
|
+
}: ReferenceMarkdownProps) {
|
|
76
|
+
const { source, tokens } = useMemo(() => tokenizeReferences(text), [text]);
|
|
77
|
+
// actions / tokens 的 identity 决定 mentions 的 identity:官方 MarkdownText 在 fileMentions
|
|
78
|
+
// 上 memo(流式期间换 identity 会丢渲染缓存),所以按两者记忆。
|
|
79
|
+
const mentions = useMemo(() => referenceMentions(actions, tokens), [actions, tokens]);
|
|
80
|
+
return (
|
|
81
|
+
<MarkdownText
|
|
82
|
+
text={source}
|
|
83
|
+
streaming={streaming}
|
|
84
|
+
labels={labels}
|
|
85
|
+
fileMentions={mentions}
|
|
86
|
+
pathImages={pathImages}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// chip 上的显示文本:图标已经表示 protocol,所以省略 scheme——有 label(markdown link /
|
|
92
|
+
// mention)就用 label,否则用 path(带行号 fragment)。反引号会截断 inline code,替换掉。
|
|
93
|
+
function displayTextOf(reference: Reference): string {
|
|
94
|
+
const base = reference.title ?? `${reference.path ?? ""}${rangeOf(reference)}`;
|
|
95
|
+
return base.replaceAll("`", "'");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rangeOf(reference: Reference): string {
|
|
99
|
+
if (reference.lineStart === undefined) return "";
|
|
100
|
+
const column = reference.column === undefined ? "" : `C${String(reference.column)}`;
|
|
101
|
+
return reference.lineEnd === undefined
|
|
102
|
+
? `#L${String(reference.lineStart)}${column}`
|
|
103
|
+
: `#L${String(reference.lineStart)}${column}-L${String(reference.lineEnd)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 本地引用的原文切片 → inline code;外部地址原样留下。引用不会嵌套(解析器保证),
|
|
107
|
+
// 这里仍按 offset 顺序跳过被前一个切片覆盖的引用。
|
|
108
|
+
// 边界:引用紧贴相邻 inline code 的反引号(`` `x`file:y ``)时两个 code span 会并成一个,
|
|
109
|
+
// 这种写法退化成普通 code——与上一版包反引号的行为一致。
|
|
110
|
+
function tokenizeReferences(text: string): {
|
|
111
|
+
readonly source: string;
|
|
112
|
+
readonly tokens: Map<string, Reference>;
|
|
113
|
+
} {
|
|
114
|
+
const tokens = new Map<string, Reference>();
|
|
115
|
+
let out = "";
|
|
116
|
+
let cursor = 0;
|
|
117
|
+
for (const span of findReferences(text)) {
|
|
118
|
+
if (!isLocalReference(span.reference) || span.start < cursor) continue;
|
|
119
|
+
const display = displayTextOf(span.reference);
|
|
120
|
+
tokens.set(display, span.reference);
|
|
121
|
+
out += `${text.slice(cursor, span.start)}\`${display}\``;
|
|
122
|
+
cursor = span.end;
|
|
123
|
+
}
|
|
124
|
+
return { source: cursor === 0 ? text : out + text.slice(cursor), tokens };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// skill 认名字、file 认路径;其它 protocol(dsh-session 等)渲染成 chip 但还没有点击目标。
|
|
128
|
+
function openReference(reference: Reference, actions: ReferenceActions | undefined): void {
|
|
129
|
+
const path = reference.path;
|
|
130
|
+
if (actions === undefined || path === undefined) return;
|
|
131
|
+
if (reference.protocol === "skill") {
|
|
132
|
+
actions.openSkill(path);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (reference.protocol === "file") actions.openFile(path);
|
|
136
|
+
}
|
package/src/reference.ts
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import type { Nodes, Root } from "mdast";
|
|
2
|
+
import type {
|
|
3
|
+
CompileContext,
|
|
4
|
+
Extension as MdastExtension,
|
|
5
|
+
Transform,
|
|
6
|
+
} from "mdast-util-from-markdown";
|
|
7
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
8
|
+
import { gfmFromMarkdown } from "mdast-util-gfm";
|
|
9
|
+
import {
|
|
10
|
+
asciiAlpha,
|
|
11
|
+
asciiAlphanumeric,
|
|
12
|
+
asciiDigit,
|
|
13
|
+
markdownLineEndingOrSpace,
|
|
14
|
+
unicodePunctuation,
|
|
15
|
+
unicodeWhitespace,
|
|
16
|
+
} from "micromark-util-character";
|
|
17
|
+
import { gfm } from "micromark-extension-gfm";
|
|
18
|
+
import { codes, types } from "micromark-util-symbol";
|
|
19
|
+
import type {
|
|
20
|
+
Code,
|
|
21
|
+
Construct,
|
|
22
|
+
Event,
|
|
23
|
+
Extension,
|
|
24
|
+
Previous,
|
|
25
|
+
State,
|
|
26
|
+
Token,
|
|
27
|
+
Tokenizer,
|
|
28
|
+
} from "micromark-util-types";
|
|
29
|
+
|
|
30
|
+
declare module "micromark-util-types" {
|
|
31
|
+
interface TokenTypeMap {
|
|
32
|
+
reference: "reference";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Reference {
|
|
37
|
+
readonly protocol: string;
|
|
38
|
+
|
|
39
|
+
readonly origin?: string;
|
|
40
|
+
|
|
41
|
+
readonly path?: string;
|
|
42
|
+
|
|
43
|
+
readonly title?: string;
|
|
44
|
+
readonly lineStart?: number;
|
|
45
|
+
readonly lineEnd?: number;
|
|
46
|
+
readonly column?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReferenceSpan {
|
|
50
|
+
readonly start: number;
|
|
51
|
+
readonly end: number;
|
|
52
|
+
readonly reference: Reference;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_PROTOCOL = "file";
|
|
56
|
+
|
|
57
|
+
const EXTERNAL_PROTOCOLS: readonly string[] = ["http", "https", "mailto"];
|
|
58
|
+
|
|
59
|
+
const LINE_FRAGMENT = /^L(\d+)(?:C(\d+))?(?:-L(\d+))?$/u;
|
|
60
|
+
|
|
61
|
+
const SCHEME = /^([A-Za-z][A-Za-z0-9+.-]*):/u;
|
|
62
|
+
|
|
63
|
+
const BARE_PATH = /^([^:]+):(\d+)(?::(\d+))?$/u;
|
|
64
|
+
|
|
65
|
+
const URI_UNSAFE = /[%#()\s<>"`]/gu;
|
|
66
|
+
|
|
67
|
+
const PATH_PUNCTUATION: readonly Code[] = [
|
|
68
|
+
codes.slash,
|
|
69
|
+
codes.dash,
|
|
70
|
+
codes.underscore,
|
|
71
|
+
codes.dot,
|
|
72
|
+
codes.plusSign,
|
|
73
|
+
codes.percentSign,
|
|
74
|
+
codes.atSign,
|
|
75
|
+
codes.tilde,
|
|
76
|
+
codes.equalsTo,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const TRIGGERS: readonly string[] = [":", "[", "@"];
|
|
80
|
+
|
|
81
|
+
export function parseReference(value: string, title?: string): Reference | undefined {
|
|
82
|
+
const raw = value.trim();
|
|
83
|
+
if (raw === "") return undefined;
|
|
84
|
+
const matched = SCHEME.exec(raw);
|
|
85
|
+
const protocol = matched === null ? DEFAULT_PROTOCOL : (matched[1] as string).toLowerCase();
|
|
86
|
+
let rest = matched === null ? raw : raw.slice((matched[0] as string).length);
|
|
87
|
+
|
|
88
|
+
if (matched !== null && /\s/u.test(rest)) return undefined;
|
|
89
|
+
const fragment = splitLineFragment(rest);
|
|
90
|
+
rest = fragment.head;
|
|
91
|
+
|
|
92
|
+
let origin: string | undefined;
|
|
93
|
+
if (rest.startsWith("//")) {
|
|
94
|
+
const end = authorityEnd(rest);
|
|
95
|
+
const authority = rest.slice(2, end);
|
|
96
|
+
if (authority === "") return undefined;
|
|
97
|
+
origin = `${protocol}://${authority}`;
|
|
98
|
+
rest = rest.slice(end).replace(/^\//u, "");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const decoded = protocol === DEFAULT_PROTOCOL ? decodeUri(rest) : rest;
|
|
102
|
+
if (decoded === undefined || (decoded === "" && origin === undefined)) return undefined;
|
|
103
|
+
return {
|
|
104
|
+
protocol,
|
|
105
|
+
...(origin === undefined ? {} : { origin }),
|
|
106
|
+
...(decoded === "" ? {} : { path: decoded }),
|
|
107
|
+
...(title === undefined || title === "" ? {} : { title }),
|
|
108
|
+
...fragment.lines,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function parseReferenceToken(value: string): Reference | undefined {
|
|
113
|
+
return SCHEME.test(value) ? parseReference(value) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatReference(reference: Reference): string {
|
|
117
|
+
const protocol = reference.protocol === "" ? DEFAULT_PROTOCOL : reference.protocol;
|
|
118
|
+
const path = reference.path ?? "";
|
|
119
|
+
|
|
120
|
+
if (reference.origin !== undefined) {
|
|
121
|
+
return `${reference.origin}${path === "" ? "" : `/${path}`}`;
|
|
122
|
+
}
|
|
123
|
+
return `${protocol}:${encodeUri(path)}${lineFragmentOf(reference)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isLocalReference(reference: Reference): boolean {
|
|
127
|
+
return !EXTERNAL_PROTOCOLS.includes(reference.protocol);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function findReferences(text: string): readonly ReferenceSpan[] {
|
|
131
|
+
const spans: ReferenceSpan[] = [];
|
|
132
|
+
if (text === "" || !TRIGGERS.some((trigger) => text.includes(trigger))) return spans;
|
|
133
|
+
collectReferences(parseReferenceDocument(text) as unknown as MarkdownNode, spans);
|
|
134
|
+
return spans;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseReferenceDocument(text: string): Root {
|
|
138
|
+
return fromMarkdown(text, {
|
|
139
|
+
extensions: [gfm(), referenceSyntax()],
|
|
140
|
+
mdastExtensions: [gfmFromMarkdown(), referenceFromMarkdown()],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface MarkdownNode {
|
|
145
|
+
type: string;
|
|
146
|
+
value?: string;
|
|
147
|
+
url?: string;
|
|
148
|
+
children?: MarkdownNode[];
|
|
149
|
+
position?: { start: MarkdownPoint; end: MarkdownPoint };
|
|
150
|
+
protocol?: string;
|
|
151
|
+
origin?: string;
|
|
152
|
+
path?: string;
|
|
153
|
+
title?: string;
|
|
154
|
+
lineStart?: number;
|
|
155
|
+
lineEnd?: number;
|
|
156
|
+
column?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface MarkdownPoint {
|
|
160
|
+
readonly line: number;
|
|
161
|
+
readonly column: number;
|
|
162
|
+
readonly offset: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function collectReferences(node: MarkdownNode, spans: ReferenceSpan[]): void {
|
|
166
|
+
const position = node.position;
|
|
167
|
+
if (node.type === "reference" && position !== undefined) {
|
|
168
|
+
const reference = referenceOfNode(node);
|
|
169
|
+
if (reference !== undefined) {
|
|
170
|
+
spans.push({ start: position.start.offset, end: position.end.offset, reference });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const child of node.children ?? []) collectReferences(child, spans);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function referenceOfNode(node: MarkdownNode): Reference | undefined {
|
|
177
|
+
const protocol = node.protocol;
|
|
178
|
+
if (protocol === undefined) return undefined;
|
|
179
|
+
return {
|
|
180
|
+
protocol,
|
|
181
|
+
...(node.origin === undefined ? {} : { origin: node.origin }),
|
|
182
|
+
...(node.path === undefined ? {} : { path: node.path }),
|
|
183
|
+
...(node.title === undefined ? {} : { title: node.title }),
|
|
184
|
+
...(node.lineStart === undefined ? {} : { lineStart: node.lineStart }),
|
|
185
|
+
...(node.lineEnd === undefined ? {} : { lineEnd: node.lineEnd }),
|
|
186
|
+
...(node.column === undefined ? {} : { column: node.column }),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const previousReference: Previous = function (code) {
|
|
191
|
+
return isBoundary(code);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const tokenizeReference: Tokenizer = function (effects, ok, nok) {
|
|
195
|
+
const { events } = this;
|
|
196
|
+
let at = false;
|
|
197
|
+
let head = "";
|
|
198
|
+
|
|
199
|
+
return start;
|
|
200
|
+
|
|
201
|
+
function start(code: Code): State | undefined {
|
|
202
|
+
if (insideLabel(events)) return nok(code);
|
|
203
|
+
effects.enter("reference");
|
|
204
|
+
if (code === codes.atSign) {
|
|
205
|
+
at = true;
|
|
206
|
+
effects.consume(code);
|
|
207
|
+
return afterAt;
|
|
208
|
+
}
|
|
209
|
+
return headStart(code);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function afterAt(code: Code): State | undefined {
|
|
213
|
+
return asciiAlpha(code) ? headStart(code) : pathStart(code);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function headStart(code: Code): State | undefined {
|
|
217
|
+
if (!asciiAlpha(code)) return nok(code);
|
|
218
|
+
head = "";
|
|
219
|
+
return headInside(code);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function headInside(code: Code): State | undefined {
|
|
223
|
+
if (code === null || !asciiAlpha(code)) return afterHead(code);
|
|
224
|
+
head += String.fromCodePoint(code);
|
|
225
|
+
effects.consume(code);
|
|
226
|
+
return headInside;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function afterHead(code: Code): State | undefined {
|
|
230
|
+
if (code === codes.colon && isKnownScheme(head)) {
|
|
231
|
+
effects.consume(code);
|
|
232
|
+
return head.toLowerCase() === "skill" ? skillInside : fileInside;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return at ? pathInside(code) : nok(code);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function pathStart(code: Code): State | undefined {
|
|
239
|
+
if (!isPathCode(code) || code === codes.atSign) return nok(code);
|
|
240
|
+
effects.consume(code);
|
|
241
|
+
return pathInside;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function fileInside(code: Code): State | undefined {
|
|
245
|
+
if (!isFileCode(code)) return nok(code);
|
|
246
|
+
effects.consume(code);
|
|
247
|
+
return fileRest;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function fileRest(code: Code): State | undefined {
|
|
251
|
+
if (isFileCode(code)) {
|
|
252
|
+
effects.consume(code);
|
|
253
|
+
return fileRest;
|
|
254
|
+
}
|
|
255
|
+
return finish(code);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function skillInside(code: Code): State | undefined {
|
|
259
|
+
if (!isSkillCode(code)) return nok(code);
|
|
260
|
+
effects.consume(code);
|
|
261
|
+
return skillRest;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function skillRest(code: Code): State | undefined {
|
|
265
|
+
if (isSkillCode(code)) {
|
|
266
|
+
effects.consume(code);
|
|
267
|
+
return skillRest;
|
|
268
|
+
}
|
|
269
|
+
return finish(code);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pathInside(code: Code): State | undefined {
|
|
273
|
+
if (isPathCode(code) || code === codes.numberSign) {
|
|
274
|
+
effects.consume(code);
|
|
275
|
+
return pathInside;
|
|
276
|
+
}
|
|
277
|
+
if (code !== codes.colon) return isBoundary(code) ? finish(code) : nok(code);
|
|
278
|
+
effects.consume(code);
|
|
279
|
+
return lineInside;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function lineInside(code: Code): State | undefined {
|
|
283
|
+
if (!asciiDigit(code)) return nok(code);
|
|
284
|
+
effects.consume(code);
|
|
285
|
+
return lineRest;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function lineRest(code: Code): State | undefined {
|
|
289
|
+
if (asciiDigit(code)) {
|
|
290
|
+
effects.consume(code);
|
|
291
|
+
return lineRest;
|
|
292
|
+
}
|
|
293
|
+
if (code !== codes.colon) return finish(code);
|
|
294
|
+
effects.consume(code);
|
|
295
|
+
return columnInside;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function columnInside(code: Code): State | undefined {
|
|
299
|
+
if (!asciiDigit(code)) return nok(code);
|
|
300
|
+
effects.consume(code);
|
|
301
|
+
return columnRest;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function columnRest(code: Code): State | undefined {
|
|
305
|
+
if (asciiDigit(code)) {
|
|
306
|
+
effects.consume(code);
|
|
307
|
+
return columnRest;
|
|
308
|
+
}
|
|
309
|
+
return finish(code);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function finish(code: Code): State | undefined {
|
|
313
|
+
if (!isBoundary(code)) return nok(code);
|
|
314
|
+
effects.exit("reference");
|
|
315
|
+
return ok(code);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const referenceConstruct: Construct = {
|
|
320
|
+
name: "reference",
|
|
321
|
+
previous: previousReference,
|
|
322
|
+
tokenize: tokenizeReference,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const referenceSyntaxExtension: Extension = {
|
|
326
|
+
text: {
|
|
327
|
+
[codes.atSign]: referenceConstruct,
|
|
328
|
+
[codes.lowercaseF]: referenceConstruct,
|
|
329
|
+
[codes.uppercaseF]: referenceConstruct,
|
|
330
|
+
[codes.lowercaseS]: referenceConstruct,
|
|
331
|
+
[codes.uppercaseS]: referenceConstruct,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export function referenceSyntax(): Extension {
|
|
336
|
+
return referenceSyntaxExtension;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function insideLabel(events: readonly Event[]): boolean {
|
|
340
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
341
|
+
const token = events[index]?.[1];
|
|
342
|
+
if (token === undefined) continue;
|
|
343
|
+
if (
|
|
344
|
+
(token.type === types.labelLink || token.type === types.labelImage) &&
|
|
345
|
+
token._balanced !== true
|
|
346
|
+
) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isBoundary(code: Code): boolean {
|
|
354
|
+
return (
|
|
355
|
+
code === null ||
|
|
356
|
+
markdownLineEndingOrSpace(code) ||
|
|
357
|
+
unicodeWhitespace(code) ||
|
|
358
|
+
unicodePunctuation(code)
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isPathCode(code: Code): boolean {
|
|
363
|
+
if (code === null || markdownLineEndingOrSpace(code)) return false;
|
|
364
|
+
if (!unicodePunctuation(code)) return true;
|
|
365
|
+
return PATH_PUNCTUATION.includes(code);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function isFileCode(code: Code): boolean {
|
|
369
|
+
return isPathCode(code) || code === codes.numberSign || code === codes.colon;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isSkillCode(code: Code): boolean {
|
|
373
|
+
return (
|
|
374
|
+
asciiAlphanumeric(code) ||
|
|
375
|
+
code === codes.dash ||
|
|
376
|
+
code === codes.underscore ||
|
|
377
|
+
code === codes.dot
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function enterReference(this: CompileContext, token: Token): undefined {
|
|
382
|
+
this.enter({ type: "reference" } as unknown as Nodes, token);
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function exitReference(this: CompileContext, token: Token): undefined {
|
|
387
|
+
const node = this.stack[this.stack.length - 1] as unknown as MarkdownNode | undefined;
|
|
388
|
+
const raw = this.sliceSerialize(token);
|
|
389
|
+
const reference = parseBareReference(raw);
|
|
390
|
+
if (node !== undefined) {
|
|
391
|
+
if (reference === undefined) {
|
|
392
|
+
node.type = "text";
|
|
393
|
+
node.value = raw;
|
|
394
|
+
} else {
|
|
395
|
+
Object.assign(node, reference);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
this.exit(token);
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const transformLinks: Transform = function (tree) {
|
|
403
|
+
convertLinks(tree as unknown as MarkdownNode);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
function convertLinks(parent: MarkdownNode): void {
|
|
407
|
+
const children = parent.children;
|
|
408
|
+
if (children === undefined) return;
|
|
409
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
410
|
+
const child = children[index];
|
|
411
|
+
if (child === undefined) continue;
|
|
412
|
+
if (child.type === "link") {
|
|
413
|
+
const reference = parseReference(child.url ?? "", labelOf(child));
|
|
414
|
+
if (reference !== undefined) {
|
|
415
|
+
children[index] = referenceNodeOf(reference, child, children[index - 1]);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
convertLinks(child);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function referenceNodeOf(
|
|
424
|
+
reference: Reference,
|
|
425
|
+
link: MarkdownNode,
|
|
426
|
+
previous: MarkdownNode | undefined,
|
|
427
|
+
): MarkdownNode {
|
|
428
|
+
const node: MarkdownNode = { type: "reference", ...reference };
|
|
429
|
+
const position = link.position;
|
|
430
|
+
if (position === undefined) return node;
|
|
431
|
+
node.position = startsAfterAt(previous, position.start) ? atStart(position) : position;
|
|
432
|
+
return node;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function startsAfterAt(previous: MarkdownNode | undefined, start: MarkdownPoint): boolean {
|
|
436
|
+
if (previous?.type !== "text" || previous.value?.endsWith("@") !== true) return false;
|
|
437
|
+
const position = previous.position;
|
|
438
|
+
if (position === undefined || position.end.offset !== start.offset) return false;
|
|
439
|
+
|
|
440
|
+
return position.end.offset - position.start.offset === previous.value.length;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function atStart(position: { start: MarkdownPoint; end: MarkdownPoint }): {
|
|
444
|
+
start: MarkdownPoint;
|
|
445
|
+
end: MarkdownPoint;
|
|
446
|
+
} {
|
|
447
|
+
return {
|
|
448
|
+
start: {
|
|
449
|
+
line: position.start.line,
|
|
450
|
+
column: position.start.column - 1,
|
|
451
|
+
offset: position.start.offset - 1,
|
|
452
|
+
},
|
|
453
|
+
end: position.end,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function labelOf(node: MarkdownNode): string | undefined {
|
|
458
|
+
const label = textOf(node);
|
|
459
|
+
return label === "" ? undefined : label;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function textOf(node: MarkdownNode): string {
|
|
463
|
+
if (node.value !== undefined) return node.value;
|
|
464
|
+
let text = "";
|
|
465
|
+
for (const child of node.children ?? []) text += textOf(child);
|
|
466
|
+
return text;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseBareReference(raw: string): Reference | undefined {
|
|
470
|
+
const body = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
471
|
+
if (body === "") return undefined;
|
|
472
|
+
const scheme = SCHEME.exec(body);
|
|
473
|
+
if (scheme !== null && isKnownScheme(scheme[1] as string)) return parseReference(body);
|
|
474
|
+
const matched = BARE_PATH.exec(body);
|
|
475
|
+
const path = matched?.[1];
|
|
476
|
+
const line = matched?.[2];
|
|
477
|
+
if (path === undefined || line === undefined) return parseReference(body);
|
|
478
|
+
const decoded = decodeUri(path);
|
|
479
|
+
if (decoded === undefined || decoded === "") return undefined;
|
|
480
|
+
const column = matched?.[3];
|
|
481
|
+
return {
|
|
482
|
+
protocol: DEFAULT_PROTOCOL,
|
|
483
|
+
path: decoded,
|
|
484
|
+
lineStart: Number(line),
|
|
485
|
+
...(column === undefined ? {} : { column: Number(column) }),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function referenceFromMarkdown(): MdastExtension {
|
|
490
|
+
return {
|
|
491
|
+
enter: { reference: enterReference },
|
|
492
|
+
exit: { reference: exitReference },
|
|
493
|
+
transforms: [transformLinks],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function isKnownScheme(name: string): boolean {
|
|
498
|
+
const protocol = name.toLowerCase();
|
|
499
|
+
return protocol === "file" || protocol === "skill";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function splitLineFragment(value: string): {
|
|
503
|
+
readonly head: string;
|
|
504
|
+
readonly lines: Partial<Pick<Reference, "lineStart" | "lineEnd" | "column">>;
|
|
505
|
+
} {
|
|
506
|
+
const hashAt = value.indexOf("#");
|
|
507
|
+
if (hashAt === -1) return { head: value, lines: {} };
|
|
508
|
+
const matched = LINE_FRAGMENT.exec(value.slice(hashAt + 1));
|
|
509
|
+
if (matched === null) return { head: value, lines: {} };
|
|
510
|
+
const lineStart = matched[1];
|
|
511
|
+
if (lineStart === undefined) return { head: value, lines: {} };
|
|
512
|
+
const column = matched[2];
|
|
513
|
+
const lineEnd = matched[3];
|
|
514
|
+
return {
|
|
515
|
+
head: value.slice(0, hashAt),
|
|
516
|
+
lines: {
|
|
517
|
+
lineStart: Number(lineStart),
|
|
518
|
+
...(column === undefined ? {} : { column: Number(column) }),
|
|
519
|
+
...(lineEnd === undefined ? {} : { lineEnd: Number(lineEnd) }),
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function lineFragmentOf(reference: Reference): string {
|
|
525
|
+
if (reference.lineStart === undefined) return "";
|
|
526
|
+
const column = reference.column === undefined ? "" : `C${String(reference.column)}`;
|
|
527
|
+
const lineEnd = reference.lineEnd === undefined ? "" : `-L${String(reference.lineEnd)}`;
|
|
528
|
+
return `#L${String(reference.lineStart)}${column}${lineEnd}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function authorityEnd(value: string): number {
|
|
532
|
+
for (let index = 2; index < value.length; index += 1) {
|
|
533
|
+
const char = value[index];
|
|
534
|
+
if (char === "/" || char === "?" || char === "#") return index;
|
|
535
|
+
}
|
|
536
|
+
return value.length;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function encodeUri(uri: string): string {
|
|
540
|
+
return uri.replace(
|
|
541
|
+
URI_UNSAFE,
|
|
542
|
+
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function decodeUri(value: string): string | undefined {
|
|
547
|
+
try {
|
|
548
|
+
return decodeURIComponent(value);
|
|
549
|
+
} catch {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
}
|