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

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/README.md CHANGED
@@ -21,7 +21,6 @@ csn-lsp --stdio
21
21
  | 設定 | 内容 | 既定 |
22
22
  | :---------------- | :------------------------------------------------------------------------------------- | :----------------- |
23
23
  | `sources` | ページを読む場所。ワークスペースからの相対パス | ワークスペース全体 |
24
- | `decorations` | サイトの `customDecorations` と同じ記号。`[! 注意]` をリンクではなく装飾記法として読む | なし |
25
24
  | `unresolvedLinks` | 存在しないページへのリンクの診断。`off` / `hint` / `information` / `warning` / `error` | `warning` |
26
25
  | `frontmatter` | 1 行目の `---` を frontmatter (YAML) として飛ばすか | `true` |
27
26
 
@@ -30,18 +29,18 @@ csn-lsp --stdio
30
29
  エディタの診断と同じ判定で、サイトのすべてのページのリンクを調べる。CI で使う想定。
31
30
 
32
31
  ```sh
33
- csn-lsp check src/content src/pages --decorations '|!~#'
32
+ csn-lsp check src/content src/pages
34
33
  # src/content/posts/a.csn:5:3 error リンク先のページが見つからない: [無いページ]
35
34
  ```
36
35
 
37
36
  - 引数のディレクトリの下の `.csn` / `.csnx` を読む。省略すると今いるディレクトリを読む
38
37
  - ページの題名はファイルの 1 行目。大文字小文字と、空白と `_` の違いは無視する
38
+ - `[! 注意]` のような Cosense の文字装飾の記号の括弧は、Cosense Web と同じく装飾として読み、リンクとして調べない
39
39
  - エラーが 1 件でもあれば終了コード 1、無ければ 0。オプションの誤りは 2
40
40
 
41
41
  | オプション | 内容 | 既定 |
42
42
  | :--------------------------- | :--------------------------------------------------- | :------ |
43
43
  | `--unresolved-links <level>` | `off` / `hint` / `information` / `warning` / `error` | `error` |
44
- | `--decorations <markers>` | サイトの装飾記法の記号を 1 つの文字列で (`'\|!~#'`) | なし |
45
44
  | `--no-frontmatter` | 1 行目の `---` を frontmatter ではなく題名として読む | |
46
45
 
47
46
  `check` の既定が `error` なのは、ビルドを止めるための道具だから。報告だけにしたいときは
@@ -51,7 +50,8 @@ csn-lsp check src/content src/pages --decorations '|!~#'
51
50
 
52
51
  エディタのプラグインなどから、部品として使える。
53
52
 
54
- - `@cosense-toolbox/lsp/tokens`: `computeTokens` / `encodeTokens` / `legendOf` / `withNotations` など
55
- - `notations` で独自の記法 (記号と、送るトークンの型の名前) を足せる
53
+ - `@cosense-toolbox/lsp/tokens`: `computeTokens` / `encodeTokens` / `legendOf` など
54
+ - `notations` で装飾の記号に名前を付け、その名前のトークンとして送れる (`[! 注意]` の `!` に `warning` など)。
55
+ 記号は Cosense の文字装飾の記号 (`!"#%&'()*+,-./{|}<>_~=`) に限る。ほかの記号 (`@` など) の括弧はリンクのまま
56
56
  - `frontmatter: false` で、1 行目の `---` を YAML として飛ばさずに読める
57
57
  - `@cosense-toolbox/lsp/completion`: `detectCompletion` / `completionItems` / `definitionOf` など
package/dist/check.d.mts CHANGED
@@ -19,7 +19,7 @@ type UnresolvedSeverity = typeof UnresolvedSeverity.Type;
19
19
  *
20
20
  * The same functions decide as in the editor, so what the editor flags is what fails here.
21
21
  */
22
- declare const USAGE = "Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --decorations <markers> the site's own decoration markers, as one string: '|!~#'\n --no-frontmatter read a first line of --- as the title, not as YAML\n";
22
+ declare const USAGE = "Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --no-frontmatter read a first line of --- as the title, not as YAML\n";
23
23
  /** What the command prints, and the code it exits with. */
24
24
  interface CheckResult {
25
25
  readonly output: string;
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.mts","names":[],"sources":["../src/diagnostics.ts","../src/check.ts"],"mappings":";;;;;;;;;;;;cAsBa,oBAAkB,OAAA;KAEnB,4BAA4B,mBAAmB;;;;;;;;cCP9C;;UAYI;WACN;WACA;;;UAIM;;WAEN,OAAO;;WAEP,iBAAiB;;WAEjB,eAAe;;WAEf;;KAGC;;UAGK;WACN;WACA;WACA;WACA,OAAO;WACP;;;;;;cA+DE,cAAa,OAAA,iBAAA,cAAA,eAKvB,qBAAmB,OAAO,OAAO,cAAc;;cAuBrC,WAAY,MAAM,uBAAuB,gBAAc,OAAO,OAAO"}
1
+ {"version":3,"file":"check.d.mts","names":[],"sources":["../src/diagnostics.ts","../src/check.ts"],"mappings":";;;;;;;;;;;;cAsBa,oBAAkB,OAAA;KAEnB,4BAA4B,mBAAmB;;;;;;;;cCR9C;;UAWI;WACN;WACA;;;UAIM;;WAEN,OAAO;;WAEP,iBAAiB;;WAEjB,eAAe;;WAEf;;KAGC;;UAGK;WACN;WACA;WACA;WACA,OAAO;WACP;;;;;;cAyDE,cAAa,OAAA,iBAAA,cAAA,eAKvB,qBAAmB,OAAO,OAAO,cAAc;;cAuBrC,WAAY,MAAM,uBAAuB,gBAAc,OAAO,OAAO"}
package/dist/check.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as readPageFiles, l as severityOf, n as indexOf, o as defaultSettings, s as parseOptionsOf, u as unresolvedLinkDiagnostics } from "./workspace-DouAOZnK.mjs";
2
- import { Array as Array$1, Effect, Match, Option, Predicate, pipe } from "effect";
1
+ import { i as readPageFiles, n as indexOf, o as severityOf, s as unresolvedLinkDiagnostics } from "./workspace-XJqmAxae.mjs";
2
+ import { Array, Effect, Match, Option, Predicate, pipe } from "effect";
3
3
  import { DiagnosticSeverity } from "vscode-languageserver/node";
4
4
  import { relative, resolve, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
@@ -16,7 +16,6 @@ every [link] to a page none of them holds.
16
16
 
17
17
  Options:
18
18
  --unresolved-links <level> off | hint | information | warning | error (default: error)
19
- --decorations <markers> the site's own decoration markers, as one string: '|!~#'
20
19
  --no-frontmatter read a first line of --- as the title, not as YAML
21
20
  `;
22
21
  /**
@@ -30,22 +29,17 @@ const argsOf = (args, cwd) => pipe(Option.liftThrowable(parseArgs)({
30
29
  strict: true,
31
30
  options: {
32
31
  "unresolved-links": { type: "string" },
33
- decorations: { type: "string" },
34
32
  "no-frontmatter": { type: "boolean" }
35
33
  }
36
34
  }), Option.map(({ values, positionals }) => ({
37
- roots: Array$1.match(positionals, {
35
+ roots: Array.match(positionals, {
38
36
  onEmpty: () => [cwd],
39
- onNonEmpty: (directories) => Array$1.map(directories, (directory) => resolve(cwd, directory))
37
+ onNonEmpty: (directories) => Array.map(directories, (directory) => resolve(cwd, directory))
40
38
  }),
41
39
  unresolvedLinks: pipe(Option.fromNullable(values["unresolved-links"]), Option.match({
42
40
  onNone: () => DEFAULT_LEVEL,
43
41
  onSome: severityOf
44
42
  })),
45
- parseOptions: parseOptionsOf({
46
- ...defaultSettings,
47
- decorations: Array.from(values.decorations ?? "")
48
- }),
49
43
  frontmatter: values["no-frontmatter"] !== true
50
44
  })));
51
45
  const levelOf = (severity) => Match.value(severity).pipe(Match.when(DiagnosticSeverity.Error, () => "error"), Match.when(DiagnosticSeverity.Warning, () => "warning"), Match.when(DiagnosticSeverity.Information, () => "information"), Match.orElse(() => "hint"));
@@ -63,7 +57,7 @@ const reportOf = (file) => ({ range, severity, message }) => ({
63
57
  */
64
58
  const checkSite = ({ roots, unresolvedLinks, parseOptions = {}, frontmatter = true }) => Effect.map(readPageFiles(roots, { frontmatter }), (files) => {
65
59
  const index = indexOf(files);
66
- return Array$1.flatMap(files, (file) => Array$1.map(unresolvedLinkDiagnostics(index, file.text, {
60
+ return Array.flatMap(files, (file) => Array.map(unresolvedLinkDiagnostics(index, file.text, {
67
61
  severity: unresolvedLinks,
68
62
  components: file.path.endsWith(".csnx"),
69
63
  parseOptions,
@@ -79,8 +73,8 @@ const runCheck = (args, cwd) => Option.match(argsOf(args, cwd), {
79
73
  exitCode: 2
80
74
  }),
81
75
  onSome: (options) => Effect.map(checkSite(options), (reports) => ({
82
- output: Array$1.map(reports, lineOf(cwd)).join(""),
83
- exitCode: Array$1.some(reports, ({ level }) => level === "error") ? 1 : 0
76
+ output: Array.map(reports, lineOf(cwd)).join(""),
77
+ exitCode: Array.some(reports, ({ level }) => level === "error") ? 1 : 0
84
78
  }))
85
79
  });
86
80
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"check.mjs","names":["Arr"],"sources":["../src/check.ts"],"sourcesContent":["import { relative, resolve, sep } from \"node:path\"\nimport { parseArgs } from \"node:util\"\n\nimport type { ParseOptions } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Effect, Match, Option, pipe, Predicate } from \"effect\"\nimport { type Diagnostic, DiagnosticSeverity } from \"vscode-languageserver/node\"\n\nimport { severityOf, unresolvedLinkDiagnostics, type UnresolvedSeverity } from \"./diagnostics\"\nimport { defaultSettings, parseOptionsOf } from \"./settings\"\nimport { indexOf, type PageFile, readPageFiles } from \"./workspace\"\n\n/**\n * `csn-lsp check`: the server's diagnostics for every page of a site, for a terminal or CI.\n *\n * The same functions decide as in the editor, so what the editor flags is what fails here.\n */\n\nexport const USAGE = `Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --decorations <markers> the site's own decoration markers, as one string: '|!~#'\n --no-frontmatter read a first line of --- as the title, not as YAML\n`\n\n/** What the command prints, and the code it exits with. */\nexport interface CheckResult {\n readonly output: string\n readonly exitCode: number\n}\n\n/** Where to look and how to read, for `checkSite`. */\nexport interface CheckSiteOptions {\n /** The directories whose `.csn` / `.csnx` pages are read, and checked against each other. */\n readonly roots: ReadonlyArray<string>\n /** How a link to a missing page is reported; `off` reports none. */\n readonly unresolvedLinks: UnresolvedSeverity\n /** How to parse: a site's notation extensions, as its build parses with. */\n readonly parseOptions?: ParseOptions\n /** Whether a `---` fence on a page's first line opens YAML to skip (default: true). */\n readonly frontmatter?: boolean\n}\n\nexport type ReportLevel = \"error\" | \"warning\" | \"information\" | \"hint\"\n\n/** One thing found: where, counting lines and columns from 1, how loudly, and what. */\nexport interface Report {\n readonly path: string\n readonly line: number\n readonly column: number\n readonly level: ReportLevel\n readonly message: string\n}\n\n/**\n * Reports are errors unless told otherwise: the command exists to stop a build, and a\n * warning that passes would not.\n */\nconst DEFAULT_LEVEL: UnresolvedSeverity = \"error\"\n\nconst argsOf = (args: ReadonlyArray<string>, cwd: string): Option.Option<CheckSiteOptions> =>\n pipe(\n Option.liftThrowable(parseArgs)({\n args: [...args],\n allowPositionals: true,\n strict: true,\n options: {\n \"unresolved-links\": { type: \"string\" },\n decorations: { type: \"string\" },\n \"no-frontmatter\": { type: \"boolean\" },\n },\n }),\n Option.map(({ values, positionals }) => ({\n roots: Arr.match(positionals, {\n onEmpty: () => [cwd],\n onNonEmpty: (directories) => Arr.map(directories, (directory) => resolve(cwd, directory)),\n }),\n unresolvedLinks: pipe(\n Option.fromNullable(values[\"unresolved-links\"]),\n Option.match({ onNone: () => DEFAULT_LEVEL, onSome: severityOf }),\n ),\n // Each marker is one character, so a string of them is the list.\n parseOptions: parseOptionsOf({\n ...defaultSettings,\n decorations: Array.from(values.decorations ?? \"\"),\n }),\n frontmatter: values[\"no-frontmatter\"] !== true,\n })),\n )\n\nconst levelOf = (severity: DiagnosticSeverity | undefined): ReportLevel =>\n Match.value(severity).pipe(\n Match.when(DiagnosticSeverity.Error, (): ReportLevel => \"error\"),\n Match.when(DiagnosticSeverity.Warning, (): ReportLevel => \"warning\"),\n Match.when(DiagnosticSeverity.Information, (): ReportLevel => \"information\"),\n Match.orElse((): ReportLevel => \"hint\"),\n )\n\n/** A file's diagnostic as a report, counting from 1 where the LSP counts from 0. */\nconst reportOf =\n (file: PageFile) =>\n ({ range, severity, message }: Diagnostic): Report => ({\n path: file.path,\n line: range.start.line + 1,\n column: range.start.character + 1,\n level: levelOf(severity),\n // The LSP lets a message be markup; a report is text either way.\n message: Predicate.isString(message) ? message : message.value,\n })\n\n/**\n * Every link to a missing page under `roots`, file by file in the order they were read.\n * The judgement is the editor's own, so what an editor flags is what is reported here.\n */\nexport const checkSite = ({\n roots,\n unresolvedLinks,\n parseOptions = {},\n frontmatter = true,\n}: CheckSiteOptions): Effect.Effect<ReadonlyArray<Report>> =>\n Effect.map(readPageFiles(roots, { frontmatter }), (files) => {\n const index = indexOf(files)\n return Arr.flatMap(files, (file) =>\n Arr.map(\n unresolvedLinkDiagnostics(index, file.text, {\n severity: unresolvedLinks,\n components: file.path.endsWith(\".csnx\"),\n parseOptions,\n frontmatter,\n }),\n reportOf(file),\n ),\n )\n })\n\n/** A report as a line of output: `path:line:column level message`, `path` from `cwd`. */\nconst lineOf =\n (cwd: string) =>\n ({ path, line, column, level, message }: Report): string =>\n `${relative(cwd, path).split(sep).join(\"/\")}:${line}:${column} ${level} ${message}\\n`\n\n/** Runs a check as `args` asks, from `cwd`. Never fails: a bad option is exit code 2. */\nexport const runCheck = (args: ReadonlyArray<string>, cwd: string): Effect.Effect<CheckResult> =>\n Option.match(argsOf(args, cwd), {\n onNone: () => Effect.succeed({ output: USAGE, exitCode: 2 }),\n onSome: (options) =>\n Effect.map(checkSite(options), (reports) => ({\n output: Arr.map(reports, lineOf(cwd)).join(\"\"),\n exitCode: Arr.some(reports, ({ level }) => level === \"error\") ? 1 : 0,\n })),\n })\n"],"mappings":";;;;;;;;;;;AAiBA,MAAa,QAAQ;;;;;;;;;;;;;;AA4CrB,MAAM,gBAAoC;AAE1C,MAAM,UAAU,MAA6B,QAC3C,KACE,OAAO,cAAc,SAAS,CAAC,CAAC;CAC9B,MAAM,CAAC,GAAG,IAAI;CACd,kBAAkB;CAClB,QAAQ;CACR,SAAS;EACP,oBAAoB,EAAE,MAAM,SAAS;EACrC,aAAa,EAAE,MAAM,SAAS;EAC9B,kBAAkB,EAAE,MAAM,UAAU;CACtC;AACF,CAAC,GACD,OAAO,KAAK,EAAE,QAAQ,mBAAmB;CACvC,OAAOA,QAAI,MAAM,aAAa;EAC5B,eAAe,CAAC,GAAG;EACnB,aAAa,gBAAgBA,QAAI,IAAI,cAAc,cAAc,QAAQ,KAAK,SAAS,CAAC;CAC1F,CAAC;CACD,iBAAiB,KACf,OAAO,aAAa,OAAO,mBAAmB,GAC9C,OAAO,MAAM;EAAE,cAAc;EAAe,QAAQ;CAAW,CAAC,CAClE;CAEA,cAAc,eAAe;EAC3B,GAAG;EACH,aAAa,MAAM,KAAK,OAAO,eAAe,EAAE;CAClD,CAAC;CACD,aAAa,OAAO,sBAAsB;AAC5C,EAAE,CACJ;AAEF,MAAM,WAAW,aACf,MAAM,MAAM,QAAQ,CAAC,CAAC,KACpB,MAAM,KAAK,mBAAmB,aAA0B,OAAO,GAC/D,MAAM,KAAK,mBAAmB,eAA4B,SAAS,GACnE,MAAM,KAAK,mBAAmB,mBAAgC,aAAa,GAC3E,MAAM,aAA0B,MAAM,CACxC;;AAGF,MAAM,YACH,UACA,EAAE,OAAO,UAAU,eAAmC;CACrD,MAAM,KAAK;CACX,MAAM,MAAM,MAAM,OAAO;CACzB,QAAQ,MAAM,MAAM,YAAY;CAChC,OAAO,QAAQ,QAAQ;CAEvB,SAAS,UAAU,SAAS,OAAO,IAAI,UAAU,QAAQ;AAC3D;;;;;AAMF,MAAa,aAAa,EACxB,OACA,iBACA,eAAe,CAAC,GAChB,cAAc,WAEd,OAAO,IAAI,cAAc,OAAO,EAAE,YAAY,CAAC,IAAI,UAAU;CAC3D,MAAM,QAAQ,QAAQ,KAAK;CAC3B,OAAOA,QAAI,QAAQ,QAAQ,SACzBA,QAAI,IACF,0BAA0B,OAAO,KAAK,MAAM;EAC1C,UAAU;EACV,YAAY,KAAK,KAAK,SAAS,OAAO;EACtC;EACA;CACF,CAAC,GACD,SAAS,IAAI,CACf,CACF;AACF,CAAC;;AAGH,MAAM,UACH,SACA,EAAE,MAAM,MAAM,QAAQ,OAAO,cAC5B,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ;;AAGtF,MAAa,YAAY,MAA6B,QACpD,OAAO,MAAM,OAAO,MAAM,GAAG,GAAG;CAC9B,cAAc,OAAO,QAAQ;EAAE,QAAQ;EAAO,UAAU;CAAE,CAAC;CAC3D,SAAS,YACP,OAAO,IAAI,UAAU,OAAO,IAAI,aAAa;EAC3C,QAAQA,QAAI,IAAI,SAAS,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;EAC7C,UAAUA,QAAI,KAAK,UAAU,EAAE,YAAY,UAAU,OAAO,IAAI,IAAI;CACtE,EAAE;AACN,CAAC"}
1
+ {"version":3,"file":"check.mjs","names":["Arr"],"sources":["../src/check.ts"],"sourcesContent":["import { relative, resolve, sep } from \"node:path\"\nimport { parseArgs } from \"node:util\"\n\nimport type { ParseOptions } from \"@cosense-toolbox/parser\"\nimport { Array as Arr, Effect, Match, Option, pipe, Predicate } from \"effect\"\nimport { type Diagnostic, DiagnosticSeverity } from \"vscode-languageserver/node\"\n\nimport { severityOf, unresolvedLinkDiagnostics, type UnresolvedSeverity } from \"./diagnostics\"\nimport { indexOf, type PageFile, readPageFiles } from \"./workspace\"\n\n/**\n * `csn-lsp check`: the server's diagnostics for every page of a site, for a terminal or CI.\n *\n * The same functions decide as in the editor, so what the editor flags is what fails here.\n */\n\nexport const USAGE = `Usage: csn-lsp check [directory ...] [options]\n\nReads the .csn and .csnx pages under each directory (default: the current one) and reports\nevery [link] to a page none of them holds.\n\nOptions:\n --unresolved-links <level> off | hint | information | warning | error (default: error)\n --no-frontmatter read a first line of --- as the title, not as YAML\n`\n\n/** What the command prints, and the code it exits with. */\nexport interface CheckResult {\n readonly output: string\n readonly exitCode: number\n}\n\n/** Where to look and how to read, for `checkSite`. */\nexport interface CheckSiteOptions {\n /** The directories whose `.csn` / `.csnx` pages are read, and checked against each other. */\n readonly roots: ReadonlyArray<string>\n /** How a link to a missing page is reported; `off` reports none. */\n readonly unresolvedLinks: UnresolvedSeverity\n /** How to parse: a site's notation extensions, as its build parses with. */\n readonly parseOptions?: ParseOptions\n /** Whether a `---` fence on a page's first line opens YAML to skip (default: true). */\n readonly frontmatter?: boolean\n}\n\nexport type ReportLevel = \"error\" | \"warning\" | \"information\" | \"hint\"\n\n/** One thing found: where, counting lines and columns from 1, how loudly, and what. */\nexport interface Report {\n readonly path: string\n readonly line: number\n readonly column: number\n readonly level: ReportLevel\n readonly message: string\n}\n\n/**\n * Reports are errors unless told otherwise: the command exists to stop a build, and a\n * warning that passes would not.\n */\nconst DEFAULT_LEVEL: UnresolvedSeverity = \"error\"\n\nconst argsOf = (args: ReadonlyArray<string>, cwd: string): Option.Option<CheckSiteOptions> =>\n pipe(\n Option.liftThrowable(parseArgs)({\n args: [...args],\n allowPositionals: true,\n strict: true,\n options: {\n \"unresolved-links\": { type: \"string\" },\n \"no-frontmatter\": { type: \"boolean\" },\n },\n }),\n Option.map(({ values, positionals }) => ({\n roots: Arr.match(positionals, {\n onEmpty: () => [cwd],\n onNonEmpty: (directories) => Arr.map(directories, (directory) => resolve(cwd, directory)),\n }),\n unresolvedLinks: pipe(\n Option.fromNullable(values[\"unresolved-links\"]),\n Option.match({ onNone: () => DEFAULT_LEVEL, onSome: severityOf }),\n ),\n frontmatter: values[\"no-frontmatter\"] !== true,\n })),\n )\n\nconst levelOf = (severity: DiagnosticSeverity | undefined): ReportLevel =>\n Match.value(severity).pipe(\n Match.when(DiagnosticSeverity.Error, (): ReportLevel => \"error\"),\n Match.when(DiagnosticSeverity.Warning, (): ReportLevel => \"warning\"),\n Match.when(DiagnosticSeverity.Information, (): ReportLevel => \"information\"),\n Match.orElse((): ReportLevel => \"hint\"),\n )\n\n/** A file's diagnostic as a report, counting from 1 where the LSP counts from 0. */\nconst reportOf =\n (file: PageFile) =>\n ({ range, severity, message }: Diagnostic): Report => ({\n path: file.path,\n line: range.start.line + 1,\n column: range.start.character + 1,\n level: levelOf(severity),\n // The LSP lets a message be markup; a report is text either way.\n message: Predicate.isString(message) ? message : message.value,\n })\n\n/**\n * Every link to a missing page under `roots`, file by file in the order they were read.\n * The judgement is the editor's own, so what an editor flags is what is reported here.\n */\nexport const checkSite = ({\n roots,\n unresolvedLinks,\n parseOptions = {},\n frontmatter = true,\n}: CheckSiteOptions): Effect.Effect<ReadonlyArray<Report>> =>\n Effect.map(readPageFiles(roots, { frontmatter }), (files) => {\n const index = indexOf(files)\n return Arr.flatMap(files, (file) =>\n Arr.map(\n unresolvedLinkDiagnostics(index, file.text, {\n severity: unresolvedLinks,\n components: file.path.endsWith(\".csnx\"),\n parseOptions,\n frontmatter,\n }),\n reportOf(file),\n ),\n )\n })\n\n/** A report as a line of output: `path:line:column level message`, `path` from `cwd`. */\nconst lineOf =\n (cwd: string) =>\n ({ path, line, column, level, message }: Report): string =>\n `${relative(cwd, path).split(sep).join(\"/\")}:${line}:${column} ${level} ${message}\\n`\n\n/** Runs a check as `args` asks, from `cwd`. Never fails: a bad option is exit code 2. */\nexport const runCheck = (args: ReadonlyArray<string>, cwd: string): Effect.Effect<CheckResult> =>\n Option.match(argsOf(args, cwd), {\n onNone: () => Effect.succeed({ output: USAGE, exitCode: 2 }),\n onSome: (options) =>\n Effect.map(checkSite(options), (reports) => ({\n output: Arr.map(reports, lineOf(cwd)).join(\"\"),\n exitCode: Arr.some(reports, ({ level }) => level === \"error\") ? 1 : 0,\n })),\n })\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,QAAQ;;;;;;;;;;;;;AA2CrB,MAAM,gBAAoC;AAE1C,MAAM,UAAU,MAA6B,QAC3C,KACE,OAAO,cAAc,SAAS,CAAC,CAAC;CAC9B,MAAM,CAAC,GAAG,IAAI;CACd,kBAAkB;CAClB,QAAQ;CACR,SAAS;EACP,oBAAoB,EAAE,MAAM,SAAS;EACrC,kBAAkB,EAAE,MAAM,UAAU;CACtC;AACF,CAAC,GACD,OAAO,KAAK,EAAE,QAAQ,mBAAmB;CACvC,OAAOA,MAAI,MAAM,aAAa;EAC5B,eAAe,CAAC,GAAG;EACnB,aAAa,gBAAgBA,MAAI,IAAI,cAAc,cAAc,QAAQ,KAAK,SAAS,CAAC;CAC1F,CAAC;CACD,iBAAiB,KACf,OAAO,aAAa,OAAO,mBAAmB,GAC9C,OAAO,MAAM;EAAE,cAAc;EAAe,QAAQ;CAAW,CAAC,CAClE;CACA,aAAa,OAAO,sBAAsB;AAC5C,EAAE,CACJ;AAEF,MAAM,WAAW,aACf,MAAM,MAAM,QAAQ,CAAC,CAAC,KACpB,MAAM,KAAK,mBAAmB,aAA0B,OAAO,GAC/D,MAAM,KAAK,mBAAmB,eAA4B,SAAS,GACnE,MAAM,KAAK,mBAAmB,mBAAgC,aAAa,GAC3E,MAAM,aAA0B,MAAM,CACxC;;AAGF,MAAM,YACH,UACA,EAAE,OAAO,UAAU,eAAmC;CACrD,MAAM,KAAK;CACX,MAAM,MAAM,MAAM,OAAO;CACzB,QAAQ,MAAM,MAAM,YAAY;CAChC,OAAO,QAAQ,QAAQ;CAEvB,SAAS,UAAU,SAAS,OAAO,IAAI,UAAU,QAAQ;AAC3D;;;;;AAMF,MAAa,aAAa,EACxB,OACA,iBACA,eAAe,CAAC,GAChB,cAAc,WAEd,OAAO,IAAI,cAAc,OAAO,EAAE,YAAY,CAAC,IAAI,UAAU;CAC3D,MAAM,QAAQ,QAAQ,KAAK;CAC3B,OAAOA,MAAI,QAAQ,QAAQ,SACzBA,MAAI,IACF,0BAA0B,OAAO,KAAK,MAAM;EAC1C,UAAU;EACV,YAAY,KAAK,KAAK,SAAS,OAAO;EACtC;EACA;CACF,CAAC,GACD,SAAS,IAAI,CACf,CACF;AACF,CAAC;;AAGH,MAAM,UACH,SACA,EAAE,MAAM,MAAM,QAAQ,OAAO,cAC5B,GAAG,SAAS,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ;;AAGtF,MAAa,YAAY,MAA6B,QACpD,OAAO,MAAM,OAAO,MAAM,GAAG,GAAG;CAC9B,cAAc,OAAO,QAAQ;EAAE,QAAQ;EAAO,UAAU;CAAE,CAAC;CAC3D,SAAS,YACP,OAAO,IAAI,UAAU,OAAO,IAAI,aAAa;EAC3C,QAAQA,MAAI,IAAI,SAAS,OAAO,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;EAC7C,UAAUA,MAAI,KAAK,UAAU,EAAE,YAAY,UAAU,OAAO,IAAI,IAAI;CACtE,EAAE;AACN,CAAC"}
@@ -10,7 +10,7 @@ import { CompletionItemKind } from "vscode-languageserver/node";
10
10
  const OTHER_NOTATIONS = [
11
11
  /^\$/,
12
12
  /^\//,
13
- /^[!"#%&'()*+,\-./{|}<>_~]+(?:\s|$)/,
13
+ /^[!"#%&'()*+,\-./{|}<>_~=]+(?:\s|$)/,
14
14
  /^.+\.icon(?:\*\d+)?$/,
15
15
  /https?:\/\//i,
16
16
  /\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i
@@ -1 +1 @@
1
- {"version":3,"file":"completion.mjs","names":["Arr"],"sources":["../src/completion.ts"],"sourcesContent":["import { normalizeLineEndings, parse, type ParseOptions } from \"@cosense-toolbox/parser\"\nimport { collect } from \"@cosense-toolbox/parser/utils\"\nimport { Array as Arr, Match, Option, pipe } from \"effect\"\nimport {\n type CompletionItem,\n CompletionItemKind,\n type Definition,\n type Position,\n} from \"vscode-languageserver/node\"\n\nimport type { Index, Page } from \"./workspace\"\n\n/**\n * Where a completion applies, decided from the text alone.\n *\n * Cosense's own editor closes a bracket the moment it is typed, so a link is completed\n * inside a *closed* pair rather than after an opening one. Everything here is pure: the\n * candidates are somebody else's problem (see workspace.ts).\n */\nexport interface CompletionDetection {\n readonly kind: \"link\" | \"hashtag\"\n readonly query: string\n readonly replaceStart: number\n readonly replaceEnd: number\n /** `replaceStart` up to the cursor — what the client sees as already typed. */\n readonly typedText: string\n}\n\n// --- Which bracket is a link --------------------------------------------------------------\n\n/**\n * A marker run followed by whitespace, or by nothing at all: the parser waits for a body,\n * but a menu that opens between `[* ` and its text is the flicker this rule exists to stop.\n * The markers are every character Cosense accepts in a decoration, from help-jp/文字装飾記法.\n */\nconst STARTS_DECORATION = /^[!\"#%&'()*+,\\-./{|}<>_~]+(?:\\s|$)/\n\n/**\n * The notations a bracket can hold other than a page link: a formula, a project link, a\n * decoration, an icon, a URL, an image path.\n */\nconst OTHER_NOTATIONS: ReadonlyArray<RegExp> = [\n /^\\$/,\n /^\\//,\n STARTS_DECORATION,\n /^.+\\.icon(?:\\*\\d+)?$/,\n /https?:\\/\\//i,\n /\\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i,\n]\n\n/**\n * True when the bracket holds a page link, or is on its way to one — the only bracket\n * completion answers in.\n *\n * The other notations (`[* 見出し]`, `[$ x^2]`, `[taro.icon]`, `[ラベル https://…]`,\n * `[a.png]`, `[/my-project/page]`) are left alone, because accepting a candidate replaces\n * the whole bracket and would take the notation with it.\n */\nexport const isLinkBracket = (inner: string): boolean =>\n inner === \"\" || !Arr.some(OTHER_NOTATIONS, (notation) => notation.test(inner))\n\n// --- Where the cursor is ------------------------------------------------------------------\n\nconst isBracket = (char: string): boolean => char === \"[\" || char === \"]\"\n\n/**\n * The `[` that opens the bracket around `cursor`: the nearest bracket to its left, if that\n * is an opening one. A `[` that is the second half of `[[` opens bold or a large image, not\n * a link.\n */\nconst openingBefore = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(0, cursor).split(\"\"),\n Arr.findLastIndex(isBracket),\n Option.filter((open) => line[open] === \"[\" && line[open - 1] !== \"[\"),\n )\n\n/** The `]` that closes it: the nearest bracket to the right of `cursor`, if a closing one. */\nconst closingAfter = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(cursor).split(\"\"),\n Arr.findFirstIndex(isBracket),\n Option.map((offset) => cursor + offset),\n Option.filter((close) => line[close] === \"]\"),\n )\n\n/** Only inside a *closed* pair, matching Cosense. */\nconst detectLink = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.all([openingBefore(line, cursor), closingAfter(line, cursor)]),\n // The whole bracket content, not just up to the cursor: Cosense treats the link text\n // as one unit, so the candidates are the same wherever the cursor sits.\n Option.map(([open, close]) => ({ open, close, inner: line.slice(open + 1, close) })),\n Option.filter(({ inner }) => isLinkBracket(inner)),\n Option.map(({ open, close, inner }) => ({\n kind: \"link\",\n query: inner,\n replaceStart: open,\n replaceEnd: close + 1,\n typedText: line.slice(open, cursor),\n })),\n )\n\n/**\n * A `#` at a tag boundary (the line's start, or after whitespace) and the tag name typed\n * after it up to the cursor, mirroring the parser's hashtag construct.\n */\nconst TAG_BEFORE_CURSOR = /(?:^|(?<=\\s))#([^\\s[\\]#]*)$/\n\nconst detectHashtag = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.fromNullable(TAG_BEFORE_CURSOR.exec(line.slice(0, cursor))),\n Option.map(({ index, 1: query = \"\" }) => ({\n kind: \"hashtag\",\n query,\n replaceStart: index,\n replaceEnd: cursor,\n typedText: line.slice(index, cursor),\n })),\n )\n\n/** Pure line/cursor -> completion trigger detection. No AST, no I/O. */\nexport const detectCompletion = (\n line: string,\n cursor: number,\n): Option.Option<CompletionDetection> =>\n pipe(\n detectLink(line, cursor),\n Option.orElse(() => detectHashtag(line, cursor)),\n )\n\n// --- In a document ------------------------------------------------------------------------\n\n/**\n * Inside code, a bracket is not notation. The cursor must be strictly between the\n * backticks (not touching them) to count as inside an inline span.\n */\nconst isInCode = (\n text: string,\n { line, character }: Position,\n parseOptions: ParseOptions,\n): boolean => {\n const page = parse(text, parseOptions)\n return (\n Arr.some(\n collect(page, \"codeBlock\"),\n ({ position }) => line >= position.start.line && line <= position.end.line,\n ) ||\n Arr.some(\n collect(page, \"inlineCode\"),\n ({ position }) =>\n line === position.start.line &&\n character > position.start.column &&\n character < position.end.column,\n )\n )\n}\n\n/**\n * `detectCompletion` for one line of a document, suppressed inside code. `parseOptions` are\n * the site's notation extensions, parsed with as the site's build parses.\n */\nexport const detectCompletionInDocument = (\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<CompletionDetection> =>\n pipe(\n Option.liftPredicate(text, (document) => !isInCode(document, position, parseOptions)),\n Option.map((document) => normalizeLineEndings(document).split(\"\\n\")[position.line] ?? \"\"),\n Option.flatMap((line) => detectCompletion(line, position.character)),\n )\n\n// --- Matching titles ----------------------------------------------------------------------\n\n/**\n * Title matching, so `[Side Kanban]` and `[side_kanban]` count as one page the way Cosense\n * links them: case-insensitive, space and `_` alike, and NFKC so width variants match too.\n */\nexport const normalizeForMatch = (s: string): string =>\n s.normalize(\"NFKC\").toLowerCase().replace(/[_ ]/g, \" \")\n\n/** The title as a tag writes it: Cosense folds a space to `_`. */\nexport const asTagName = (title: string): string => title.replace(/ /g, \"_\")\n\n/** A title that cannot be written as a tag at all is not offered as one. */\nexport const isTaggable = (title: string): boolean => !/[\\s[\\]#]/.test(asTagName(title))\n\n// --- Answers ------------------------------------------------------------------------------\n\n/** The last part of a `/`-separated location. */\nconst fileNameOf = (location: string): string =>\n Option.getOrElse(Arr.last(location.split(\"/\")), () => location)\n\n/**\n * A page as a candidate for `detection`, or None when it does not fit: a tag cannot hold a\n * space or a bracket, so a page whose title does is not offered after `#` (accepting it\n * would write something that reads as two tags).\n */\nconst candidate =\n (detection: CompletionDetection, line: number) =>\n (page: Page): Option.Option<CompletionItem> => {\n const query = normalizeForMatch(detection.query)\n const normalized = normalizeForMatch(page.title)\n const notation = Match.value(detection.kind).pipe(\n Match.when(\"hashtag\", () => `#${asTagName(page.title)}`),\n Match.when(\"link\", () => `[${page.title}]`),\n Match.exhaustive,\n )\n return pipe(\n Option.some(page),\n Option.filter(() => detection.kind === \"link\" || isTaggable(page.title)),\n Option.filter(() => query === \"\" || normalized.includes(query)),\n Option.map((): CompletionItem => ({\n label: page.title,\n kind: CompletionItemKind.File,\n // The file name alone: a whole path is cut off in the menu before the name shows.\n detail: fileNameOf(page.location),\n // The whole path, where the editor shows more room once the item is selected.\n documentation: page.location,\n // The whole notation is replaced, brackets included: the reader typed the `[`,\n // and leaving it in place would give `[[title]]`.\n textEdit: {\n range: {\n start: { line, character: detection.replaceStart },\n end: { line, character: detection.replaceEnd },\n },\n newText: notation,\n },\n // A client filters by the text from the edit's start to the cursor, `[設` here, so\n // it has to find that in the notation rather than in the bare title.\n filterText: notation,\n sortText: `${normalized} ${page.location}`,\n })),\n )\n }\n\n/** The pages that complete what is typed at `position`, or none outside a link or a tag. */\nexport const completionItems = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): CompletionItem[] =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.match({\n onNone: () => [],\n onSome: (detection) => Arr.filterMap(index.pages, candidate(detection, position.line)),\n }),\n )\n\n/** The file of the page named at `position`, opened at its top. None for a missing page. */\nexport const definitionOf = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<Definition> =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.map(({ query }) => normalizeForMatch(query)),\n Option.flatMap((key) =>\n Arr.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key),\n ),\n Option.map(({ uri }) => {\n const start = { line: 0, character: 0 }\n return { uri, range: { start, end: start } }\n }),\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"completion.mjs","names":["Arr"],"sources":["../src/completion.ts"],"sourcesContent":["import { normalizeLineEndings, parse, type ParseOptions } from \"@cosense-toolbox/parser\"\nimport { collect } from \"@cosense-toolbox/parser/utils\"\nimport { Array as Arr, Match, Option, pipe } from \"effect\"\nimport {\n type CompletionItem,\n CompletionItemKind,\n type Definition,\n type Position,\n} from \"vscode-languageserver/node\"\n\nimport type { Index, Page } from \"./workspace\"\n\n/**\n * Where a completion applies, decided from the text alone.\n *\n * Cosense's own editor closes a bracket the moment it is typed, so a link is completed\n * inside a *closed* pair rather than after an opening one. Everything here is pure: the\n * candidates are somebody else's problem (see workspace.ts).\n */\nexport interface CompletionDetection {\n readonly kind: \"link\" | \"hashtag\"\n readonly query: string\n readonly replaceStart: number\n readonly replaceEnd: number\n /** `replaceStart` up to the cursor — what the client sees as already typed. */\n readonly typedText: string\n}\n\n// --- Which bracket is a link --------------------------------------------------------------\n\n/**\n * A marker run followed by whitespace, or by nothing at all: the parser waits for a body,\n * but a menu that opens between `[* ` and its text is the flicker this rule exists to stop.\n * The markers are every character Cosense accepts in a decoration, from help-jp/文字装飾記法.\n */\nconst STARTS_DECORATION = /^[!\"#%&'()*+,\\-./{|}<>_~=]+(?:\\s|$)/\n\n/**\n * The notations a bracket can hold other than a page link: a formula, a project link, a\n * decoration, an icon, a URL, an image path.\n */\nconst OTHER_NOTATIONS: ReadonlyArray<RegExp> = [\n /^\\$/,\n /^\\//,\n STARTS_DECORATION,\n /^.+\\.icon(?:\\*\\d+)?$/,\n /https?:\\/\\//i,\n /\\.(png|jpe?g|gif|webp|svg|bmp|avif)$/i,\n]\n\n/**\n * True when the bracket holds a page link, or is on its way to one — the only bracket\n * completion answers in.\n *\n * The other notations (`[* 見出し]`, `[$ x^2]`, `[taro.icon]`, `[ラベル https://…]`,\n * `[a.png]`, `[/my-project/page]`) are left alone, because accepting a candidate replaces\n * the whole bracket and would take the notation with it.\n */\nexport const isLinkBracket = (inner: string): boolean =>\n inner === \"\" || !Arr.some(OTHER_NOTATIONS, (notation) => notation.test(inner))\n\n// --- Where the cursor is ------------------------------------------------------------------\n\nconst isBracket = (char: string): boolean => char === \"[\" || char === \"]\"\n\n/**\n * The `[` that opens the bracket around `cursor`: the nearest bracket to its left, if that\n * is an opening one. A `[` that is the second half of `[[` opens bold or a large image, not\n * a link.\n */\nconst openingBefore = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(0, cursor).split(\"\"),\n Arr.findLastIndex(isBracket),\n Option.filter((open) => line[open] === \"[\" && line[open - 1] !== \"[\"),\n )\n\n/** The `]` that closes it: the nearest bracket to the right of `cursor`, if a closing one. */\nconst closingAfter = (line: string, cursor: number): Option.Option<number> =>\n pipe(\n line.slice(cursor).split(\"\"),\n Arr.findFirstIndex(isBracket),\n Option.map((offset) => cursor + offset),\n Option.filter((close) => line[close] === \"]\"),\n )\n\n/** Only inside a *closed* pair, matching Cosense. */\nconst detectLink = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.all([openingBefore(line, cursor), closingAfter(line, cursor)]),\n // The whole bracket content, not just up to the cursor: Cosense treats the link text\n // as one unit, so the candidates are the same wherever the cursor sits.\n Option.map(([open, close]) => ({ open, close, inner: line.slice(open + 1, close) })),\n Option.filter(({ inner }) => isLinkBracket(inner)),\n Option.map(({ open, close, inner }) => ({\n kind: \"link\",\n query: inner,\n replaceStart: open,\n replaceEnd: close + 1,\n typedText: line.slice(open, cursor),\n })),\n )\n\n/**\n * A `#` at a tag boundary (the line's start, or after whitespace) and the tag name typed\n * after it up to the cursor, mirroring the parser's hashtag construct.\n */\nconst TAG_BEFORE_CURSOR = /(?:^|(?<=\\s))#([^\\s[\\]#]*)$/\n\nconst detectHashtag = (line: string, cursor: number): Option.Option<CompletionDetection> =>\n pipe(\n Option.fromNullable(TAG_BEFORE_CURSOR.exec(line.slice(0, cursor))),\n Option.map(({ index, 1: query = \"\" }) => ({\n kind: \"hashtag\",\n query,\n replaceStart: index,\n replaceEnd: cursor,\n typedText: line.slice(index, cursor),\n })),\n )\n\n/** Pure line/cursor -> completion trigger detection. No AST, no I/O. */\nexport const detectCompletion = (\n line: string,\n cursor: number,\n): Option.Option<CompletionDetection> =>\n pipe(\n detectLink(line, cursor),\n Option.orElse(() => detectHashtag(line, cursor)),\n )\n\n// --- In a document ------------------------------------------------------------------------\n\n/**\n * Inside code, a bracket is not notation. The cursor must be strictly between the\n * backticks (not touching them) to count as inside an inline span.\n */\nconst isInCode = (\n text: string,\n { line, character }: Position,\n parseOptions: ParseOptions,\n): boolean => {\n const page = parse(text, parseOptions)\n return (\n Arr.some(\n collect(page, \"codeBlock\"),\n ({ position }) => line >= position.start.line && line <= position.end.line,\n ) ||\n Arr.some(\n collect(page, \"inlineCode\"),\n ({ position }) =>\n line === position.start.line &&\n character > position.start.column &&\n character < position.end.column,\n )\n )\n}\n\n/**\n * `detectCompletion` for one line of a document, suppressed inside code. `parseOptions` are\n * the site's notation extensions, parsed with as the site's build parses.\n */\nexport const detectCompletionInDocument = (\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<CompletionDetection> =>\n pipe(\n Option.liftPredicate(text, (document) => !isInCode(document, position, parseOptions)),\n Option.map((document) => normalizeLineEndings(document).split(\"\\n\")[position.line] ?? \"\"),\n Option.flatMap((line) => detectCompletion(line, position.character)),\n )\n\n// --- Matching titles ----------------------------------------------------------------------\n\n/**\n * Title matching, so `[Side Kanban]` and `[side_kanban]` count as one page the way Cosense\n * links them: case-insensitive, space and `_` alike, and NFKC so width variants match too.\n */\nexport const normalizeForMatch = (s: string): string =>\n s.normalize(\"NFKC\").toLowerCase().replace(/[_ ]/g, \" \")\n\n/** The title as a tag writes it: Cosense folds a space to `_`. */\nexport const asTagName = (title: string): string => title.replace(/ /g, \"_\")\n\n/** A title that cannot be written as a tag at all is not offered as one. */\nexport const isTaggable = (title: string): boolean => !/[\\s[\\]#]/.test(asTagName(title))\n\n// --- Answers ------------------------------------------------------------------------------\n\n/** The last part of a `/`-separated location. */\nconst fileNameOf = (location: string): string =>\n Option.getOrElse(Arr.last(location.split(\"/\")), () => location)\n\n/**\n * A page as a candidate for `detection`, or None when it does not fit: a tag cannot hold a\n * space or a bracket, so a page whose title does is not offered after `#` (accepting it\n * would write something that reads as two tags).\n */\nconst candidate =\n (detection: CompletionDetection, line: number) =>\n (page: Page): Option.Option<CompletionItem> => {\n const query = normalizeForMatch(detection.query)\n const normalized = normalizeForMatch(page.title)\n const notation = Match.value(detection.kind).pipe(\n Match.when(\"hashtag\", () => `#${asTagName(page.title)}`),\n Match.when(\"link\", () => `[${page.title}]`),\n Match.exhaustive,\n )\n return pipe(\n Option.some(page),\n Option.filter(() => detection.kind === \"link\" || isTaggable(page.title)),\n Option.filter(() => query === \"\" || normalized.includes(query)),\n Option.map((): CompletionItem => ({\n label: page.title,\n kind: CompletionItemKind.File,\n // The file name alone: a whole path is cut off in the menu before the name shows.\n detail: fileNameOf(page.location),\n // The whole path, where the editor shows more room once the item is selected.\n documentation: page.location,\n // The whole notation is replaced, brackets included: the reader typed the `[`,\n // and leaving it in place would give `[[title]]`.\n textEdit: {\n range: {\n start: { line, character: detection.replaceStart },\n end: { line, character: detection.replaceEnd },\n },\n newText: notation,\n },\n // A client filters by the text from the edit's start to the cursor, `[設` here, so\n // it has to find that in the notation rather than in the bare title.\n filterText: notation,\n sortText: `${normalized} ${page.location}`,\n })),\n )\n }\n\n/** The pages that complete what is typed at `position`, or none outside a link or a tag. */\nexport const completionItems = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): CompletionItem[] =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.match({\n onNone: () => [],\n onSome: (detection) => Arr.filterMap(index.pages, candidate(detection, position.line)),\n }),\n )\n\n/** The file of the page named at `position`, opened at its top. None for a missing page. */\nexport const definitionOf = (\n index: Index,\n text: string,\n position: Position,\n parseOptions: ParseOptions = {},\n): Option.Option<Definition> =>\n pipe(\n detectCompletionInDocument(text, position, parseOptions),\n Option.map(({ query }) => normalizeForMatch(query)),\n Option.flatMap((key) =>\n Arr.findFirst(index.pages, (page) => normalizeForMatch(page.title) === key),\n ),\n Option.map(({ uri }) => {\n const start = { line: 0, character: 0 }\n return { uri, range: { start, end: start } }\n }),\n )\n"],"mappings":"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"}
package/dist/main.mjs CHANGED
@@ -13,7 +13,7 @@ const check = pipe(Effect.promise(() => import("./check.mjs")), Effect.flatMap((
13
13
  (exitCode === 2 ? process.stderr : process.stdout).write(output);
14
14
  process.exitCode = exitCode;
15
15
  })));
16
- const serve = Effect.promise(() => import("./server-DPPxtJ6Y.mjs"));
16
+ const serve = Effect.promise(() => import("./server-DWgrivkG.mjs"));
17
17
  Effect.runFork(Match.value(command).pipe(Match.when("check", () => Effect.asVoid(check)), Match.orElse(() => Effect.asVoid(serve))));
18
18
  //#endregion
19
19
  export {};
@@ -1,9 +1,40 @@
1
- import { a as encodeTokens, i as computeTokens, n as LEGEND } from "./tokens-D0NP_uLo.mjs";
1
+ import { a as encodeTokens, i as computeTokens, n as LEGEND } from "./tokens-pb6vW18i.mjs";
2
2
  import { completionItems, definitionOf } from "./completion.mjs";
3
- import { a as rootsOf, c as settingsOf, o as defaultSettings, r as readIndex, s as parseOptionsOf, t as emptyIndex, u as unresolvedLinkDiagnostics } from "./workspace-DouAOZnK.mjs";
4
- import { Array, Effect, Option, Ref, pipe } from "effect";
3
+ import { a as rootsOf, o as severityOf, r as readIndex, s as unresolvedLinkDiagnostics, t as emptyIndex } from "./workspace-XJqmAxae.mjs";
4
+ import { Array, Effect, Option, Record, Ref, Schema, pipe } from "effect";
5
5
  import { ProposedFeatures, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
6
6
  import { TextDocument } from "vscode-languageserver-textdocument";
7
+ //#region src/settings.ts
8
+ const defaultSettings = {
9
+ sources: [],
10
+ unresolvedLinks: "warning",
11
+ frontmatter: true
12
+ };
13
+ /** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */
14
+ const decodeOr = (schema, fallback) => (value) => Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback);
15
+ /** The options as an object, which the editor may send in any shape or not at all. */
16
+ const OptionsObject = Schema.Record({
17
+ key: Schema.String,
18
+ value: Schema.Unknown
19
+ });
20
+ /**
21
+ * The non-empty strings of a list. Anything else in it is dropped rather than failing the
22
+ * whole list, and anything but a list counts as none.
23
+ */
24
+ const stringsOf = (value) => Array.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString));
25
+ /**
26
+ * Each setting read on its own, so one of the wrong shape falls back to its default
27
+ * without taking the others with it.
28
+ */
29
+ const settingsOf = (options) => {
30
+ const field = (key) => pipe(Schema.decodeUnknownOption(OptionsObject)(options), Option.flatMap(Record.get(key)), Option.getOrUndefined);
31
+ return {
32
+ sources: stringsOf(field("sources")),
33
+ unresolvedLinks: severityOf(field("unresolvedLinks")),
34
+ frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field("frontmatter"))
35
+ };
36
+ };
37
+ //#endregion
7
38
  //#region src/server.ts
8
39
  const connection = createConnection(ProposedFeatures.all);
9
40
  const documents = new TextDocuments(TextDocument);
@@ -17,11 +48,8 @@ const readsComponents = (document) => Array.contains(CSNX_LANGUAGE_IDS, document
17
48
  /** What the reader set in the editor's initialization options (see settings.ts). */
18
49
  const settings = Ref.unsafeMake(defaultSettings);
19
50
  const currentSettings = () => Effect.runSync(Ref.get(settings));
20
- /** How to parse, so notation reads as the site's build reads it. */
21
- const currentParseOptions = () => parseOptionsOf(currentSettings());
22
51
  const semanticTokensOf = (document) => ({ data: encodeTokens(computeTokens(document.getText(), {
23
52
  components: readsComponents(document),
24
- parseOptions: currentParseOptions(),
25
53
  frontmatter: currentSettings().frontmatter
26
54
  })) });
27
55
  /**
@@ -49,7 +77,6 @@ const publishDiagnostics = (document) => pipe(Effect.all([Ref.get(index), Ref.ge
49
77
  diagnostics: unresolvedLinkDiagnostics(pages, document.getText(), {
50
78
  severity: set.unresolvedLinks,
51
79
  components: readsComponents(document),
52
- parseOptions: parseOptionsOf(set),
53
80
  frontmatter: set.frontmatter
54
81
  })
55
82
  }))
@@ -89,12 +116,12 @@ documents.onDidClose(({ document }) => {
89
116
  diagnostics: []
90
117
  })));
91
118
  });
92
- connection.onCompletion(({ textDocument: { uri }, position }) => withDocument(uri, (document) => completionItems(currentIndex(), document.getText(), position, currentParseOptions()), []));
93
- connection.onDefinition(({ textDocument: { uri }, position }) => withDocument(uri, (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position, currentParseOptions())), null));
119
+ connection.onCompletion(({ textDocument: { uri }, position }) => withDocument(uri, (document) => completionItems(currentIndex(), document.getText(), position), []));
120
+ connection.onDefinition(({ textDocument: { uri }, position }) => withDocument(uri, (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position)), null));
94
121
  connection.languages.semanticTokens.on(({ textDocument: { uri } }) => withDocument(uri, semanticTokensOf, { data: [] }));
95
122
  documents.listen(connection);
96
123
  connection.listen();
97
124
  //#endregion
98
125
  export {};
99
126
 
100
- //# sourceMappingURL=server-DPPxtJ6Y.mjs.map
127
+ //# sourceMappingURL=server-DWgrivkG.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DWgrivkG.mjs","names":["Arr","Rec","Arr"],"sources":["../src/settings.ts","../src/server.ts"],"sourcesContent":["import { Array as Arr, Option, pipe, Record as Rec, Schema } from \"effect\"\n\nimport { severityOf, type UnresolvedSeverity } from \"./diagnostics\"\n\n/**\n * What the reader can set, through the editor's initialization options:\n *\n * ```json\n * { \"sources\": [\"src\"], \"unresolvedLinks\": \"warning\", \"frontmatter\": true }\n * ```\n *\n * Which markers open a decoration is not among them: that is Cosense's syntax, and the parser\n * reads every one of them (`[! 注意]` included) as Cosense Web does.\n */\nexport interface Settings {\n /**\n * Where the pages are, relative to each workspace folder. Empty reads the whole folder.\n * A site keeps its pages in a few directories, and the rest of the repository is noise.\n */\n readonly sources: ReadonlyArray<string>\n /** How loudly a link to a missing page is reported. */\n readonly unresolvedLinks: UnresolvedSeverity\n /**\n * Whether a `---` fence on a page's first line opens YAML to skip. Off for Cosense pages,\n * which have no frontmatter: a page titled `---` would otherwise lose its first lines.\n */\n readonly frontmatter: boolean\n}\n\nexport const defaultSettings: Settings = {\n sources: [],\n unresolvedLinks: \"warning\",\n frontmatter: true,\n}\n\n/** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */\nconst decodeOr =\n <A, I>(schema: Schema.Schema<A, I>, fallback: A) =>\n (value: unknown): A =>\n Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback)\n\n/** The options as an object, which the editor may send in any shape or not at all. */\nconst OptionsObject = Schema.Record({ key: Schema.String, value: Schema.Unknown })\n\n/**\n * The non-empty strings of a list. Anything else in it is dropped rather than failing the\n * whole list, and anything but a list counts as none.\n */\nconst stringsOf = (value: unknown): ReadonlyArray<string> =>\n Arr.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString))\n\n/**\n * Each setting read on its own, so one of the wrong shape falls back to its default\n * without taking the others with it.\n */\nexport const settingsOf = (options: unknown): Settings => {\n const field = (key: keyof Settings): unknown =>\n pipe(\n Schema.decodeUnknownOption(OptionsObject)(options),\n Option.flatMap(Rec.get(key)),\n Option.getOrUndefined,\n )\n return {\n sources: stringsOf(field(\"sources\")),\n unresolvedLinks: severityOf(field(\"unresolvedLinks\")),\n frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field(\"frontmatter\")),\n }\n}\n","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, 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\nconst semanticTokensOf = (document: TextDocument): SemanticTokens => ({\n data: encodeTokens(\n computeTokens(document.getText(), {\n components: readsComponents(document),\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 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) => completionItems(currentIndex(), document.getText(), position),\n [],\n ),\n)\n\nconnection.onDefinition(({ textDocument: { uri }, position }): Definition | null =>\n withDocument(\n uri,\n (document) => Option.getOrNull(definitionOf(currentIndex(), document.getText(), position)),\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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"}
@@ -1,6 +1,5 @@
1
1
  import { Array, Match, Option, Order, pipe } from "effect";
2
2
  import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
3
- import { customDecorations } from "@cosense-toolbox/parser/extensions";
4
3
  import { visit } from "@cosense-toolbox/parser/utils";
5
4
  //#region src/tree.ts
6
5
  const leaf = (items) => ({
@@ -119,14 +118,6 @@ const LSP_TYPE = {
119
118
  * accepts token types of the caller's own naming. The LSP's types keep their places.
120
119
  */
121
120
  const legendOf = (notations) => Array.dedupe([...LEGEND, ...Array.map(notations, (notation) => notation.name)]);
122
- /** `parseOptions` that also read the markers of `notations` as decorations. */
123
- const withNotations = (parseOptions, notations) => Array.match(notations, {
124
- onEmpty: () => parseOptions,
125
- onNonEmpty: (added) => ({
126
- ...parseOptions,
127
- extensions: [...parseOptions.extensions ?? [], customDecorations(Array.map(added, (notation) => notation.marker))]
128
- })
129
- });
130
121
  /** Leaf inline nodes that map 1:1. Each sits on one line, so no splitting is needed. */
131
122
  const INLINE_TOKEN_TYPE = {
132
123
  internalLink: "link",
@@ -274,7 +265,7 @@ const computeTokens = (text, options = {}) => {
274
265
  onNone: () => [],
275
266
  onSome: (end) => lineSpans("frontmatter", lines, 0, end)
276
267
  });
277
- const body = parse(lines.slice(page.offset).join("\n"), withNotations(options.parseOptions ?? {}, notations));
268
+ const body = parse(lines.slice(page.offset).join("\n"), options.parseOptions ?? {});
278
269
  return pipe([...frontmatterTokens, ...gather(body, nodeTokens(page))], Array.filter((t) => t.length > 0), Array.sort(BY_POSITION));
279
270
  };
280
271
  /**
@@ -307,6 +298,6 @@ const encodeTokens = (tokens, legend = LEGEND) => {
307
298
  ]]), ([, rows]) => Array.flatten(rows));
308
299
  };
309
300
  //#endregion
310
- export { encodeTokens as a, legendOf as c, gather as d, leaf as f, computeTokens as i, withNotations as l, LEGEND as n, fenceOf as o, TOKEN_TYPES as r, frontmatterEnd as s, COMPONENT_LINE as t, branch as u };
301
+ export { encodeTokens as a, legendOf as c, leaf as d, computeTokens as i, branch as l, LEGEND as n, fenceOf as o, TOKEN_TYPES as r, frontmatterEnd as s, COMPONENT_LINE as t, gather as u };
311
302
 
312
- //# sourceMappingURL=tokens-D0NP_uLo.mjs.map
303
+ //# sourceMappingURL=tokens-pb6vW18i.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens-pb6vW18i.mjs","names":["Arr"],"sources":["../src/tree.ts","../src/tokens.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","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 */\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/**\n * Each token's place in `legend`: a notation's own name when the legend has it, else the\n * LSP type its kind is drawn as.\n */\nconst legendIndex = (legend: ReadonlyArray<string>) => {\n const placeOf = (type: string): Option.Option<number> =>\n Option.liftPredicate(legend.indexOf(type), (index) => index >= 0)\n const drawnAs = (token: RawToken) => Option.getOrElse(placeOf(LSP_TYPE[token.type]), () => 0)\n return (token: RawToken): number =>\n Match.value(token).pipe(\n Match.when({ type: \"notation\" }, (notation) =>\n Option.getOrElse(placeOf(notation.name), () => drawnAs(notation)),\n ),\n Match.orElse(drawnAs),\n )\n}\n\n/**\n * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.\n * `legend` is the one sent at initialize: LEGEND, or `legendOf(notations)`.\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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"}
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;
@@ -45,8 +48,6 @@ declare const LEGEND: readonly ["namespace", "type", "function", "variable", "pr
45
48
  * accepts token types of the caller's own naming. The LSP's types keep their places.
46
49
  */
47
50
  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
51
  declare const COMPONENT_LINE: RegExp;
51
52
  /** The last line of the `---` fence around YAML at the very top, if the file opens with one. */
52
53
  declare const frontmatterEnd: (lines: ReadonlyArray<string>) => Option.Option<number>;
@@ -58,7 +59,7 @@ declare const fenceOf: (lines: ReadonlyArray<string>, frontmatter: boolean) => O
58
59
  interface ComputeTokensOptions {
59
60
  /** `.csnx` also reads a line that is one component tag. `.csn` never does. */
60
61
  readonly components?: boolean;
61
- /** How to parse: the site's notation extensions, so `[! 注意]` is not read as a link. */
62
+ /** How to parse: the site's own notation extensions, as its build parses with them. */
62
63
  readonly parseOptions?: ParseOptions;
63
64
  /** Notations the caller adds, each sent as a token of its own name (see `Notation`). */
64
65
  readonly notations?: ReadonlyArray<Notation>;
@@ -76,5 +77,5 @@ declare const computeTokens: (text: string, options?: ComputeTokensOptions) => R
76
77
  */
77
78
  declare const encodeTokens: (tokens: ReadonlyArray<RawToken>, legend?: ReadonlyArray<string>) => number[];
78
79
  //#endregion
79
- export { COMPONENT_LINE, ComputeTokensOptions, LEGEND, Notation, RawToken, TOKEN_TYPES, TokenType, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf, withNotations };
80
+ export { COMPONENT_LINE, ComputeTokensOptions, LEGEND, Notation, RawToken, TOKEN_TYPES, TokenType, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf };
80
81
  //# 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;;;;;cAqDA,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;;;;;cA+CpE,eACX,QAAQ,cAAc,WACtB,SAAQ"}
package/dist/tokens.mjs CHANGED
@@ -1,2 +1,2 @@
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 { a as encodeTokens, c as legendOf, i as computeTokens, n as LEGEND, o as fenceOf, r as TOKEN_TYPES, s as frontmatterEnd, t as COMPONENT_LINE } from "./tokens-pb6vW18i.mjs";
2
+ export { COMPONENT_LINE, LEGEND, TOKEN_TYPES, computeTokens, encodeTokens, fenceOf, frontmatterEnd, legendOf };
@@ -1,8 +1,7 @@
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 { d as leaf, l as branch, o as fenceOf, t as COMPONENT_LINE, u as gather } from "./tokens-pb6vW18i.mjs";
2
2
  import { normalizeForMatch } from "./completion.mjs";
3
- import { Array, Effect, Match, Option, Record, Schema, pipe } from "effect";
3
+ import { Array, Effect, Match, Option, Schema, pipe } from "effect";
4
4
  import { normalizeLineEndings, parse } from "@cosense-toolbox/parser";
5
- import { customDecorations } from "@cosense-toolbox/parser/extensions";
6
5
  import { DiagnosticSeverity } from "vscode-languageserver/node";
7
6
  import { basename, extname, join, relative, resolve, sep } from "node:path";
8
7
  import { readFile, readdir } from "node:fs/promises";
@@ -63,44 +62,6 @@ const unresolvedLinkDiagnostics = (index, text, options) => {
63
62
  }));
64
63
  };
65
64
  //#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
65
  //#region src/workspace.ts
105
66
  const emptyIndex = { pages: [] };
106
67
  const PAGE_EXTENSIONS = [".csn", ".csnx"];
@@ -166,6 +127,6 @@ const rootsOf = (folders, sources) => pipe(Array.filterMap(folders ?? [], ({ uri
166
127
  onNonEmpty: (under) => Array.map(under, (source) => resolve(folder, source))
167
128
  })));
168
129
  //#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 };
130
+ export { rootsOf as a, readPageFiles as i, indexOf as n, severityOf as o, readIndex as r, unresolvedLinkDiagnostics as s, emptyIndex as t };
170
131
 
171
- //# sourceMappingURL=workspace-DouAOZnK.mjs.map
132
+ //# sourceMappingURL=workspace-XJqmAxae.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-XJqmAxae.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":";;;;;;;;;;;;;;;;AAsBA,MAAa,qBAAqB,OAAO,QAAQ,OAAO,QAAQ,eAAe,WAAW,OAAO;;;;;AAQjG,MAAa,cAAc,YACzB,OAAO,UACL,OAAO,oBAAoB,kBAAkB,CAAC,CAAC,OAAO,SAC5B,SAC5B;;AAGF,MAAM,eAAe,aACnB,MAAM,MAAM,QAAQ,CAAC,CAAC,KACpB,MAAM,KAAK,aAAa,OAAO,KAAK,CAAC,GACrC,MAAM,KAAK,cAAc,OAAO,KAAK,mBAAmB,IAAI,CAAC,GAC7D,MAAM,KAAK,qBAAqB,OAAO,KAAK,mBAAmB,WAAW,CAAC,GAC3E,MAAM,KAAK,iBAAiB,OAAO,KAAK,mBAAmB,OAAO,CAAC,GACnE,MAAM,KAAK,eAAe,OAAO,KAAK,mBAAmB,KAAK,CAAC,GAC/D,MAAM,UACR;;;;;AAsBF,MAAM,WACJ,MACA,EACE,aAAa,OACb,eAAe,CAAC,GAChB,cAAc,WAEU;CAC1B,MAAM,QAAQ,qBAAqB,IAAI,CAAC,CAAC,MAAM,IAAI;CAEnD,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,WAAW,GAAG;EACvD,cAAc;EACd,SAAS,QAAQ,MAAM;CACzB,CAAC;CACD,MAAM,mBAAmB,SACvB,cAAc,eAAe,KAAK,MAAM,OAAO,WAAW,EAAE;CAE9D,OAAO,OAAO,MAAM,MAAM,MAAM,MAAM,CAAC,CAAC,KAAK,IAAI,GAAG,YAAY,IAAI,SAClE,MAAM,MAAM,IAAI,CAAC,CAAC,KAChB,MAAM,KAAK,EAAE,MAAM,QAAQ,SAAS,KAAa,CAAC,CAAC,CAAC,GACpD,MAAM,KAAK,EAAE,MAAM,OAAO,IAAI,SAC5B,gBAAgB,KAAK,SAAS,MAAM,IAAI,IAAI,KAAa,CAAC,CAAC,IAAI,OAAe,CAAC,CAAC,CAClF,GACA,MAAM,KAAK,EAAE,MAAM,eAAe,IAAI,SACpC,OAAO,CAAC;EAAE;EAAM,MAAM,KAAK,SAAS,MAAM,OAAO;CAAO,CAAC,CAAC,CAC5D,GACA,MAAM,aAAa,OAAe,CAAC,CAAC,CAAC,CACvC,CACF;AACF;;AAGA,MAAa,6BACX,OACA,MACA,YACiB;CACjB,MAAM,WAAW,IAAI,IAAIA,MAAI,IAAI,MAAM,QAAQ,SAAS,kBAAkB,KAAK,KAAK,CAAC,CAAC;CACtF,OAAO,KACL,YAAY,QAAQ,QAAQ,GAC5B,OAAO,MAAM;EACX,cAAc,CAAC;EACf,SAAS,aACP,KACE,QAAQ,MAAM,OAAO,GACrBA,MAAI,QAAQ,EAAE,WAAW,CAAC,SAAS,IAAI,kBAAkB,KAAK,MAAM,CAAC,CAAC,GACtEA,MAAI,KAAK,EAAE,MAAM,YAAwB;GACvC,OAAO;IACL,OAAO;KAAE;KAAM,WAAW,KAAK,SAAS,MAAM;IAAO;IACrD,KAAK;KAAE;KAAM,WAAW,KAAK,SAAS,IAAI;IAAO;GACnD;GACA;GACA,QAAQ;GACR,MAAM;GACN,SAAS,qBAAqB,KAAK,OAAO;EAC5C,EAAE,CACJ;CACJ,CAAC,CACH;AACF;;;AC7FA,MAAa,aAAoB,EAAE,OAAO,CAAC,EAAE;AAI7C,MAAM,kBAAyC,CAAC,QAAQ,OAAO;AAI/D,MAAM,mBAA0C;CAC9C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,aAAa,cACjB,KACE,OAAO,iBAAiB,QAAQ,WAAW,EAAE,eAAe,KAAK,CAAC,CAAC,GACnE,OAAO,oBAAoB,CAAC,CAAC,CAC/B;;AAGF,MAAM,WAAW,WAAmB,UAClC,MAAM,MAAM,KAAK,CAAC,CAAC,KACjB,MAAM,MACH,QAAQ,IAAI,YAAY,KAAKC,MAAI,SAAS,kBAAkB,IAAI,IAAI,SAC/D,OAAO,QAAQ,CAAC,CAAC,CACzB,GACA,MAAM,MACH,QAAQ,IAAI,YAAY,SACnB,UAAU,KAAK,WAAW,MAAM,IAAI,CAAC,CAC7C,GACA,MAAM,MACH,SAASA,MAAI,SAAS,iBAAiB,QAAQ,KAAK,IAAI,CAAC,SACpD,OAAO,QAAQ,CAAC,KAAK,WAAW,MAAM,IAAI,CAAC,CAAC,CACpD,GACA,MAAM,aAAa,OAAO,QAAQ,CAAC,CAAC,CAAC,CACvC;;AAGF,MAAM,aAAa,cACjB,KACE,UAAU,SAAS,GACnB,OAAO,IAAIA,MAAI,QAAQ,UAAU,CAAC,MAAM,KAAK,WAAW,GAAG,CAAC,CAAC,GAC7D,OAAO,QAAQ,OAAO,SAAS,UAAU,QAAQ,WAAW,KAAK,CAAC,CAAC,GACnE,OAAO,IAAIA,MAAI,OAAO,CACxB;;AAKF,MAAM,sBAAsB,SAC1B,KACE,OAAO,cAAc,OAAO,QAAQ,IAAI,WAAW,OAAO,CAAC,GAC3D,OAAO,SAAS,QACd,KACE,IAAI,QAAQ,SAAS,CAAC,GACtB,OAAO,eAAe,QAAQ,QAAQ,EAAE,GACxC,OAAO,KAAK,QAAQ,IAAI,MAAM,MAAM,CAAC,CAAC,CACxC,CACF,GACA,OAAO,gBAAgB,IAAI,CAC7B;AAEF,MAAM,aAAa,SACjB,KACE,KAAK,MAAM,IAAI,GACfA,MAAI,KAAK,SAAS,KAAK,KAAK,CAAC,GAC7BA,MAAI,WAAW,SAAS,SAAS,EAAE,CACrC;;;;;;;;;;;AAYF,MAAM,WAAW,MAAc,MAAc,gBAC3C,KACE,KAAK,QAAQ,UAAU,IAAI,IAC1B,eACC,OAAO,UACL,OAAO,IACL,OAAO,cAAc,kBAAkB,WAAW,GAClD,kBACF,SACM,UACR,GACF,WACA,OAAO,gBAAgB,SAAS,MAAM,QAAQ,IAAI,CAAC,CAAC,CACtD;;AAGF,MAAM,cAAc,MAAc,SAAyB,SAAS,MAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;;AAmBnG,MAAM,gBACJ,MACA,MACA,EAAE,cAAc,WAEhB,KACE,OAAO,iBAAiB,SAAS,MAAM,MAAM,CAAC,GAC9C,OAAO,KAAK,SACV,OAAO,KAAK;CACV,MAAM;EACJ,OAAO,QAAQ,MAAM,MAAM,WAAW;EACtC,KAAK,cAAc,IAAI,CAAC,CAAC;EACzB,UAAU,WAAW,MAAM,IAAI;CACjC;CACA;CACA;AACF,CAAC,CACH,GACA,OAAO,oBAAoB,OAAO,KAAK,CAAC,CAC1C;;AAGF,MAAM,cACH,aACA,SACC,KACE,UAAU,IAAI,GACd,OAAO,QAAQ,OAAO,SAAS,SAAS,aAAa,MAAM,MAAM,OAAO,CAAC,CAAC,GAC1E,OAAO,IAAIA,MAAI,QAAQ,CACzB;;;;;AAMJ,MAAa,iBACX,OACA,UAA4B,CAAC,MAE7B,KACE,OAAO,QAAQ,OAAO,WAAW,OAAO,CAAC,GACzC,OAAO,KAAK,UAAUA,MAAI,WAAWA,MAAI,QAAQ,KAAK,IAAI,GAAG,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CACvF;;AAGF,MAAa,WAAW,WAA2C,EACjE,OAAOA,MAAI,IAAI,QAAQ,SAAS,KAAK,IAAI,EAC3C;;AAGA,MAAa,aACX,OACA,UAA4B,CAAC,MACJ,OAAO,IAAI,cAAc,OAAO,OAAO,GAAG,OAAO;;;;;AAM5E,MAAa,WACX,SACA,YAEA,KACEA,MAAI,UAAU,WAAW,CAAC,IAAI,EAAE,UAAU,OAAO,cAAc,aAAa,CAAC,CAAC,GAAG,CAAC,GAClFA,MAAI,SAAS,WACXA,MAAI,MAAM,SAAS;CACjB,eAAe,CAAC,MAAM;CACtB,aAAa,UAAUA,MAAI,IAAI,QAAQ,WAAW,QAAQ,QAAQ,MAAM,CAAC;AAC3E,CAAC,CACH,CACF"}
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.3",
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",
@@ -45,7 +45,7 @@
45
45
  "prepublishOnly": "bun run build"
46
46
  },
47
47
  "dependencies": {
48
- "@cosense-toolbox/parser": "0.1.0-beta.2",
48
+ "@cosense-toolbox/parser": "0.1.0-beta.3",
49
49
  "effect": "^3.22.1",
50
50
  "vscode-languageserver": "^10.1.0",
51
51
  "vscode-languageserver-textdocument": "^1.0.12"
@@ -57,7 +57,6 @@
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public",
60
- "tag": "beta",
61
60
  "registry": "https://registry.npmjs.org/"
62
61
  }
63
62
  }
@@ -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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"tokens-D0NP_uLo.mjs","names":["Arr"],"sources":["../src/tree.ts","../src/tokens.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","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 { customDecorations } from \"@cosense-toolbox/parser/extensions\"\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 notation the caller adds to Cosense's: a marker that opens a decoration, as `!` opens\n * `[! 注意]`, and the token type it is sent as. The marker is also taught to the parser, so\n * the bracket is not read as a link.\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 */\nexport const legendOf = (notations: ReadonlyArray<Notation>): ReadonlyArray<string> =>\n Arr.dedupe([...LEGEND, ...Arr.map(notations, (notation) => notation.name)])\n\n/** `parseOptions` that also read the markers of `notations` as decorations. */\nexport const withNotations = (\n parseOptions: ParseOptions,\n notations: ReadonlyArray<Notation>,\n): ParseOptions =>\n Arr.match(notations, {\n onEmpty: () => parseOptions,\n onNonEmpty: (added): ParseOptions => ({\n ...parseOptions,\n extensions: [\n ...(parseOptions.extensions ?? []),\n customDecorations(Arr.map(added, (notation) => notation.marker)),\n ],\n }),\n })\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 notation extensions, so `[! 注意]` is not read as a link. */\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(\n lines.slice(page.offset).join(\"\\n\"),\n withNotations(options.parseOptions ?? {}, notations),\n )\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/**\n * Each token's place in `legend`: a notation's own name when the legend has it, else the\n * LSP type its kind is drawn as.\n */\nconst legendIndex = (legend: ReadonlyArray<string>) => {\n const placeOf = (type: string): Option.Option<number> =>\n Option.liftPredicate(legend.indexOf(type), (index) => index >= 0)\n const drawnAs = (token: RawToken) => Option.getOrElse(placeOf(LSP_TYPE[token.type]), () => 0)\n return (token: RawToken): number =>\n Match.value(token).pipe(\n Match.when({ type: \"notation\" }, (notation) =>\n Option.getOrElse(placeOf(notation.name), () => drawnAs(notation)),\n ),\n Match.orElse(drawnAs),\n )\n}\n\n/**\n * LSP relative encoding: [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]*.\n * `legend` is the one sent at initialize: LEGEND, or `legendOf(notations)`.\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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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"workspace-DouAOZnK.mjs","names":["Arr","Arr","Rec","Arr"],"sources":["../src/diagnostics.ts","../src/settings.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 { ParseOptions } from \"@cosense-toolbox/parser\"\nimport { customDecorations } from \"@cosense-toolbox/parser/extensions\"\nimport { Array as Arr, Option, pipe, Record as Rec, Schema } from \"effect\"\n\nimport { severityOf, type UnresolvedSeverity } from \"./diagnostics\"\n\n/**\n * What the reader can set, through the editor's initialization options:\n *\n * ```json\n * { \"sources\": [\"src\"], \"decorations\": [\"!\"], \"unresolvedLinks\": \"warning\", \"frontmatter\": true }\n * ```\n */\nexport interface Settings {\n /**\n * Where the pages are, relative to each workspace folder. Empty reads the whole folder.\n * A site keeps its pages in a few directories, and the rest of the repository is noise.\n */\n readonly sources: ReadonlyArray<string>\n /**\n * Markers read as text decorations on top of Cosense's own (`* / - _`), as the site's\n * `customDecorations` has them. Without them, `[! 注意]` reads as a link to a page called\n * \"! 注意\".\n */\n readonly decorations: ReadonlyArray<string>\n /** How loudly a link to a missing page is reported. */\n readonly unresolvedLinks: UnresolvedSeverity\n /**\n * Whether a `---` fence on a page's first line opens YAML to skip. Off for Cosense pages,\n * which have no frontmatter: a page titled `---` would otherwise lose its first lines.\n */\n readonly frontmatter: boolean\n}\n\nexport const defaultSettings: Settings = {\n sources: [],\n decorations: [],\n unresolvedLinks: \"warning\",\n frontmatter: true,\n}\n\n/** `schema`'s reading of `value`, or `fallback` when `value` is not of that shape. */\nconst decodeOr =\n <A, I>(schema: Schema.Schema<A, I>, fallback: A) =>\n (value: unknown): A =>\n Option.getOrElse(Schema.decodeUnknownOption(schema)(value), () => fallback)\n\n/** The options as an object, which the editor may send in any shape or not at all. */\nconst OptionsObject = Schema.Record({ key: Schema.String, value: Schema.Unknown })\n\n/**\n * The non-empty strings of a list. Anything else in it is dropped rather than failing the\n * whole list, and anything but a list counts as none.\n */\nconst stringsOf = (value: unknown): ReadonlyArray<string> =>\n Arr.filter(decodeOr(Schema.Array(Schema.Unknown), [])(value), Schema.is(Schema.NonEmptyString))\n\n/**\n * Each setting read on its own, so one of the wrong shape falls back to its default\n * without taking the others with it.\n */\nexport const settingsOf = (options: unknown): Settings => {\n const field = (key: keyof Settings): unknown =>\n pipe(\n Schema.decodeUnknownOption(OptionsObject)(options),\n Option.flatMap(Rec.get(key)),\n Option.getOrUndefined,\n )\n return {\n sources: stringsOf(field(\"sources\")),\n decorations: stringsOf(field(\"decorations\")),\n unresolvedLinks: severityOf(field(\"unresolvedLinks\")),\n frontmatter: decodeOr(Schema.Boolean, defaultSettings.frontmatter)(field(\"frontmatter\")),\n }\n}\n\n/** How the pages are parsed, so the server reads notation as the site's build does. */\nexport const parseOptionsOf = ({ decorations }: Settings): ParseOptions =>\n Arr.match(decorations, {\n onEmpty: (): ParseOptions => ({}),\n onNonEmpty: (markers): ParseOptions => ({ extensions: [customDecorations(markers)] }),\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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"}