@cosense-toolbox/lsp 0.1.0-beta.2 → 0.1.0-beta.4

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/dist/tokens.d.mts CHANGED
@@ -25,9 +25,12 @@ type RawToken = (Span & {
25
25
  readonly name: string;
26
26
  });
27
27
  /**
28
- * A notation the caller adds to Cosense's: a marker that opens a decoration, as `!` opens
29
- * `[! 注意]`, and the token type it is sent as. The marker is also taught to the parser, so
30
- * the bracket is not read as a link.
28
+ * A name the caller gives to a decoration marker, as `warning` to the `!` of `[! 注意]`: the
29
+ * token type that decoration is sent as.
30
+ *
31
+ * The marker has to be one Cosense decorates with (`` !"#%&'()*+,-./{|}<>_~= ``), since
32
+ * those are the ones the parser reads as a decoration. Any other, such as `@`, leaves the
33
+ * bracket a link, as it is in Cosense Web: how a notation looks does not change how it parses.
31
34
  */
32
35
  interface Notation {
33
36
  readonly marker: string;
@@ -43,10 +46,11 @@ declare const LEGEND: readonly ["namespace", "type", "function", "variable", "pr
43
46
  /**
44
47
  * LEGEND with the names of `notations` after it, each once: the legend for a client that
45
48
  * accepts token types of the caller's own naming. The LSP's types keep their places.
49
+ *
50
+ * A client that colours by this package's own names instead passes those to `encodeTokens`
51
+ * (`[...TOKEN_TYPES, ...names]`): a token whose own name is in the legend is sent as that.
46
52
  */
47
53
  declare const legendOf: (notations: ReadonlyArray<Notation>) => ReadonlyArray<string>;
48
- /** `parseOptions` that also read the markers of `notations` as decorations. */
49
- declare const withNotations: (parseOptions: ParseOptions, notations: ReadonlyArray<Notation>) => ParseOptions;
50
54
  declare const COMPONENT_LINE: RegExp;
51
55
  /** The last line of the `---` fence around YAML at the very top, if the file opens with one. */
52
56
  declare const frontmatterEnd: (lines: ReadonlyArray<string>) => Option.Option<number>;
@@ -58,7 +62,7 @@ declare const fenceOf: (lines: ReadonlyArray<string>, frontmatter: boolean) => O
58
62
  interface ComputeTokensOptions {
59
63
  /** `.csnx` also reads a line that is one component tag. `.csn` never does. */
60
64
  readonly components?: boolean;
61
- /** How to parse: the site's notation extensions, so `[! 注意]` is not read as a link. */
65
+ /** How to parse: the site's own notation extensions, as its build parses with them. */
62
66
  readonly parseOptions?: ParseOptions;
63
67
  /** Notations the caller adds, each sent as a token of its own name (see `Notation`). */
64
68
  readonly notations?: ReadonlyArray<Notation>;
@@ -72,9 +76,10 @@ interface ComputeTokensOptions {
72
76
  declare const computeTokens: (text: string, options?: ComputeTokensOptions) => RawToken[];
73
77
  /**
74
78
  * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.
75
- * `legend` is the one sent at initialize: LEGEND, or `legendOf(notations)`.
79
+ * `legend` is the one sent at initialize: LEGEND, `legendOf(notations)`, or a legend of the
80
+ * client's own naming such as `[...TOKEN_TYPES, ...names]`.
76
81
  */
77
82
  declare const encodeTokens: (tokens: ReadonlyArray<RawToken>, legend?: ReadonlyArray<string>) => number[];
78
83
  //#endregion
79
- export { COMPONENT_LINE, ComputeTokensOptions, LEGEND, Notation, RawToken, TOKEN_TYPES, TokenType, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf, withNotations };
84
+ export { COMPONENT_LINE, ComputeTokensOptions, LEGEND, Notation, RawToken, TOKEN_TYPES, TokenType, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf };
80
85
  //# sourceMappingURL=tokens.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.mts","names":[],"sources":["../src/tokens.ts"],"mappings":";;;;;;;;;;;cAsBa;KAgCD,oBAAoB;;KAG3B,mBAAmB,QAAQ;UAEtB;WACC;WACA;WACA;;KAGC,YACP;WAAkB,MAAM;MACxB;WAAkB;WAA2B;;;;;;;UAOjC;WACN;WACA;;;;;;;;cASE;;;;;cAqDA,WAAY,WAAW,cAAc,cAAY;;cAIjD,gBACX,cAAc,cACd,WAAW,cAAc,cACxB;cA2EU,gBAAc;;cA2Gd,iBAAkB,OAAO,0BAAwB,OAAO;;;;;cAUxD,UACX,OAAO,uBACP,yBACC,OAAO;UAgIO;;WAEN;;WAEA,eAAe;;WAEf,YAAY,cAAc;;;;;WAK1B;;;cAIE,gBAAiB,cAAc,UAAS,yBAA4B;;;;;cAkDpE,eACX,QAAQ,cAAc,WACtB,SAAQ"}
1
+ {"version":3,"file":"tokens.d.mts","names":[],"sources":["../src/tokens.ts"],"mappings":";;;;;;;;;;;cAqBa;KAgCD,oBAAoB;;KAG3B,mBAAmB,QAAQ;UAEtB;WACC;WACA;WACA;;KAGC,YACP;WAAkB,MAAM;MACxB;WAAkB;WAA2B;;;;;;;;;;UAUjC;WACN;WACA;;;;;;;;cASE;;;;;;;;cAwDA,WAAY,WAAW,cAAc,cAAY;cAkEjD,gBAAc;;cA2Gd,iBAAkB,OAAO,0BAAwB,OAAO;;;;;cAUxD,UACX,OAAO,uBACP,yBACC,OAAO;UAgIO;;WAEN;;WAEA,eAAe;;WAEf,YAAY,cAAc;;;;;WAK1B;;;cAIE,gBAAiB,cAAc,UAAS,yBAA4B;;;;;;cAqDpE,eACX,QAAQ,cAAc,WACtB,SAAQ"}
package/dist/tokens.mjs CHANGED
@@ -1,2 +1,283 @@
1
- import { a as encodeTokens, c as legendOf, i as computeTokens, l as withNotations, n as LEGEND, o as fenceOf, r as TOKEN_TYPES, s as frontmatterEnd, t as COMPONENT_LINE } from "./tokens-D0NP_uLo.mjs";
2
- export { COMPONENT_LINE, LEGEND, TOKEN_TYPES, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf, withNotations };
1
+ import { n as gather, r as leaf, t as branch } from "./tree-sXzD2703.mjs";
2
+ import { Array, Match, Option, Order, pipe } from "effect";
3
+ import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
4
+ //#region src/tokens.ts
5
+ /**
6
+ * Cosense notation as LSP semantic tokens.
7
+ *
8
+ * These names are the ones this package reasons in, and they are not what goes on the wire.
9
+ * A client announces the token types it knows at initialize and drops everything else —
10
+ * some send the LSP's own list and nothing more — so `quote` would simply never be drawn.
11
+ * LEGEND below is what is sent, and LSP_TYPE is the translation.
12
+ */
13
+ const TOKEN_TYPES = [
14
+ "title",
15
+ "link",
16
+ "projectLink",
17
+ "externalLink",
18
+ "hashtag",
19
+ "code",
20
+ "codeBlock",
21
+ "formula",
22
+ "icon",
23
+ "quote",
24
+ "bold",
25
+ "italic",
26
+ "strike",
27
+ "underline",
28
+ "image",
29
+ "table",
30
+ "bold2",
31
+ "bold3",
32
+ "component",
33
+ "attribute",
34
+ "attributeValue",
35
+ "expression",
36
+ "frontmatter",
37
+ "notation"
38
+ ];
39
+ /**
40
+ * The legend sent to the client: the LSP's own token types, in its own order.
41
+ *
42
+ * Every entry is in the set a client is expected to understand, which is what makes the
43
+ * colours appear without the reader configuring anything.
44
+ */
45
+ const LEGEND = [
46
+ "namespace",
47
+ "type",
48
+ "function",
49
+ "variable",
50
+ "property",
51
+ "parameter",
52
+ "decorator",
53
+ "macro",
54
+ "keyword",
55
+ "string",
56
+ "number",
57
+ "comment"
58
+ ];
59
+ /**
60
+ * What each notation is drawn as. Chosen so that things a reader separates by eye land on
61
+ * different entries: a link is not a tag, and a quote is not a heading. `bold` and its
62
+ * louder siblings share one entry, because the LSP has no notion of weight and a theme
63
+ * gives each type a single colour.
64
+ */
65
+ const LSP_TYPE = {
66
+ title: "namespace",
67
+ link: "function",
68
+ projectLink: "function",
69
+ externalLink: "string",
70
+ icon: "function",
71
+ image: "string",
72
+ hashtag: "decorator",
73
+ code: "string",
74
+ codeBlock: "string",
75
+ formula: "number",
76
+ table: "property",
77
+ quote: "comment",
78
+ frontmatter: "comment",
79
+ component: "type",
80
+ attribute: "parameter",
81
+ attributeValue: "string",
82
+ expression: "variable",
83
+ bold: "keyword",
84
+ bold2: "keyword",
85
+ bold3: "keyword",
86
+ italic: "macro",
87
+ strike: "comment",
88
+ underline: "variable",
89
+ notation: "decorator"
90
+ };
91
+ /**
92
+ * LEGEND with the names of `notations` after it, each once: the legend for a client that
93
+ * accepts token types of the caller's own naming. The LSP's types keep their places.
94
+ *
95
+ * A client that colours by this package's own names instead passes those to `encodeTokens`
96
+ * (`[...TOKEN_TYPES, ...names]`): a token whose own name is in the legend is sent as that.
97
+ */
98
+ const legendOf = (notations) => Array.dedupe([...LEGEND, ...Array.map(notations, (notation) => notation.name)]);
99
+ /** Leaf inline nodes that map 1:1. Each sits on one line, so no splitting is needed. */
100
+ const INLINE_TOKEN_TYPE = {
101
+ internalLink: "link",
102
+ externalLink: "externalLink",
103
+ projectLink: "projectLink",
104
+ hashtag: "hashtag",
105
+ inlineCode: "code",
106
+ image: "image",
107
+ icon: "icon",
108
+ formula: "formula"
109
+ };
110
+ /** Top to bottom, then left to right: the order the LSP encoding needs. */
111
+ const BY_POSITION = Order.combine(Order.mapInput(Order.number, (token) => token.line), Order.mapInput(Order.number, (token) => token.char));
112
+ const onlyIf = (condition, value) => condition ? Option.some(value) : Option.none();
113
+ /** A token over `[from, to)` of one line. */
114
+ const token = (type, line, from, to) => ({
115
+ line,
116
+ char: from,
117
+ length: to - from,
118
+ type
119
+ });
120
+ const spanToken = (type, position) => token(type, position.start.line, position.start.column, position.end.column);
121
+ /** How many characters from `from` the `^`-anchored `pattern` takes; 0 when it takes none. */
122
+ const matchLength = (pattern, text, from) => pipe(Option.fromNullable(pattern.exec(text.slice(from))), Option.match({
123
+ onNone: () => 0,
124
+ onSome: ([matched]) => matched.length
125
+ }));
126
+ /** Cosense sizes emphasis by asterisk count (`[*]`..`[*****]`); sizeLevel is 0-indexed. */
127
+ const boldTokenType = (sizeLevel) => Match.value(sizeLevel).pipe(Match.when((level) => level >= 2, () => "bold3"), Match.when(1, () => "bold2"), Match.orElse(() => "bold"));
128
+ /** Every decoration a run carries, since Cosense applies all of them: `[-* x]` is both. */
129
+ const decorationTokenTypes = (node) => Array.getSomes([
130
+ onlyIf(node.underline, "underline"),
131
+ onlyIf(node.strike, "strike"),
132
+ onlyIf(node.italic, "italic"),
133
+ onlyIf(node.bold, boldTokenType(node.sizeLevel))
134
+ ]);
135
+ const COMPONENT_LINE = /^\s*<\/?[A-Z][A-Za-z0-9_.]*(\s|\/?>)/;
136
+ const TAG_OPEN = /^(\s*<\/?)([A-Z][A-Za-z0-9_.]*)/;
137
+ const ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_:.-]*/;
138
+ const SPACE = /^\s*/;
139
+ /** Where a quoted value opened at `from` ends: after its closing quote, or at the line's end. */
140
+ const quotedEnd = (text, from) => pipe(text.indexOf(text.charAt(from), from + 1), Option.liftPredicate((close) => close >= 0), Option.match({
141
+ onNone: () => text.length,
142
+ onSome: (close) => close + 1
143
+ }));
144
+ /** Where `{ ... }` opened at `from` ends, counting nested braces: after its `}`, or the line's end. */
145
+ const bracedEnd = (text, from) => pipe(text.slice(from).split(""), Array.scan(0, (depth, char) => char === "{" ? depth + 1 : char === "}" ? depth - 1 : depth), Array.drop(1), Array.findFirstIndex((depth) => depth === 0), Option.match({
146
+ onNone: () => text.length,
147
+ onSome: (index) => from + index + 1
148
+ }));
149
+ /** The attribute value that starts at `from`: its type and where it ends. */
150
+ const valueAt = (text, from) => Match.value(text.charAt(from)).pipe(Match.when((char) => char === "\"" || char === "'", () => Option.some(["attributeValue", quotedEnd(text, from)])), Match.when("{", () => Option.some(["expression", bracedEnd(text, from)])), Match.orElse(() => Option.none()));
151
+ /** `at` past any whitespace. */
152
+ const skipSpace = (text, at) => at + matchLength(SPACE, text, at);
153
+ /** Whether the tag closes at `at`, with `>` or `/>`, or the line ends there. */
154
+ const closesAt = (text, at) => at >= text.length || text.startsWith(">", at) || text.startsWith("/>", at);
155
+ /**
156
+ * An attribute named over `[at, nameEnd)`: the name, then `=` and a value if one follows.
157
+ * A name with no `=` after it stands alone, as JSX reads `<Dialog open>`.
158
+ */
159
+ const attribute = (line, text, at, nameEnd) => {
160
+ const name = token("attribute", line, at, nameEnd);
161
+ const afterName = skipSpace(text, nameEnd);
162
+ return pipe(Option.liftPredicate(afterName, (equals) => text.charAt(equals) === "="), Option.map((equals) => skipSpace(text, equals + 1)), Option.match({
163
+ onNone: () => [[name], afterName],
164
+ onSome: (valueStart) => pipe(valueAt(text, valueStart), Option.match({
165
+ onNone: () => [[name], valueStart],
166
+ onSome: ([type, end]) => [[name, token(type, line, valueStart, end)], end]
167
+ }))
168
+ }));
169
+ };
170
+ /** One attribute from `from`, or None once the tag has closed. */
171
+ const attributeStep = (line, text) => (from) => pipe(Option.liftPredicate(skipSpace(text, from), (at) => !closesAt(text, at)), Option.map((at) => pipe(Option.liftPredicate(at + matchLength(ATTRIBUTE_NAME, text, at), (end) => end > at), Option.match({
172
+ onNone: () => [[], at + 1],
173
+ onSome: (nameEnd) => attribute(line, text, at, nameEnd)
174
+ }))));
175
+ /**
176
+ * The tag that opens a component line, read as JSX: its name, then each attribute and its
177
+ * value, up to the `>` that closes it. Text after the tag is left alone; it is neither
178
+ * Cosense notation nor part of the tag.
179
+ */
180
+ const componentTokens = (line, text) => pipe(Option.fromNullable(TAG_OPEN.exec(text)), Option.map(([head, lead = "", name = ""]) => [token("component", line, lead.length, lead.length + name.length), ...Array.flatten(Array.unfold(head.length, attributeStep(line, text)))]), Option.getOrElse(() => []));
181
+ /** The last line of the `---` fence around YAML at the very top, if the file opens with one. */
182
+ const frontmatterEnd = (lines) => pipe(Option.liftPredicate(lines, (all) => all[0]?.trim() === "---"), Option.flatMap(Array.findFirstIndex((line, index) => index > 0 && line.trim() === "---")));
183
+ /**
184
+ * `frontmatterEnd`, or None when the caller reads no frontmatter: a Cosense page has none,
185
+ * and a page whose title is `---` would otherwise lose its first lines to it.
186
+ */
187
+ const fenceOf = (lines, frontmatter) => Option.flatMap(onlyIf(frontmatter, lines), frontmatterEnd);
188
+ /** Each non-empty line from `first` to `last`, whole. */
189
+ const lineSpans = (type, lines, first, last) => Array.filterMap(Array.range(first, last), (line) => pipe(Option.fromNullable(lines[line]), Option.filter((text) => text.length > 0), Option.map((text) => token(type, line, 0, text.length))));
190
+ /** The line's text, when the file reads components and the line opens with a tag. */
191
+ const componentLine = (page, line) => pipe(Option.fromNullable(page.lines[line]), Option.filter((text) => page.components && COMPONENT_LINE.test(text)));
192
+ /** The `>` and spaces that open a quote line, from its indentation onwards. */
193
+ const quoteToken = (page, node, line) => pipe(onlyIf(node.quote, page.lines[line] ?? ""), Option.map((text) => matchLength(/^[> ]*/, text, node.indent)), Option.filter((length) => length > 0), Option.map((length) => token("quote", line, node.indent, node.indent + length)));
194
+ /** A token for each of `notations` whose marker opens the decoration. */
195
+ const notationTokens = (notations, node, position) => Array.filterMap(notations, ({ marker, name }) => onlyIf(node.markers.includes(marker), {
196
+ line: position.start.line,
197
+ char: position.start.column,
198
+ length: position.end.column - position.start.column,
199
+ type: "notation",
200
+ name
201
+ }));
202
+ const nodeTokens = (page) => (node) => {
203
+ const lineOf = (position) => position.start.line + page.offset;
204
+ const shift = (position) => ({
205
+ start: {
206
+ ...position.start,
207
+ line: position.start.line + page.offset
208
+ },
209
+ end: {
210
+ ...position.end,
211
+ line: position.end.line + page.offset
212
+ }
213
+ });
214
+ return Match.value(node).pipe(Match.discriminators("type")({
215
+ title: (title) => leaf(pipe(componentLine(page, lineOf(title.position)), Option.match({
216
+ onNone: () => [spanToken("title", shift(title.position))],
217
+ onSome: (text) => componentTokens(lineOf(title.position), text)
218
+ }))),
219
+ codeBlock: (block) => leaf(lineSpans("codeBlock", page.lines, lineOf(block.position), block.position.end.line + page.offset)),
220
+ table: (table) => leaf(lineSpans("table", page.lines, lineOf(table.position), table.position.end.line + page.offset)),
221
+ line: (line) => pipe(componentLine(page, lineOf(line.position)), Option.match({
222
+ onSome: (text) => leaf(componentTokens(lineOf(line.position), text)),
223
+ onNone: () => branch(Option.toArray(quoteToken(page, line, lineOf(line.position))))
224
+ })),
225
+ decoration: (decoration) => branch([...decorationTokenTypes(decoration).map((type) => spanToken(type, shift(decoration.position))), ...notationTokens(page.notations, decoration, shift(decoration.position))])
226
+ }), Match.orElse((other) => branch(pipe(Option.fromNullable(INLINE_TOKEN_TYPE[other.type]), Option.map((type) => spanToken(type, shift(other.position))), Option.toArray))));
227
+ };
228
+ /** Pure text -> tokens. The LSP delta encoding lives in encodeTokens, so this stays testable. */
229
+ const computeTokens = (text, options = {}) => {
230
+ const lines = normalizeLineEndings(text).split("\n");
231
+ const notations = options.notations ?? [];
232
+ const fence = fenceOf(lines, options.frontmatter ?? true);
233
+ const page = {
234
+ lines,
235
+ offset: Option.match(fence, {
236
+ onNone: () => 0,
237
+ onSome: (end) => end + 1
238
+ }),
239
+ components: options.components ?? false,
240
+ notations
241
+ };
242
+ const frontmatterTokens = Option.match(fence, {
243
+ onNone: () => [],
244
+ onSome: (end) => lineSpans("frontmatter", lines, 0, end)
245
+ });
246
+ const body = parse(lines.slice(page.offset).join("\n"), options.parseOptions ?? {});
247
+ return pipe([...frontmatterTokens, ...gather(body, nodeTokens(page))], Array.filter((t) => t.length > 0), Array.sort(BY_POSITION));
248
+ };
249
+ /** The name a token goes by: a notation's is the caller's, any other's its type. */
250
+ const ownName = (token) => Match.value(token).pipe(Match.when({ type: "notation" }, (notation) => notation.name), Match.orElse((builtin) => builtin.type));
251
+ /**
252
+ * Each token's place in `legend`: its own name when the legend has it, else the LSP type
253
+ * its kind is drawn as. LEGEND holds none of the own names, so it always gets the LSP type.
254
+ */
255
+ const legendIndex = (legend) => {
256
+ const placeOf = (type) => Option.liftPredicate(legend.indexOf(type), (index) => index >= 0);
257
+ return (token) => pipe(placeOf(ownName(token)), Option.orElse(() => placeOf(LSP_TYPE[token.type])), Option.getOrElse(() => 0));
258
+ };
259
+ /**
260
+ * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.
261
+ * `legend` is the one sent at initialize: LEGEND, `legendOf(notations)`, or a legend of the
262
+ * client's own naming such as `[...TOKEN_TYPES, ...names]`.
263
+ */
264
+ const encodeTokens = (tokens, legend = LEGEND) => {
265
+ const indexOf = legendIndex(legend);
266
+ return pipe(Array.sort(tokens, BY_POSITION), Array.mapAccum({
267
+ line: 0,
268
+ char: 0
269
+ }, (previous, t) => [{
270
+ line: t.line,
271
+ char: t.char
272
+ }, [
273
+ t.line - previous.line,
274
+ t.line === previous.line ? t.char - previous.char : t.char,
275
+ t.length,
276
+ indexOf(t),
277
+ 0
278
+ ]]), ([, rows]) => Array.flatten(rows));
279
+ };
280
+ //#endregion
281
+ export { COMPONENT_LINE, LEGEND, TOKEN_TYPES, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf };
282
+
283
+ //# sourceMappingURL=tokens.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.mjs","names":["Arr"],"sources":["../src/tokens.ts"],"sourcesContent":["import type {\n AnyNode,\n AnyNodeType,\n Decoration,\n NodeOfType,\n ParseOptions,\n Position,\n} from \"@cosense-toolbox/parser\"\nimport { normalizeLineEndings, parse } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Match, Option, Order, pipe } from \"effect\"\n\nimport { branch, gather, leaf, type Picked } from \"./tree\"\n\n/**\n * Cosense notation as LSP semantic tokens.\n *\n * These names are the ones this package reasons in, and they are not what goes on the wire.\n * A client announces the token types it knows at initialize and drops everything else —\n * some send the LSP's own list and nothing more — so `quote` would simply never be drawn.\n * LEGEND below is what is sent, and LSP_TYPE is the translation.\n */\nexport const TOKEN_TYPES = [\n \"title\",\n \"link\",\n \"projectLink\",\n \"externalLink\",\n \"hashtag\",\n \"code\",\n \"codeBlock\",\n \"formula\",\n \"icon\",\n \"quote\",\n \"bold\",\n \"italic\",\n \"strike\",\n \"underline\",\n \"image\",\n \"table\",\n \"bold2\",\n \"bold3\",\n // `.csnx` only: a line that opens with a component tag. The tag's name, its attribute\n // names, and the two kinds of value, as JSX reads them.\n \"component\",\n \"attribute\",\n \"attributeValue\",\n \"expression\",\n // The YAML at the top of the file, which is not Cosense notation at all.\n \"frontmatter\",\n // A notation the caller defines (see `Notation`), such as `[! 注意]`. The token carries\n // the name the caller gave it.\n \"notation\",\n] as const\n\nexport type TokenType = (typeof TOKEN_TYPES)[number]\n\n/** Every token type but `notation`, whose tokens also carry a name. */\ntype BuiltinTokenType = Exclude<TokenType, \"notation\">\n\ninterface Span {\n readonly line: number\n readonly char: number\n readonly length: number\n}\n\nexport type RawToken =\n | (Span & { readonly type: BuiltinTokenType })\n | (Span & { readonly type: \"notation\"; readonly name: string })\n\n/**\n * A name the caller gives to a decoration marker, as `warning` to the `!` of `[! 注意]`: the\n * token type that decoration is sent as.\n *\n * The marker has to be one Cosense decorates with (`` !\"#%&'()*+,-./{|}<>_~= ``), since\n * those are the ones the parser reads as a decoration. Any other, such as `@`, leaves the\n * bracket a link, as it is in Cosense Web: how a notation looks does not change how it parses.\n */\nexport interface Notation {\n readonly marker: string\n readonly name: string\n}\n\n/**\n * The legend sent to the client: the LSP's own token types, in its own order.\n *\n * Every entry is in the set a client is expected to understand, which is what makes the\n * colours appear without the reader configuring anything.\n */\nexport const LEGEND = [\n \"namespace\",\n \"type\",\n \"function\",\n \"variable\",\n \"property\",\n \"parameter\",\n \"decorator\",\n \"macro\",\n \"keyword\",\n \"string\",\n \"number\",\n \"comment\",\n] as const\n\n/**\n * What each notation is drawn as. Chosen so that things a reader separates by eye land on\n * different entries: a link is not a tag, and a quote is not a heading. `bold` and its\n * louder siblings share one entry, because the LSP has no notion of weight and a theme\n * gives each type a single colour.\n */\nconst LSP_TYPE: Record<TokenType, (typeof LEGEND)[number]> = {\n title: \"namespace\",\n link: \"function\",\n projectLink: \"function\",\n externalLink: \"string\",\n icon: \"function\",\n image: \"string\",\n hashtag: \"decorator\",\n code: \"string\",\n codeBlock: \"string\",\n formula: \"number\",\n table: \"property\",\n quote: \"comment\",\n frontmatter: \"comment\",\n component: \"type\",\n attribute: \"parameter\",\n attributeValue: \"string\",\n expression: \"variable\",\n bold: \"keyword\",\n bold2: \"keyword\",\n bold3: \"keyword\",\n italic: \"macro\",\n strike: \"comment\",\n underline: \"variable\",\n // What a caller's notation is sent as when the legend has no type of its name.\n notation: \"decorator\",\n}\n\n/**\n * LEGEND with the names of `notations` after it, each once: the legend for a client that\n * accepts token types of the caller's own naming. The LSP's types keep their places.\n *\n * A client that colours by this package's own names instead passes those to `encodeTokens`\n * (`[...TOKEN_TYPES, ...names]`): a token whose own name is in the legend is sent as that.\n */\nexport const legendOf = (notations: ReadonlyArray<Notation>): ReadonlyArray<string> =>\n Arr.dedupe([...LEGEND, ...Arr.map(notations, (notation) => notation.name)])\n\n/** Leaf inline nodes that map 1:1. Each sits on one line, so no splitting is needed. */\nconst INLINE_TOKEN_TYPE: Partial<Record<AnyNodeType, BuiltinTokenType>> = {\n internalLink: \"link\",\n externalLink: \"externalLink\",\n projectLink: \"projectLink\",\n hashtag: \"hashtag\",\n inlineCode: \"code\",\n image: \"image\",\n icon: \"icon\",\n formula: \"formula\",\n}\n\n/** Top to bottom, then left to right: the order the LSP encoding needs. */\nconst BY_POSITION: Order.Order<RawToken> = Order.combine(\n Order.mapInput(Order.number, (token: RawToken) => token.line),\n Order.mapInput(Order.number, (token: RawToken) => token.char),\n)\n\nconst onlyIf = <A>(condition: boolean, value: A): Option.Option<A> =>\n condition ? Option.some(value) : Option.none()\n\n/** A token over `[from, to)` of one line. */\nconst token = (type: BuiltinTokenType, line: number, from: number, to: number): RawToken => ({\n line,\n char: from,\n length: to - from,\n type,\n})\n\nconst spanToken = (type: BuiltinTokenType, position: Position): RawToken =>\n token(type, position.start.line, position.start.column, position.end.column)\n\n/** How many characters from `from` the `^`-anchored `pattern` takes; 0 when it takes none. */\nconst matchLength = (pattern: RegExp, text: string, from: number): number =>\n pipe(\n Option.fromNullable(pattern.exec(text.slice(from))),\n Option.match({ onNone: () => 0, onSome: ([matched]) => matched.length }),\n )\n\n// --- Emphasis ---------------------------------------------------------------------------\n\n/** Cosense sizes emphasis by asterisk count (`[*]`..`[*****]`); sizeLevel is 0-indexed. */\nconst boldTokenType = (sizeLevel: number): BuiltinTokenType =>\n Match.value(sizeLevel).pipe(\n Match.when(\n (level) => level >= 2,\n (): BuiltinTokenType => \"bold3\",\n ),\n Match.when(1, (): BuiltinTokenType => \"bold2\"),\n Match.orElse((): BuiltinTokenType => \"bold\"),\n )\n\n/** Every decoration a run carries, since Cosense applies all of them: `[-* x]` is both. */\nconst decorationTokenTypes = (node: Decoration): ReadonlyArray<BuiltinTokenType> =>\n Arr.getSomes([\n onlyIf(node.underline, \"underline\" as const),\n onlyIf(node.strike, \"strike\" as const),\n onlyIf(node.italic, \"italic\" as const),\n onlyIf(node.bold, boldTokenType(node.sizeLevel)),\n ])\n\n// --- Component lines (`.csnx`) ----------------------------------------------------------\n\nexport const COMPONENT_LINE = /^\\s*<\\/?[A-Z][A-Za-z0-9_.]*(\\s|\\/?>)/\nconst TAG_OPEN = /^(\\s*<\\/?)([A-Z][A-Za-z0-9_.]*)/\nconst ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_:.-]*/\nconst SPACE = /^\\s*/\n\n/** Where a quoted value opened at `from` ends: after its closing quote, or at the line's end. */\nconst quotedEnd = (text: string, from: number): number =>\n pipe(\n text.indexOf(text.charAt(from), from + 1),\n Option.liftPredicate((close) => close >= 0),\n Option.match({ onNone: () => text.length, onSome: (close) => close + 1 }),\n )\n\n/** Where `{ ... }` opened at `from` ends, counting nested braces: after its `}`, or the line's end. */\nconst bracedEnd = (text: string, from: number): number =>\n pipe(\n text.slice(from).split(\"\"),\n Arr.scan(0, (depth, char) => (char === \"{\" ? depth + 1 : char === \"}\" ? depth - 1 : depth)),\n // scan starts with the depth before any character; the rest line up with the characters.\n Arr.drop(1),\n Arr.findFirstIndex((depth) => depth === 0),\n Option.match({ onNone: () => text.length, onSome: (index) => from + index + 1 }),\n )\n\ntype ValueType = \"attributeValue\" | \"expression\"\n\n/** The attribute value that starts at `from`: its type and where it ends. */\nconst valueAt = (text: string, from: number): Option.Option<readonly [ValueType, number]> =>\n Match.value(text.charAt(from)).pipe(\n Match.when(\n (char) => char === '\"' || char === \"'\",\n () => Option.some([\"attributeValue\", quotedEnd(text, from)] as const),\n ),\n Match.when(\"{\", () => Option.some([\"expression\", bracedEnd(text, from)] as const)),\n Match.orElse(() => Option.none()),\n )\n\n/** The tokens of one attribute, and where the next may start. */\ntype Step = readonly [ReadonlyArray<RawToken>, number]\n\n/** `at` past any whitespace. */\nconst skipSpace = (text: string, at: number): number => at + matchLength(SPACE, text, at)\n\n/** Whether the tag closes at `at`, with `>` or `/>`, or the line ends there. */\nconst closesAt = (text: string, at: number): boolean =>\n at >= text.length || text.startsWith(\">\", at) || text.startsWith(\"/>\", at)\n\n/**\n * An attribute named over `[at, nameEnd)`: the name, then `=` and a value if one follows.\n * A name with no `=` after it stands alone, as JSX reads `<Dialog open>`.\n */\nconst attribute = (line: number, text: string, at: number, nameEnd: number): Step => {\n const name = token(\"attribute\", line, at, nameEnd)\n const afterName = skipSpace(text, nameEnd)\n return pipe(\n Option.liftPredicate(afterName, (equals) => text.charAt(equals) === \"=\"),\n Option.map((equals) => skipSpace(text, equals + 1)),\n Option.match({\n onNone: (): Step => [[name], afterName],\n onSome: (valueStart): Step =>\n pipe(\n valueAt(text, valueStart),\n Option.match({\n onNone: (): Step => [[name], valueStart],\n onSome: ([type, end]): Step => [[name, token(type, line, valueStart, end)], end],\n }),\n ),\n }),\n )\n}\n\n/** One attribute from `from`, or None once the tag has closed. */\nconst attributeStep =\n (line: number, text: string) =>\n (from: number): Option.Option<Step> =>\n pipe(\n Option.liftPredicate(skipSpace(text, from), (at) => !closesAt(text, at)),\n Option.map((at) =>\n pipe(\n Option.liftPredicate(at + matchLength(ATTRIBUTE_NAME, text, at), (end) => end > at),\n Option.match({\n // Not a name: step over the character, since the tag may still go on after it.\n onNone: (): Step => [[], at + 1],\n onSome: (nameEnd) => attribute(line, text, at, nameEnd),\n }),\n ),\n ),\n )\n\n/**\n * The tag that opens a component line, read as JSX: its name, then each attribute and its\n * value, up to the `>` that closes it. Text after the tag is left alone; it is neither\n * Cosense notation nor part of the tag.\n */\nconst componentTokens = (line: number, text: string): ReadonlyArray<RawToken> =>\n pipe(\n Option.fromNullable(TAG_OPEN.exec(text)),\n Option.map(([head, lead = \"\", name = \"\"]) => [\n token(\"component\", line, lead.length, lead.length + name.length),\n ...Arr.flatten(Arr.unfold(head.length, attributeStep(line, text))),\n ]),\n Option.getOrElse(() => []),\n )\n\n// --- The page ---------------------------------------------------------------------------\n\n/** The last line of the `---` fence around YAML at the very top, if the file opens with one. */\nexport const frontmatterEnd = (lines: ReadonlyArray<string>): Option.Option<number> =>\n pipe(\n Option.liftPredicate(lines, (all) => all[0]?.trim() === \"---\"),\n Option.flatMap(Arr.findFirstIndex((line, index) => index > 0 && line.trim() === \"---\")),\n )\n\n/**\n * `frontmatterEnd`, or None when the caller reads no frontmatter: a Cosense page has none,\n * and a page whose title is `---` would otherwise lose its first lines to it.\n */\nexport const fenceOf = (\n lines: ReadonlyArray<string>,\n frontmatter: boolean,\n): Option.Option<number> => Option.flatMap(onlyIf(frontmatter, lines), frontmatterEnd)\n\n/** What the node visitor needs to know about the file around the parsed body. */\ninterface Page {\n readonly lines: ReadonlyArray<string>\n /** Lines before the body: the frontmatter, which the parser never sees. */\n readonly offset: number\n readonly components: boolean\n readonly notations: ReadonlyArray<Notation>\n}\n\n/** Each non-empty line from `first` to `last`, whole. */\nconst lineSpans = (\n type: BuiltinTokenType,\n lines: ReadonlyArray<string>,\n first: number,\n last: number,\n): ReadonlyArray<RawToken> =>\n Arr.filterMap(Arr.range(first, last), (line) =>\n pipe(\n Option.fromNullable(lines[line]),\n Option.filter((text) => text.length > 0),\n Option.map((text) => token(type, line, 0, text.length)),\n ),\n )\n\n/** The line's text, when the file reads components and the line opens with a tag. */\nconst componentLine = (page: Page, line: number): Option.Option<string> =>\n pipe(\n Option.fromNullable(page.lines[line]),\n Option.filter((text) => page.components && COMPONENT_LINE.test(text)),\n )\n\n/** The `>` and spaces that open a quote line, from its indentation onwards. */\nconst quoteToken = (page: Page, node: NodeOfType<\"line\">, line: number): Option.Option<RawToken> =>\n pipe(\n onlyIf(node.quote, page.lines[line] ?? \"\"),\n Option.map((text) => matchLength(/^[> ]*/, text, node.indent)),\n Option.filter((length) => length > 0),\n Option.map((length) => token(\"quote\", line, node.indent, node.indent + length)),\n )\n\n/** A token for each of `notations` whose marker opens the decoration. */\nconst notationTokens = (\n notations: ReadonlyArray<Notation>,\n node: Decoration,\n position: Position,\n): ReadonlyArray<RawToken> =>\n Arr.filterMap(notations, ({ marker, name }) =>\n onlyIf(node.markers.includes(marker), {\n line: position.start.line,\n char: position.start.column,\n length: position.end.column - position.start.column,\n type: \"notation\" as const,\n name,\n }),\n )\n\nconst nodeTokens =\n (page: Page) =>\n (node: AnyNode): Picked<RawToken> => {\n const lineOf = (position: Position) => position.start.line + page.offset\n const shift = (position: Position): Position => ({\n start: { ...position.start, line: position.start.line + page.offset },\n end: { ...position.end, line: position.end.line + page.offset },\n })\n return Match.value(node).pipe(\n Match.discriminators(\"type\")({\n // A page that opens with a component has no title line: the tag is what it is.\n title: (title) =>\n leaf(\n pipe(\n componentLine(page, lineOf(title.position)),\n Option.match({\n onNone: () => [spanToken(\"title\", shift(title.position))],\n onSome: (text) => componentTokens(lineOf(title.position), text),\n }),\n ),\n ),\n codeBlock: (block) =>\n leaf(\n lineSpans(\n \"codeBlock\",\n page.lines,\n lineOf(block.position),\n block.position.end.line + page.offset,\n ),\n ),\n table: (table) =>\n leaf(\n lineSpans(\n \"table\",\n page.lines,\n lineOf(table.position),\n table.position.end.line + page.offset,\n ),\n ),\n // A component tag owns its line, so nothing on it is Cosense notation.\n line: (line) =>\n pipe(\n componentLine(page, lineOf(line.position)),\n Option.match({\n onSome: (text) => leaf(componentTokens(lineOf(line.position), text)),\n onNone: () => branch(Option.toArray(quoteToken(page, line, lineOf(line.position)))),\n }),\n ),\n // Descend: a decoration can wrap a link or an image (`[* [page]]`), and that child\n // needs its own token over the same span.\n decoration: (decoration) =>\n branch([\n ...decorationTokenTypes(decoration).map((type) =>\n spanToken(type, shift(decoration.position)),\n ),\n ...notationTokens(page.notations, decoration, shift(decoration.position)),\n ]),\n }),\n Match.orElse((other) =>\n branch(\n pipe(\n Option.fromNullable(INLINE_TOKEN_TYPE[other.type]),\n Option.map((type) => spanToken(type, shift(other.position))),\n Option.toArray,\n ),\n ),\n ),\n )\n }\n\nexport interface ComputeTokensOptions {\n /** `.csnx` also reads a line that is one component tag. `.csn` never does. */\n readonly components?: boolean\n /** How to parse: the site's own notation extensions, as its build parses with them. */\n readonly parseOptions?: ParseOptions\n /** Notations the caller adds, each sent as a token of its own name (see `Notation`). */\n readonly notations?: ReadonlyArray<Notation>\n /**\n * Whether a `---` fence on the first line opens YAML to skip (default: true). Off for\n * Cosense pages, which have no frontmatter.\n */\n readonly frontmatter?: boolean\n}\n\n/** Pure text -> tokens. The LSP delta encoding lives in encodeTokens, so this stays testable. */\nexport const computeTokens = (text: string, options: ComputeTokensOptions = {}): RawToken[] => {\n const lines = normalizeLineEndings(text).split(\"\\n\")\n const notations = options.notations ?? []\n // Frontmatter is YAML, so the Cosense parser must not see it: `---` would otherwise read\n // as a strikethrough marker and the keys as plain text on the title line.\n const fence = fenceOf(lines, options.frontmatter ?? true)\n const page: Page = {\n lines,\n offset: Option.match(fence, { onNone: () => 0, onSome: (end) => end + 1 }),\n components: options.components ?? false,\n notations,\n }\n\n const frontmatterTokens = Option.match(fence, {\n onNone: () => [],\n onSome: (end) => lineSpans(\"frontmatter\", lines, 0, end),\n })\n const body = parse(lines.slice(page.offset).join(\"\\n\"), options.parseOptions ?? {})\n\n return pipe(\n [...frontmatterTokens, ...gather(body, nodeTokens(page))],\n Arr.filter((t) => t.length > 0),\n Arr.sort(BY_POSITION),\n )\n}\n\n/** The name a token goes by: a notation's is the caller's, any other's its type. */\nconst ownName = (token: RawToken): string =>\n Match.value(token).pipe(\n Match.when({ type: \"notation\" }, (notation) => notation.name),\n Match.orElse((builtin) => builtin.type),\n )\n\n/**\n * Each token's place in `legend`: its own name when the legend has it, else the LSP type\n * its kind is drawn as. LEGEND holds none of the own names, so it always gets the LSP type.\n */\nconst legendIndex = (legend: ReadonlyArray<string>) => {\n const placeOf = (type: string): Option.Option<number> =>\n Option.liftPredicate(legend.indexOf(type), (index) => index >= 0)\n return (token: RawToken): number =>\n pipe(\n placeOf(ownName(token)),\n Option.orElse(() => placeOf(LSP_TYPE[token.type])),\n Option.getOrElse(() => 0),\n )\n}\n\n/**\n * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.\n * `legend` is the one sent at initialize: LEGEND, `legendOf(notations)`, or a legend of the\n * client's own naming such as `[...TOKEN_TYPES, ...names]`.\n */\nexport const encodeTokens = (\n tokens: ReadonlyArray<RawToken>,\n legend: ReadonlyArray<string> = LEGEND,\n): number[] => {\n const indexOf = legendIndex(legend)\n return pipe(\n Arr.sort(tokens, BY_POSITION),\n Arr.mapAccum({ line: 0, char: 0 }, (previous, t) => [\n { line: t.line, char: t.char },\n [\n t.line - previous.line,\n t.line === previous.line ? t.char - previous.char : t.char,\n t.length,\n indexOf(t),\n 0,\n ],\n ]),\n ([, rows]) => Arr.flatten(rows),\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { visit } from "@cosense-toolbox/parser/utils";
2
+ //#region src/tree.ts
3
+ const leaf = (items) => ({
4
+ items,
5
+ descend: false
6
+ });
7
+ const branch = (items) => ({
8
+ items,
9
+ descend: true
10
+ });
11
+ /**
12
+ * Everything `pick` takes from `tree`, in document order.
13
+ *
14
+ * The parser's `visit` hands nodes to a callback, so something has to hold what the callback
15
+ * finds. That is kept to here: `pick` stays a pure function of one node.
16
+ */
17
+ const gather = (tree, pick) => {
18
+ const found = [];
19
+ visit(tree, (node) => {
20
+ const { items, descend } = pick(node);
21
+ found.push(...items);
22
+ return descend ? void 0 : "skip";
23
+ });
24
+ return found;
25
+ };
26
+ //#endregion
27
+ export { gather as n, leaf as r, branch as t };
28
+
29
+ //# sourceMappingURL=tree-sXzD2703.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-sXzD2703.mjs","names":[],"sources":["../src/tree.ts"],"sourcesContent":["import type { AnyNode } from \"@cosense-toolbox/parser\"\nimport { visit } from \"@cosense-toolbox/parser/utils\"\n\n/** What one node gives, and whether its children are read too. */\nexport interface Picked<A> {\n readonly items: ReadonlyArray<A>\n readonly descend: boolean\n}\n\nexport const leaf = <A>(items: ReadonlyArray<A>): Picked<A> => ({ items, descend: false })\n\nexport const branch = <A>(items: ReadonlyArray<A>): Picked<A> => ({ items, descend: true })\n\n/**\n * Everything `pick` takes from `tree`, in document order.\n *\n * The parser's `visit` hands nodes to a callback, so something has to hold what the callback\n * finds. That is kept to here: `pick` stays a pure function of one node.\n */\nexport const gather = <A>(tree: AnyNode, pick: (node: AnyNode) => Picked<A>): ReadonlyArray<A> => {\n const found: A[] = []\n visit(tree, (node) => {\n const { items, descend } = pick(node)\n found.push(...items)\n return descend ? undefined : \"skip\"\n })\n return found\n}\n"],"mappings":";;AASA,MAAa,QAAW,WAAwC;CAAE;CAAO,SAAS;AAAM;AAExF,MAAa,UAAa,WAAwC;CAAE;CAAO,SAAS;AAAK;;;;;;;AAQzF,MAAa,UAAa,MAAe,SAAyD;CAChG,MAAM,QAAa,CAAC;CACpB,MAAM,OAAO,SAAS;EACpB,MAAM,EAAE,OAAO,YAAY,KAAK,IAAI;EACpC,MAAM,KAAK,GAAG,KAAK;EACnB,OAAO,UAAU,KAAA,IAAY;CAC/B,CAAC;CACD,OAAO;AACT"}
@@ -1,8 +1,8 @@
1
- import { d as gather, f as leaf, o as fenceOf, t as COMPONENT_LINE, u as branch } from "./tokens-D0NP_uLo.mjs";
1
+ import { n as gather, r as leaf, t as branch } from "./tree-sXzD2703.mjs";
2
+ import { COMPONENT_LINE, fenceOf } from "./tokens.mjs";
2
3
  import { normalizeForMatch } from "./completion.mjs";
3
- import { Array, Effect, Match, Option, Record, Schema, pipe } from "effect";
4
+ import { Array, Effect, Match, Option, Schema, pipe } from "effect";
4
5
  import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
5
- import { customDecorations } from "@cosense-toolbox/parser/extensions";
6
6
  import { DiagnosticSeverity } from "vscode-languageserver/node";
7
7
  import { basename, extname, join, relative, resolve, sep } from "node:path";
8
8
  import { readFile, readdir } from "node:fs/promises";
@@ -63,44 +63,6 @@ const unresolvedLinkDiagnostics = (index, text, options) => {
63
63
  }));
64
64
  };
65
65
  //#endregion
66
- //#region src/settings.ts
67
- const defaultSettings = {
68
- sources: [],
69
- decorations: [],
70
- unresolvedLinks: "warning",
71
- frontmatter: true
72
- };
73
- /** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */
74
- const decodeOr = (schema, fallback) => (value) => Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback);
75
- /** The options as an object, which the editor may send in any shape or not at all. */
76
- const OptionsObject = Schema.Record({
77
- key: Schema.String,
78
- value: Schema.Unknown
79
- });
80
- /**
81
- * The non-empty strings of a list. Anything else in it is dropped rather than failing the
82
- * whole list, and anything but a list counts as none.
83
- */
84
- const stringsOf = (value) => Array.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString));
85
- /**
86
- * Each setting read on its own, so one of the wrong shape falls back to its default
87
- * without taking the others with it.
88
- */
89
- const settingsOf = (options) => {
90
- const field = (key) => pipe(Schema.decodeUnknownOption(OptionsObject)(options), Option.flatMap(Record.get(key)), Option.getOrUndefined);
91
- return {
92
- sources: stringsOf(field("sources")),
93
- decorations: stringsOf(field("decorations")),
94
- unresolvedLinks: severityOf(field("unresolvedLinks")),
95
- frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field("frontmatter"))
96
- };
97
- };
98
- /** How the pages are parsed, so the server reads notation as the site's build does. */
99
- const parseOptionsOf = ({ decorations }) => Array.match(decorations, {
100
- onEmpty: () => ({}),
101
- onNonEmpty: (markers) => ({ extensions: [customDecorations(markers)] })
102
- });
103
- //#endregion
104
66
  //#region src/workspace.ts
105
67
  const emptyIndex = { pages: [] };
106
68
  const PAGE_EXTENSIONS = [".csn", ".csnx"];
@@ -166,6 +128,6 @@ const rootsOf = (folders, sources) => pipe(Array.filterMap(folders ?? [], ({ uri
166
128
  onNonEmpty: (under) => Array.map(under, (source) => resolve(folder, source))
167
129
  })));
168
130
  //#endregion
169
- export { rootsOf as a, settingsOf as c, readPageFiles as i, severityOf as l, indexOf as n, defaultSettings as o, readIndex as r, parseOptionsOf as s, emptyIndex as t, unresolvedLinkDiagnostics as u };
131
+ export { rootsOf as a, readPageFiles as i, indexOf as n, severityOf as o, readIndex as r, unresolvedLinkDiagnostics as s, emptyIndex as t };
170
132
 
171
- //# sourceMappingURL=workspace-DouAOZnK.mjs.map
133
+ //# sourceMappingURL=workspace-A-W0PU0g.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-A-W0PU0g.mjs","names":["Arr","Arr"],"sources":["../src/diagnostics.ts","../src/workspace.ts"],"sourcesContent":["import {\n type NodeOfType,\n normalizeLineEndings,\n parse,\n type ParseOptions,\n} from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Match, Option, pipe, Schema } from \"effect\"\nimport { type Diagnostic, DiagnosticSeverity } from \"vscode-languageserver/node\"\n\nimport { normalizeForMatch } from \"./completion\"\nimport { COMPONENT_LINE, fenceOf } from \"./tokens\"\nimport { branch, gather, leaf } from \"./tree\"\nimport type { Index } from \"./workspace\"\n\n/**\n * Links to pages no file holds.\n *\n * Only `[title]` links are checked. A tag names a tag page more often than a page, and a\n * `[/project/page]` link points outside the workspace; neither is expected to have a file.\n */\n\n/** How loudly an unresolved link is reported, as the reader sets it. */\nexport const UnresolvedSeverity = Schema.Literal(\"off\", \"hint\", \"information\", \"warning\", \"error\")\n\nexport type UnresolvedSeverity = typeof UnresolvedSeverity.Type\n\n/**\n * The severity a setting names, or `warning` for one that is missing or unknown: the build\n * warns about the same links by default (`unresolved: \"warn\"`).\n */\nexport const severityOf = (setting: unknown): UnresolvedSeverity =>\n Option.getOrElse(\n Schema.decodeUnknownOption(UnresolvedSeverity)(setting),\n (): UnresolvedSeverity => \"warning\",\n )\n\n/** The LSP's severity for a setting, or None for `off`. */\nconst lspSeverity = (severity: UnresolvedSeverity): Option.Option<DiagnosticSeverity> =>\n Match.value(severity).pipe(\n Match.when(\"off\", () => Option.none()),\n Match.when(\"hint\", () => Option.some(DiagnosticSeverity.Hint)),\n Match.when(\"information\", () => Option.some(DiagnosticSeverity.Information)),\n Match.when(\"warning\", () => Option.some(DiagnosticSeverity.Warning)),\n Match.when(\"error\", () => Option.some(DiagnosticSeverity.Error)),\n Match.exhaustive,\n )\n\nexport interface UnresolvedLinkOptions {\n readonly severity: UnresolvedSeverity\n /** `.csnx`: a component line is JSX, and a bracket in it is not a link. */\n readonly components?: boolean\n /** How to parse: the site's notation extensions, so `[! 注意]` is not read as a link. */\n readonly parseOptions?: ParseOptions\n /** Whether a `---` fence on the first line opens YAML to skip (default: true). */\n readonly frontmatter?: boolean\n}\n\n/** A `[title]` link, and the line it is on in the file. */\ninterface LinkAt {\n readonly link: NodeOfType<\"internalLink\">\n readonly line: number\n}\n\n/**\n * Every `[title]` link in the body, with the line it is on in the file. The title line holds\n * no links, and neither does a `.csnx` component line.\n */\nconst linksIn = (\n text: string,\n {\n components = false,\n parseOptions = {},\n frontmatter = true,\n }: Omit<UnresolvedLinkOptions, \"severity\">,\n): ReadonlyArray<LinkAt> => {\n const lines = normalizeLineEndings(text).split(\"\\n\")\n // The parser never sees the frontmatter, so its line numbers start after the fence.\n const offset = Option.match(fenceOf(lines, frontmatter), {\n onNone: () => 0,\n onSome: (end) => end + 1,\n })\n const isComponentLine = (line: number) =>\n components && COMPONENT_LINE.test(lines[line + offset] ?? \"\")\n\n return gather(parse(lines.slice(offset).join(\"\\n\"), parseOptions), (node) =>\n Match.value(node).pipe(\n Match.when({ type: \"title\" }, () => leaf<LinkAt>([])),\n Match.when({ type: \"line\" }, (line) =>\n isComponentLine(line.position.start.line) ? leaf<LinkAt>([]) : branch<LinkAt>([]),\n ),\n Match.when({ type: \"internalLink\" }, (link) =>\n branch([{ link, line: link.position.start.line + offset }]),\n ),\n Match.orElse(() => branch<LinkAt>([])),\n ),\n )\n}\n\n/** A diagnostic for each `[title]` link in `text` whose page is not in `index`. */\nexport const unresolvedLinkDiagnostics = (\n index: Index,\n text: string,\n options: UnresolvedLinkOptions,\n): Diagnostic[] => {\n const withFile = new Set(Arr.map(index.pages, (page) => normalizeForMatch(page.title)))\n return pipe(\n lspSeverity(options.severity),\n Option.match({\n onNone: () => [],\n onSome: (severity) =>\n pipe(\n linksIn(text, options),\n Arr.filter(({ link }) => !withFile.has(normalizeForMatch(link.target))),\n Arr.map(({ link, line }): Diagnostic => ({\n range: {\n start: { line, character: link.position.start.column },\n end: { line, character: link.position.end.column },\n },\n severity,\n source: \"cosense\",\n code: \"unresolved-link\",\n message: `リンク先のページが見つからない: [${link.target}]`,\n })),\n ),\n }),\n )\n}\n","import type { Dirent } from \"node:fs\"\nimport { readdir, readFile } from \"node:fs/promises\"\nimport { basename, extname, join, relative, resolve, sep } from \"node:path\"\nimport { fileURLToPath, pathToFileURL } from \"node:url\"\n\nimport { Array as Arr, Effect, Match, Option, pipe } from \"effect\"\n\n/**\n * The pages a workspace holds, read from disk.\n *\n * This is the one place the language server differs from a Cosense client: there is no\n * project and no account, only `.csn` and `.csnx` files. A title is what the file calls\n * itself: its first line.\n */\nexport interface Page {\n readonly title: string\n /** `file://` URI of the page's file. */\n readonly uri: string\n /**\n * Where the file is, from the root it was found under, with `/` between directories. Shown\n * next to the title so two pages of one title can be told apart.\n */\n readonly location: string\n}\n\nexport interface Index {\n /**\n * Every page file, in the order the roots and their directories list them. A link to a\n * page is not a page: only a file is, since only a file has a first line to be its title.\n */\n readonly pages: ReadonlyArray<Page>\n}\n\nexport const emptyIndex: Index = { pages: [] }\n\n// --- Finding the files --------------------------------------------------------------------\n\nconst PAGE_EXTENSIONS: ReadonlyArray<string> = [\".csn\", \".csnx\"]\n\n// Directories that never hold pages and can be very large. Skipped by name, because\n// walking into node_modules is the difference between instant and unusable.\nconst SKIP_DIRECTORIES: ReadonlyArray<string> = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".astro\",\n \".turbo\",\n \".cache\",\n]\n\n/** A directory's entries. One that cannot be read holds nothing, rather than failing the walk. */\nconst entriesOf = (directory: string): Effect.Effect<ReadonlyArray<Dirent>> =>\n pipe(\n Effect.tryPromise(() => readdir(directory, { withFileTypes: true })),\n Effect.orElseSucceed(() => []),\n )\n\n/** The page files one entry of `directory` holds: itself, what is under it, or none. */\nconst filesAt = (directory: string, entry: Dirent): Effect.Effect<ReadonlyArray<string>> =>\n Match.value(entry).pipe(\n Match.when(\n (dir) => dir.isDirectory() && Arr.contains(SKIP_DIRECTORIES, dir.name),\n () => Effect.succeed([]),\n ),\n Match.when(\n (dir) => dir.isDirectory(),\n () => pageFiles(join(directory, entry.name)),\n ),\n Match.when(\n (file) => Arr.contains(PAGE_EXTENSIONS, extname(file.name)),\n () => Effect.succeed([join(directory, entry.name)]),\n ),\n Match.orElse(() => Effect.succeed([])),\n )\n\n/** Every page file under `directory`, depth-first, in the order the directory lists them. */\nconst pageFiles = (directory: string): Effect.Effect<ReadonlyArray<string>> =>\n pipe(\n entriesOf(directory),\n Effect.map(Arr.filter((entry) => !entry.name.startsWith(\".\"))),\n Effect.flatMap(Effect.forEach((entry) => filesAt(directory, entry))),\n Effect.map(Arr.flatten),\n )\n\n// --- Reading one file ---------------------------------------------------------------------\n\n/** `text` past the leading `---` fence, which is YAML rather than a line of the page. */\nconst withoutFrontmatter = (text: string): string =>\n pipe(\n Option.liftPredicate(text, (all) => all.startsWith(\"---\\n\")),\n Option.flatMap((all) =>\n pipe(\n all.indexOf(\"\\n---\", 3),\n Option.liftPredicate((end) => end !== -1),\n Option.map((end) => all.slice(end + 4)),\n ),\n ),\n Option.getOrElse(() => text),\n )\n\nconst firstLine = (text: string): Option.Option<string> =>\n pipe(\n text.split(\"\\n\"),\n Arr.map((line) => line.trim()),\n Arr.findFirst((line) => line !== \"\"),\n )\n\n/**\n * What the file calls itself: its first line.\n *\n * The frontmatter is skipped rather than read. Cosense has no notion of a title held apart\n * from the text — the first line *is* the title, and a link finds a page by it. A `title:`\n * that disagreed with the first line would name a page nobody can see, so it is ignored\n * here even though `metadata.ts` still prefers it.\n *\n * An empty file falls back to its name, so it stays linkable.\n */\nconst titleOf = (path: string, text: string, frontmatter: boolean): string =>\n pipe(\n text.replace(/\\r\\n?/g, \"\\n\"),\n (normalized) =>\n Option.getOrElse(\n Option.map(\n Option.liftPredicate(normalized, () => frontmatter),\n withoutFrontmatter,\n ),\n () => normalized,\n ),\n firstLine,\n Option.getOrElse(() => basename(path, extname(path))),\n )\n\n/** `path` from `root`, with `/` between directories whatever the platform. */\nconst locationOf = (root: string, path: string): string => relative(root, path).split(sep).join(\"/\")\n\n/** How the pages are read. */\nexport interface ReadIndexOptions {\n /**\n * Whether a `---` fence on the first line opens YAML to skip (default: true). Off for\n * Cosense pages, which have no frontmatter: a page titled `---` keeps its title.\n */\n readonly frontmatter?: boolean\n}\n\n/** A page file as read: the page it makes, where it is on disk, and its text. */\nexport interface PageFile {\n readonly page: Page\n readonly path: string\n readonly text: string\n}\n\n/** A file that cannot be read is left out rather than failing the rest. */\nconst readPageFile = (\n root: string,\n path: string,\n { frontmatter = true }: ReadIndexOptions,\n): Effect.Effect<Option.Option<PageFile>> =>\n pipe(\n Effect.tryPromise(() => readFile(path, \"utf8\")),\n Effect.map((text) =>\n Option.some({\n page: {\n title: titleOf(path, text, frontmatter),\n uri: pathToFileURL(path).href,\n location: locationOf(root, path),\n },\n path,\n text,\n }),\n ),\n Effect.orElseSucceed(() => Option.none()),\n )\n\n/** The page files under one root. */\nconst filesUnder =\n (options: ReadIndexOptions) =>\n (root: string): Effect.Effect<ReadonlyArray<PageFile>> =>\n pipe(\n pageFiles(root),\n Effect.flatMap(Effect.forEach((path) => readPageFile(root, path, options))),\n Effect.map(Arr.getSomes),\n )\n\n/**\n * Every page file under `roots`. Roots may overlap (`src` and `src/content`), so a file\n * found twice is kept once, where it was first found.\n */\nexport const readPageFiles = (\n roots: ReadonlyArray<string>,\n options: ReadIndexOptions = {},\n): Effect.Effect<ReadonlyArray<PageFile>> =>\n pipe(\n Effect.forEach(roots, filesUnder(options)),\n Effect.map((found) => Arr.dedupeWith(Arr.flatten(found), (a, b) => a.path === b.path)),\n )\n\n/** The pages of `files`, as an index. */\nexport const indexOf = (files: ReadonlyArray<PageFile>): Index => ({\n pages: Arr.map(files, (file) => file.page),\n})\n\n/** Read every page under `roots` and index it. */\nexport const readIndex = (\n roots: ReadonlyArray<string>,\n options: ReadIndexOptions = {},\n): Effect.Effect<Index> => Effect.map(readPageFiles(roots, options), indexOf)\n\n/**\n * Where to read pages: each workspace folder, or each of `sources` under it when the reader\n * has set them. A folder that is not a `file://` URI has no files to read.\n */\nexport const rootsOf = (\n folders: ReadonlyArray<{ readonly uri: string }> | null | undefined,\n sources: ReadonlyArray<string>,\n): ReadonlyArray<string> =>\n pipe(\n Arr.filterMap(folders ?? [], ({ uri }) => Option.liftThrowable(fileURLToPath)(uri)),\n Arr.flatMap((folder) =>\n Arr.match(sources, {\n onEmpty: () => [folder],\n onNonEmpty: (under) => Arr.map(under, (source) => resolve(folder, source)),\n }),\n ),\n )\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosense-toolbox/lsp",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "type": "module",
5
5
  "description": "Language server for .csn and .csnx, and a check of a whole site's links: csn-lsp.",
6
6
  "license": "MIT",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/completion.d.mts",
29
29
  "default": "./dist/completion.mjs"
30
30
  },
31
+ "./link": {
32
+ "types": "./dist/link.d.mts",
33
+ "default": "./dist/link.mjs"
34
+ },
31
35
  "./tokens": {
32
36
  "types": "./dist/tokens.d.mts",
33
37
  "default": "./dist/tokens.mjs"
@@ -42,10 +46,12 @@
42
46
  "build": "tsdown",
43
47
  "typecheck": "tsc --noEmit",
44
48
  "test": "vitest run",
49
+ "lint": "eslint .",
50
+ "format:check": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore",
45
51
  "prepublishOnly": "bun run build"
46
52
  },
47
53
  "dependencies": {
48
- "@cosense-toolbox/parser": "0.1.0-beta.2",
54
+ "@cosense-toolbox/parser": "0.1.0-beta.4",
49
55
  "effect": "^3.22.1",
50
56
  "vscode-languageserver": "^10.1.0",
51
57
  "vscode-languageserver-textdocument": "^1.0.12"
@@ -57,7 +63,6 @@
57
63
  },
58
64
  "publishConfig": {
59
65
  "access": "public",
60
- "tag": "beta",
61
66
  "registry": "https://registry.npmjs.org/"
62
67
  }
63
68
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-DPPxtJ6Y.mjs","names":["Arr"],"sources":["../src/server.ts"],"sourcesContent":["import { Array as Arr, Effect, Option, pipe, Ref } from \"effect\"\nimport {\n type CompletionItem,\n createConnection,\n type Definition,\n type InitializeResult,\n ProposedFeatures,\n type SemanticTokens,\n TextDocuments,\n TextDocumentSyncKind,\n} from \"vscode-languageserver/node\"\nimport { TextDocument } from \"vscode-languageserver-textdocument\"\n\nimport { completionItems, definitionOf } from \"./completion\"\nimport { unresolvedLinkDiagnostics } from \"./diagnostics\"\nimport { defaultSettings, parseOptionsOf, settingsOf } from \"./settings\"\nimport { computeTokens, encodeTokens, LEGEND } from \"./tokens\"\nimport { emptyIndex, type Index, readIndex, rootsOf } from \"./workspace\"\n\nconst connection = createConnection(ProposedFeatures.all)\nconst documents = new TextDocuments(TextDocument)\n\n/** The language ids editors give `.csnx`. Not every editor names the language the same way. */\nconst CSNX_LANGUAGE_IDS: ReadonlyArray<string> = [\"csnx\", \"cosense-x\"]\n\n/**\n * `.csnx` is `.csn` plus component lines, so the format is decided per document. The\n * language id the editor sends is trusted first; a file:// URI's suffix is the fallback.\n */\nconst readsComponents = (document: TextDocument): boolean =>\n Arr.contains(CSNX_LANGUAGE_IDS, document.languageId) || document.uri.endsWith(\".csnx\")\n\n// --- Settings -----------------------------------------------------------------------------\n\n/** What the reader set in the editor's initialization options (see settings.ts). */\nconst settings = Ref.unsafeMake(defaultSettings)\n\nconst currentSettings = () => Effect.runSync(Ref.get(settings))\n\n/** How to parse, so notation reads as the site's build reads it. */\nconst currentParseOptions = () => parseOptionsOf(currentSettings())\n\nconst semanticTokensOf = (document: TextDocument): SemanticTokens => ({\n data: encodeTokens(\n computeTokens(document.getText(), {\n components: readsComponents(document),\n parseOptions: currentParseOptions(),\n frontmatter: currentSettings().frontmatter,\n }),\n ),\n})\n\n/**\n * `answer` for the open document at `uri`, or `fallback` when the editor asks about one it\n * has not synced yet.\n */\nconst withDocument = <A>(uri: string, answer: (document: TextDocument) => A, fallback: A): A =>\n pipe(\n Option.fromNullable(documents.get(uri)),\n Option.map(answer),\n Option.getOrElse(() => fallback),\n )\n\n// --- The workspace index --------------------------------------------------------------------\n\n/**\n * Every page the workspace holds. Read once at initialize and again when a page is saved,\n * because a title only changes by someone writing it, and re-reading on every keystroke\n * would walk the whole tree while the reader is typing.\n */\nconst index = Ref.unsafeMake<Option.Option<Index>>(Option.none())\nconst roots = Ref.unsafeMake<ReadonlyArray<string>>([])\n\n/** The index as it stands; empty until it has been read once. */\nconst currentIndex = (): Index => Option.getOrElse(Effect.runSync(Ref.get(index)), () => emptyIndex)\n\n// --- Diagnostics --------------------------------------------------------------------------\n\n/**\n * Sends `document`'s links to missing pages. Nothing until the index has been read once:\n * against an empty index every link would be flagged, only to be cleared a moment later.\n */\nconst publishDiagnostics = (document: TextDocument): Effect.Effect<void> =>\n pipe(\n Effect.all([Ref.get(index), Ref.get(settings)]),\n Effect.flatMap(([read, set]) =>\n Option.match(read, {\n onNone: () => Effect.void,\n onSome: (pages) =>\n Effect.promise(() =>\n connection.sendDiagnostics({\n uri: document.uri,\n diagnostics: unresolvedLinkDiagnostics(pages, document.getText(), {\n severity: set.unresolvedLinks,\n components: readsComponents(document),\n parseOptions: parseOptionsOf(set),\n frontmatter: set.frontmatter,\n }),\n }),\n ),\n }),\n ),\n )\n\n/**\n * Reads the index again in the background; requests meanwhile answer from the last one.\n * A page may have appeared or gone, so every open document is checked again after.\n */\nconst refreshIndex = (): void => {\n pipe(\n Effect.all([Ref.get(roots), Ref.get(settings)]),\n Effect.flatMap(([at, set]) => readIndex(at, { frontmatter: set.frontmatter })),\n Effect.flatMap((next) => Ref.set(index, Option.some(next))),\n Effect.flatMap(() => Effect.forEach(documents.all(), publishDiagnostics, { discard: true })),\n Effect.runFork,\n )\n}\n\n// --- Handlers -----------------------------------------------------------------------------\n\nconnection.onInitialize(({ workspaceFolders, initializationOptions }): InitializeResult => {\n const read = settingsOf(initializationOptions)\n Effect.runSync(Ref.set(settings, read))\n Effect.runSync(Ref.set(roots, rootsOf(workspaceFolders, read.sources)))\n refreshIndex()\n return {\n capabilities: {\n // Incremental: a page is one file the reader types into, and resending all of it on\n // every keystroke is what makes a large page feel slow.\n textDocumentSync: TextDocumentSyncKind.Incremental,\n semanticTokensProvider: {\n legend: { tokenTypes: [...LEGEND], tokenModifiers: [] },\n full: true,\n },\n // `[` and `#` open a link and a tag. The editor asks again on every character after\n // one, so nothing else has to be declared for typing to keep the menu up to date.\n completionProvider: { triggerCharacters: [\"[\", \"#\"] },\n definitionProvider: true,\n },\n }\n})\n\n// A title only changes when a page is written, so the index is rebuilt then rather than\n// on a timer or on every request.\ndocuments.onDidSave(refreshIndex)\n\n// Opening a document counts as a change, so this covers both.\ndocuments.onDidChangeContent(({ document }) => {\n Effect.runFork(publishDiagnostics(document))\n})\n\n// A closed document's diagnostics would otherwise linger in the editor's problem list.\ndocuments.onDidClose(({ document }) => {\n Effect.runFork(\n Effect.promise(() => connection.sendDiagnostics({ uri: document.uri, diagnostics: [] })),\n )\n})\n\nconnection.onCompletion(({ textDocument: { uri }, position }): CompletionItem[] =>\n withDocument(\n uri,\n (document) =>\n completionItems(currentIndex(), document.getText(), position, currentParseOptions()),\n [],\n ),\n)\n\nconnection.onDefinition(({ textDocument: { uri }, position }): Definition | null =>\n withDocument(\n uri,\n (document) =>\n Option.getOrNull(\n definitionOf(currentIndex(), document.getText(), position, currentParseOptions()),\n ),\n null,\n ),\n)\n\nconnection.languages.semanticTokens.on(({ textDocument: { uri } }): SemanticTokens =>\n // Asked before the document was synced: nothing to colour yet.\n withDocument(uri, semanticTokensOf, { data: [] }),\n)\n\ndocuments.listen(connection)\nconnection.listen()\n"],"mappings":"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"}