@cosense-toolbox/textmate 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qaynam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @cosense-toolbox/textmate
2
+
3
+ > **beta.** スコープ名はまだ変わりえます。安定するまではバージョン固定での利用を推奨します。
4
+
5
+ Cosense 記法の TextMate 文法。`.csn` 用の `cosense` と、コンポーネント行も読む `.csnx` 用の `cosenseX` がある。
6
+ Shiki の `LanguageRegistration` としてそのまま渡せるほか、`.tmLanguage.json` としても同梱している。
7
+
8
+ ```sh
9
+ npm i @cosense-toolbox/textmate@beta
10
+ ```
11
+
12
+ ## Shiki
13
+
14
+ ```ts
15
+ import { cosense, cosenseX } from "@cosense-toolbox/textmate"
16
+ import { createHighlighter } from "shiki"
17
+
18
+ const shiki = await createHighlighter({ themes: ["catppuccin-latte"], langs: [cosense, cosenseX] })
19
+ shiki.codeToHtml("タイトル\n[* 太字] [リンク] #tag", { lang: "cosense", theme: "catppuccin-latte" })
20
+ ```
21
+
22
+ 言語名は `cosense` / `cosense-x`、別名は `csn` / `csnx`。Markdown の ` ```csn ` にも色が付く。
23
+ Astro なら `markdown.shikiConfig.langs` に同じものを渡す。
24
+
25
+ ## ファイルとして使う
26
+
27
+ ```ts
28
+ import grammar from "@cosense-toolbox/textmate/cosense.tmLanguage.json" with { type: "json" }
29
+ ```
30
+
31
+ VS Code 拡張の `contributes.grammars` などには、このファイルを指定する。
32
+
33
+ ## スコープ
34
+
35
+ どのテーマでも色が付くように、スコープはテーマが既に塗っている名前から始めている
36
+ (`markup.bold`、`string.other.link`、`markup.raw` など)。一覧は `SCOPES` で取れる。
37
+
38
+ | 記法 | スコープ |
39
+ | -------------------------------- | ------------------------------------------------------------------------------------- |
40
+ | タイトル (1 行目) | `markup.heading.cosense` |
41
+ | `[ページ]` | `string.other.link.internal.cosense` |
42
+ | `[/project/ページ]` | `string.other.link.project.cosense` |
43
+ | URL、`[ラベル URL]` | `markup.underline.link.external.cosense` |
44
+ | 画像 | `markup.underline.link.image.cosense` |
45
+ | `[user.icon]` | `string.other.link.icon.cosense` |
46
+ | `#tag` | `entity.name.tag.hashtag.cosense` |
47
+ | `` `code` `` | `markup.inline.raw.cosense` |
48
+ | `code:` ブロック | `markup.raw.block.cosense` |
49
+ | `table:` ブロック | `markup.other.table.cosense` |
50
+ | `[$ 数式]` | `constant.other.formula.cosense` |
51
+ | `>` 引用 | 行全体に `markup.quote.cosense`、記号に `punctuation.definition.quote.begin.cosense` |
52
+ | `[* ]` / `[** ]` / `[*** ]` 以上 | `markup.bold.cosense` / `markup.bold.level2.cosense` / `markup.bold.level3.cosense` |
53
+ | `[/ ]` `[- ]` `[_ ]` | `markup.italic.cosense` / `markup.strikethrough.cosense` / `markup.underline.cosense` |
54
+ | 先頭の `---` で囲んだ YAML | `comment.block.frontmatter.cosense` |
55
+ | コンポーネントのタグ名 (`.csnx`) | `support.class.component.cosense` |
56
+ | 属性名 | `entity.other.attribute-name.cosense` |
57
+ | 引用符で囲んだ属性値 | `string.quoted.attribute-value.cosense` |
58
+ | `{ }` で囲んだ属性値 | `meta.embedded.expression.cosense` (中の数値は `constant.numeric.cosense`) |
59
+
60
+ `[-* x]` のように記号を重ねると、それぞれのスコープが全部付く。
61
+
62
+ コンポーネント行は行全体に `meta.tag.component.cosense` が付き、行頭のタグだけを JSX として読む。
63
+ タグの後ろの文章は、Cosense の記法としても読まない。`.csnx` では 1 行目がタグならタイトルにしない。
64
+
65
+ ## Language Server との関係
66
+
67
+ Zed は TextMate 文法を読まないので、Zed の色は
68
+ [`@cosense-toolbox/lsp`](../lsp) の semantic tokens から来る。
69
+ こちらはパーサーそのもので読み、この文法は正規表現でそれに寄せている。
70
+
71
+ 両者が同じところに同じ色を付けることは、テストで 1 文字ずつ突き合わせて確かめている
72
+ (`src/parity.test.ts`。パーサーの conformance fixture と examples の記事が対象)。
73
+ 記法の読み方を変えたら、両方をそろえないとこのテストが落ちる。
74
+
75
+ ## 分かっている差
76
+
77
+ - **Shiki は空行を文法に通さずに飛ばす。** そのため Shiki でだけ次のことが起きる。VS Code では起きない。
78
+ - 空行でコードブロックと表が終わらない。空行の次の行が深く字下げされていると、ブロックの続きとして色が付く。
79
+ - タイトル行が空行だと、次の行がタイトルとして色付けされる (1 行目が空行のページや、frontmatter の直後が空行のページ)。
80
+ - 字下げの深さは「ヘッダ行と同じ空白に続けて、さらに空白がある」で見ている。
81
+ タブと空白を混ぜて字下げしたページでは、パーサーとブロックの範囲がずれることがある。
82
+ - `code:` ブロックの中身は、ファイル名の言語では色付けしない。
@@ -0,0 +1,69 @@
1
+ //#region src/grammar.d.ts
2
+ /** A TextMate rule as JSON. Assignable to Shiki's and VS Code's own types. */
3
+ interface Rule {
4
+ name?: string;
5
+ match?: string;
6
+ begin?: string;
7
+ end?: string;
8
+ captures?: Record<string, Rule>;
9
+ beginCaptures?: Record<string, Rule>;
10
+ endCaptures?: Record<string, Rule>;
11
+ patterns?: Rule[];
12
+ include?: string;
13
+ applyEndPatternLast?: boolean;
14
+ }
15
+ /** A grammar as Shiki takes it (`LanguageRegistration`), which is also a `.tmLanguage.json`. */
16
+ interface Grammar {
17
+ name: Dialect;
18
+ displayName: string;
19
+ scopeName: `text.${Dialect}`;
20
+ aliases: string[];
21
+ fileTypes: string[];
22
+ patterns: Rule[];
23
+ repository: Record<string, Rule>;
24
+ }
25
+ /** `.csn`, and `.csnx`, which also reads a line that is one component tag. */
26
+ type Dialect = "cosense" | "cosense-x";
27
+ //#endregion
28
+ //#region src/scopes.d.ts
29
+ /**
30
+ * The TextMate scope each Cosense notation is drawn as.
31
+ *
32
+ * Keys are the token names @cosense-toolbox/lsp reasons in, so the two
33
+ * highlighters describe the same things; the parity test holds them to it. Each scope
34
+ * starts with a name themes already colour (`markup.bold`, `string.other.link`, ...), so
35
+ * any Shiki or VS Code theme draws Cosense without knowing about it.
36
+ */
37
+ declare const SCOPES: {
38
+ readonly title: "markup.heading.cosense";
39
+ readonly link: "string.other.link.internal.cosense";
40
+ readonly projectLink: "string.other.link.project.cosense";
41
+ readonly externalLink: "markup.underline.link.external.cosense";
42
+ readonly hashtag: "entity.name.tag.hashtag.cosense";
43
+ readonly code: "markup.inline.raw.cosense";
44
+ readonly codeBlock: "markup.raw.block.cosense";
45
+ readonly formula: "constant.other.formula.cosense";
46
+ readonly icon: "string.other.link.icon.cosense";
47
+ readonly quote: "punctuation.definition.quote.begin.cosense";
48
+ readonly bold: "markup.bold.cosense";
49
+ readonly italic: "markup.italic.cosense";
50
+ readonly strike: "markup.strikethrough.cosense";
51
+ readonly underline: "markup.underline.cosense";
52
+ readonly image: "markup.underline.link.image.cosense";
53
+ readonly table: "markup.other.table.cosense";
54
+ readonly bold2: "markup.bold.level2.cosense";
55
+ readonly bold3: "markup.bold.level3.cosense";
56
+ readonly component: "support.class.component.cosense";
57
+ readonly attribute: "entity.other.attribute-name.cosense";
58
+ readonly attributeValue: "string.quoted.attribute-value.cosense";
59
+ readonly expression: "meta.embedded.expression.cosense";
60
+ readonly frontmatter: "comment.block.frontmatter.cosense";
61
+ };
62
+ type Notation = keyof typeof SCOPES;
63
+ //#endregion
64
+ //#region src/index.d.ts
65
+ declare const cosense: Grammar;
66
+ declare const cosenseX: Grammar;
67
+ //#endregion
68
+ export { type Dialect, type Grammar, type Notation, type Rule, SCOPES, cosense, cosenseX };
69
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/grammar.ts","../src/scopes.ts","../src/index.ts"],"mappings":";;UAoBiB;EACf;EACA;EACA;EACA;EACA,WAAW,eAAe;EAC1B,gBAAgB,eAAe;EAC/B,cAAc,eAAe;EAC7B,WAAW;EACX;EACA;;;UAIe;EACf,MAAM;EACN;EACA,mBAAmB;EACnB;EACA;EACA,UAAU;EACV,YAAY,eAAe;;;KA6cjB;;;;;;;;;;;cCxeC;;;;;;;;;;;;;;;;;;;;;;;;;KA+BD,wBAAwB;;;cClCvB,SAAS;cAET,UAAU"}
package/dist/index.mjs ADDED
@@ -0,0 +1,375 @@
1
+ import { Array, Brand, Match, Option, Record, pipe } from "effect";
2
+ //#region src/scopes.ts
3
+ /**
4
+ * The TextMate scope each Cosense notation is drawn as.
5
+ *
6
+ * Keys are the token names @cosense-toolbox/lsp reasons in, so the two
7
+ * highlighters describe the same things; the parity test holds them to it. Each scope
8
+ * starts with a name themes already colour (`markup.bold`, `string.other.link`, ...), so
9
+ * any Shiki or VS Code theme draws Cosense without knowing about it.
10
+ */
11
+ const SCOPES = {
12
+ title: "markup.heading.cosense",
13
+ link: "string.other.link.internal.cosense",
14
+ projectLink: "string.other.link.project.cosense",
15
+ externalLink: "markup.underline.link.external.cosense",
16
+ hashtag: "entity.name.tag.hashtag.cosense",
17
+ code: "markup.inline.raw.cosense",
18
+ codeBlock: "markup.raw.block.cosense",
19
+ formula: "constant.other.formula.cosense",
20
+ icon: "string.other.link.icon.cosense",
21
+ quote: "punctuation.definition.quote.begin.cosense",
22
+ bold: "markup.bold.cosense",
23
+ italic: "markup.italic.cosense",
24
+ strike: "markup.strikethrough.cosense",
25
+ underline: "markup.underline.cosense",
26
+ image: "markup.underline.link.image.cosense",
27
+ table: "markup.other.table.cosense",
28
+ bold2: "markup.bold.level2.cosense",
29
+ bold3: "markup.bold.level3.cosense",
30
+ component: "support.class.component.cosense",
31
+ attribute: "entity.other.attribute-name.cosense",
32
+ attributeValue: "string.quoted.attribute-value.cosense",
33
+ expression: "meta.embedded.expression.cosense",
34
+ frontmatter: "comment.block.frontmatter.cosense"
35
+ };
36
+ //#endregion
37
+ //#region src/grammar.ts
38
+ /**
39
+ * The Cosense grammar, built from code rather than written as JSON: emphasis alone takes
40
+ * one rule per combination of markers, and those are generated.
41
+ *
42
+ * The rules follow @cosense-toolbox/parser, and where they cannot (TextMate reads line by
43
+ * line with regexes, the parser reads the whole page) they approximate it. The parity test
44
+ * measures how close that is against the language server.
45
+ *
46
+ * Rules are first built as a tagged union (`Pattern`) and only then encoded as TextMate
47
+ * JSON. The union is what keeps the grammar honest: a rule is a match or a region and never
48
+ * half of each, every `include` names a repository entry that exists, and every scope ends
49
+ * in `.cosense`. A typo in any of those fails to compile instead of silently colouring nothing.
50
+ */
51
+ const Regex = Brand.nominal();
52
+ /** A regex written raw: backslashes are the regex's, not the string literal's. */
53
+ const re = (strings, ...parts) => Regex(String.raw(strings, ...parts));
54
+ const oneOf = (alternatives) => Regex(alternatives.map((alternative) => `(?:${alternative})`).join("|"));
55
+ /** Indentation, as the parser counts it: full-width spaces (U+3000) indent too. */
56
+ const INDENT = re`[ \t\u3000]*`;
57
+ const IMAGE_EXT = re`\.(?i:png|jpe?g|gif|webp|svg|bmp|avif)`;
58
+ /**
59
+ * A URL that is drawn as an image, inside a bracket (so without `[`, `]` or spaces):
60
+ * a Gyazo page, a path ending in an image extension before any query, or a fragment
61
+ * ending in one (`#.svg`, Cosense's way of saying "this is an image").
62
+ */
63
+ const IMAGE_URL = oneOf([
64
+ re`(?i:https?://(?:i\.)?gyazo\.com/[0-9a-f]{20,})[^\s\[\]]*`,
65
+ re`(?i:https?)://[^\s\[\]?#]*${IMAGE_EXT}(?:[?#][^\s\[\]]*)?`,
66
+ re`(?i:https?)://[^\s\[\]#]*#[^\s\[\]]*${IMAGE_EXT}`
67
+ ]);
68
+ const URL = re`(?i:https?)://[^\s\[\]]+`;
69
+ /** What a bracket must end with somewhere, or it is plain text and not worth a rule. */
70
+ const CLOSES = re`(?=.*\])`;
71
+ /** A bracket closes on its own line or not at all. */
72
+ const CLOSE_ON_LINE = re`\]|(?=$)`;
73
+ const MARKERS = re`[*/\-_]`;
74
+ const NOT_STAR = re`[/\-_]`;
75
+ /**
76
+ * A line that opens with a component tag (`.csnx`), as the language server tells one:
77
+ * `<` or `</`, a capitalised name, then a space or the end of the tag.
78
+ */
79
+ const COMPONENT_LINE = re`\s*</?[A-Z][A-Za-z0-9_.]*(?:[^\S\n]|/?>)`;
80
+ const include = (key) => ({
81
+ _tag: "Include",
82
+ key
83
+ });
84
+ const single = (regex, options = {}) => ({
85
+ _tag: "Single",
86
+ regex,
87
+ scopes: options.scopes ?? [],
88
+ captures: options.captures ?? {}
89
+ });
90
+ const region = (options) => ({
91
+ _tag: "Region",
92
+ begin: options.begin,
93
+ end: options.end,
94
+ scopes: options.scopes ?? [],
95
+ beginCaptures: options.beginCaptures ?? {},
96
+ endCaptures: options.endCaptures ?? {},
97
+ patterns: options.patterns ?? [],
98
+ endPatternLast: options.endPatternLast ?? false
99
+ });
100
+ const group = (patterns) => ({
101
+ _tag: "Group",
102
+ patterns
103
+ });
104
+ const scoped = (...scopes) => ({
105
+ scopes,
106
+ patterns: []
107
+ });
108
+ const readAs = (...patterns) => ({
109
+ scopes: [],
110
+ patterns
111
+ });
112
+ const onlyIf = (condition, value) => condition ? Option.some(value) : Option.none();
113
+ const WEIGHTS = [
114
+ 0,
115
+ 1,
116
+ 2,
117
+ 3
118
+ ];
119
+ const SWITCHES = [false, true];
120
+ /** Every combination of markers, including none at all (which `emphasisRules` drops). */
121
+ const MARKER_SETS = pipe(WEIGHTS, Array.flatMap((stars) => Array.map(SWITCHES, (italic) => ({
122
+ stars,
123
+ italic
124
+ }))), Array.flatMap((set) => Array.map(SWITCHES, (strike) => ({
125
+ ...set,
126
+ strike
127
+ }))), Array.flatMap((set) => Array.map(SWITCHES, (underline) => ({
128
+ ...set,
129
+ underline
130
+ }))));
131
+ /** `[*** x]` and louder are one size: Cosense caps the size, and so does the parser. */
132
+ const WEIGHT_SCOPE = {
133
+ 0: Option.none(),
134
+ 1: Option.some(SCOPES.bold),
135
+ 2: Option.some(SCOPES.bold2),
136
+ 3: Option.some(SCOPES.bold3)
137
+ };
138
+ /** All the scopes a run carries, since Cosense applies every marker: `[-* x]` is both. */
139
+ const emphasisScopes = (emphasis) => Array.getSomes([
140
+ onlyIf(emphasis.underline, SCOPES.underline),
141
+ onlyIf(emphasis.strike, SCOPES.strike),
142
+ onlyIf(emphasis.italic, SCOPES.italic),
143
+ WEIGHT_SCOPE[emphasis.stars]
144
+ ]);
145
+ /** A lookahead over the marker run (`[-** x]` -> `-**`) that holds for exactly this many `*`. */
146
+ const starCondition = (stars) => Match.value(stars).pipe(Match.when(0, () => re`(?!${MARKERS}*?\*)`), Match.when(3, () => re`(?=(?:${NOT_STAR}*\*){3})`), Match.orElse((count) => re`(?=(?:${NOT_STAR}*\*){${count}}${NOT_STAR}*\s)`));
147
+ const markerCondition = (marker, wanted) => wanted ? re`(?=${MARKERS}*?${marker})` : re`(?!${MARKERS}*?${marker})`;
148
+ /** Lookaheads over the marker run that hold only for this combination. */
149
+ const markerConditions = (emphasis) => Regex([
150
+ starCondition(emphasis.stars),
151
+ markerCondition(re`/`, emphasis.italic),
152
+ markerCondition(re`\-`, emphasis.strike),
153
+ markerCondition(re`_`, emphasis.underline)
154
+ ].join(""));
155
+ /**
156
+ * `[<markers> body]`, one rule per combination. The body is read again for links and icons
157
+ * but not for emphasis, which does not nest in Cosense (`[* [* x]]` is bold around a link).
158
+ */
159
+ const emphasisRules = Array.filterMap(MARKER_SETS, (emphasis) => pipe(emphasisScopes(emphasis), Option.liftPredicate((scopes) => scopes.length > 0), Option.map((scopes) => region({
160
+ scopes,
161
+ begin: re`\[${markerConditions(emphasis)}(?=${MARKERS}+\s.*\])${MARKERS}+\s+`,
162
+ end: CLOSE_ON_LINE,
163
+ patterns: [include("inline-in-emphasis"), include("nested-bracket")]
164
+ }))));
165
+ /** Anything up to, but not over, a `]]`. */
166
+ const UNTIL_DOUBLE_CLOSE = re`(?:(?!\]\]).)`;
167
+ /** `[[x]]`: an image when x is one, bold otherwise. Closes on the first `]]`, depth or not. */
168
+ const strongRules = [single(re`\[\[(?:${IMAGE_URL}|${UNTIL_DOUBLE_CLOSE}*?${IMAGE_EXT}(?:[?#]${UNTIL_DOUBLE_CLOSE}*)?)\]\]`, { scopes: [SCOPES.image] }), single(re`\[\[(${UNTIL_DOUBLE_CLOSE}+)\]\]`, {
169
+ scopes: [SCOPES.bold],
170
+ captures: { 1: readAs(include("inline-in-emphasis")) }
171
+ })];
172
+ /** `[$ x^2]`. Its body is TeX, not notation, and may hold brackets of its own. */
173
+ const formulaRule = region({
174
+ scopes: [SCOPES.formula],
175
+ begin: re`\[(?=\$)${CLOSES}`,
176
+ end: CLOSE_ON_LINE,
177
+ patterns: [include("bare-bracket")]
178
+ });
179
+ /**
180
+ * Brackets whose body holds no brackets, tried in the parser's order: an icon before a
181
+ * URL before an image path before a project link, and anything left is a page link.
182
+ */
183
+ const simpleTargetRules = (allowImagePath) => Array.getSomes([
184
+ Option.some(single(re`\[[^\[\]]+\.icon(?:\*\d+)?\]`, { scopes: [SCOPES.icon] })),
185
+ Option.some(single(re`\[\s*(?:${URL}\s+)*(?:${IMAGE_URL})(?=[\s\]])(?:\s+${URL})*\s*\]`, { scopes: [SCOPES.image] })),
186
+ Option.some(single(re`\[[^\[\]]*${URL}[^\[\]]*\]`, { scopes: [SCOPES.externalLink] })),
187
+ onlyIf(allowImagePath, single(re`\[[^\[\]]*${IMAGE_EXT}\]`, { scopes: [SCOPES.image] })),
188
+ Option.some(single(re`\[/[^\[\]]*\]`, { scopes: [SCOPES.projectLink] })),
189
+ Option.some(single(re`\[(?=[^\[\]]*[^\s\[\]])[^\[\]]+\]`, { scopes: [SCOPES.link] }))
190
+ ]);
191
+ /** What follows the bracketed notation in both contexts, in the parser's order. */
192
+ const unbracketedRules = [
193
+ single(Regex("`[^`]*`"), { scopes: [SCOPES.code] }),
194
+ single(re`(?:^|(?<=[ \t\u3000]))#[^\s\[\]#]+`, { scopes: [SCOPES.hashtag] }),
195
+ single(re`(?i:https?)://[^\s\]]+`, { scopes: [SCOPES.externalLink] })
196
+ ];
197
+ /**
198
+ * A block is its header and every line indented deeper. Deeper is approximated as
199
+ * "the header's indentation, then more", which is exact when a page indents one way.
200
+ */
201
+ const block = (keyword, scope, patterns) => region({
202
+ scopes: [scope],
203
+ begin: re`^(${INDENT})(${Regex(keyword)}:)(.+)$`,
204
+ beginCaptures: {
205
+ 2: scoped("keyword.other.block.cosense"),
206
+ 3: scoped("entity.name.section.block.cosense")
207
+ },
208
+ end: re`^(?!\1[ \t\u3000])`,
209
+ patterns
210
+ });
211
+ /**
212
+ * The first line is the title, unless the file opens with YAML: then it is the first line
213
+ * after the fence. Nothing on the title line is notation. In `.csnx` a first line that is
214
+ * a component tag is that tag, and the page has no title line.
215
+ */
216
+ const head = (dialect) => {
217
+ const notComponent = dialect.components ? re`(?!${COMPONENT_LINE})` : re``;
218
+ return group([region({
219
+ begin: re`\A(?=---[ \t]*$)`,
220
+ patterns: [include("frontmatter")],
221
+ end: dialect.components ? re`^(?:(?=${COMPONENT_LINE})|(.*)$)` : re`^(.*)$`,
222
+ endCaptures: { 1: scoped(SCOPES.title) },
223
+ endPatternLast: true
224
+ }), single(re`\A${notComponent}.*$`, { scopes: [SCOPES.title] })]);
225
+ };
226
+ /** Every other entry the grammar can `include`. The mapped type makes a missing one an error. */
227
+ const REPOSITORY = {
228
+ frontmatter: region({
229
+ scopes: [SCOPES.frontmatter],
230
+ begin: re`\A---[ \t]*$`,
231
+ end: re`^---[ \t]*$`
232
+ }),
233
+ "code-block": block("code", SCOPES.codeBlock, []),
234
+ "table-block": block("table", SCOPES.table, [single(re`\t`, { scopes: ["punctuation.separator.table-cell.cosense"] })]),
235
+ component: region({
236
+ scopes: ["meta.tag.component.cosense"],
237
+ begin: re`^(?=${COMPONENT_LINE})`,
238
+ end: re`$`,
239
+ patterns: [include("component-tag")]
240
+ }),
241
+ "component-tag": region({
242
+ begin: re`^(\s*)(</?)([A-Z][A-Za-z0-9_.]*)`,
243
+ beginCaptures: {
244
+ 2: scoped("punctuation.definition.tag.begin.cosense"),
245
+ 3: scoped(SCOPES.component)
246
+ },
247
+ end: re`(/?>)|(?=$)`,
248
+ endCaptures: { 1: scoped("punctuation.definition.tag.end.cosense") },
249
+ patterns: [
250
+ single(re`[A-Za-z_:][A-Za-z0-9_:.\-]*`, { scopes: [SCOPES.attribute] }),
251
+ single(re`=`, { scopes: ["punctuation.separator.key-value.cosense"] }),
252
+ single(re`"[^"]*(?:"|$)|'[^']*(?:'|$)`, { scopes: [SCOPES.attributeValue] }),
253
+ include("expression")
254
+ ]
255
+ }),
256
+ expression: region({
257
+ scopes: [SCOPES.expression],
258
+ begin: re`\{`,
259
+ end: re`\}|(?=$)`,
260
+ beginCaptures: { 0: scoped("punctuation.section.embedded.begin.cosense") },
261
+ endCaptures: { 0: scoped("punctuation.section.embedded.end.cosense") },
262
+ patterns: [
263
+ include("expression"),
264
+ single(re`"[^"]*"|'[^']*'`, { scopes: ["string.quoted.cosense"] }),
265
+ single(re`\b\d+(?:\.\d+)?\b`, { scopes: ["constant.numeric.cosense"] }),
266
+ single(re`\b(?:true|false|null|undefined)\b`, { scopes: ["constant.language.cosense"] })
267
+ ]
268
+ }),
269
+ quote: region({
270
+ scopes: ["markup.quote.cosense"],
271
+ begin: re`^${INDENT}(>[> ]*)`,
272
+ beginCaptures: { 1: scoped(SCOPES.quote) },
273
+ end: re`$`,
274
+ patterns: [include("inline")]
275
+ }),
276
+ inline: group([
277
+ ...strongRules,
278
+ formulaRule,
279
+ ...emphasisRules,
280
+ ...simpleTargetRules(true),
281
+ ...unbracketedRules
282
+ ]),
283
+ "inline-in-emphasis": group([
284
+ ...strongRules,
285
+ formulaRule,
286
+ ...simpleTargetRules(false),
287
+ ...unbracketedRules
288
+ ]),
289
+ "nested-bracket": region({
290
+ begin: re`\[`,
291
+ end: CLOSE_ON_LINE,
292
+ patterns: [include("inline-in-emphasis"), include("nested-bracket")]
293
+ }),
294
+ "bare-bracket": region({
295
+ begin: re`\[`,
296
+ end: CLOSE_ON_LINE,
297
+ patterns: [include("bare-bracket")]
298
+ })
299
+ };
300
+ /** A field only when there is something to put in it: TextMate reads absent and empty alike. */
301
+ const field = (key, value) => Option.match(value, {
302
+ onNone: () => ({}),
303
+ onSome: (v) => ({ [key]: v })
304
+ });
305
+ const nonEmpty = (items) => Option.liftPredicate(items, (list) => list.length > 0);
306
+ /** TextMate gives a rule several scopes by separating them with spaces. */
307
+ const nameField = (scopes) => field("name", Option.map(nonEmpty(scopes), (list) => list.join(" ")));
308
+ const patternsField = (patterns) => field("patterns", Option.map(nonEmpty(patterns), Array.map(encode)));
309
+ const encodeCaptures = (captures) => pipe(Object.entries(captures), Array.map(([group, capture]) => [group, {
310
+ ...nameField(capture.scopes),
311
+ ...patternsField(capture.patterns)
312
+ }]), nonEmpty, Option.map(Record.fromEntries));
313
+ const encode = (pattern) => Match.value(pattern).pipe(Match.tag("Include", ({ key }) => ({ include: `#${key}` })), Match.tag("Group", ({ patterns }) => patternsField(patterns)), Match.tag("Single", (rule) => ({
314
+ ...nameField(rule.scopes),
315
+ match: rule.regex,
316
+ ...field("captures", encodeCaptures(rule.captures))
317
+ })), Match.tag("Region", (rule) => ({
318
+ ...nameField(rule.scopes),
319
+ begin: rule.begin,
320
+ end: rule.end,
321
+ ...field("beginCaptures", encodeCaptures(rule.beginCaptures)),
322
+ ...field("endCaptures", encodeCaptures(rule.endCaptures)),
323
+ ...patternsField(rule.patterns),
324
+ ...field("applyEndPatternLast", onlyIf(rule.endPatternLast, true))
325
+ })), Match.exhaustive);
326
+ const DIALECTS = {
327
+ cosense: {
328
+ displayName: "Cosense",
329
+ extension: "csn",
330
+ components: false
331
+ },
332
+ "cosense-x": {
333
+ displayName: "Cosense X",
334
+ extension: "csnx",
335
+ components: true
336
+ }
337
+ };
338
+ /** What a line can be, in the order the parser decides it. */
339
+ const linePatterns = (dialect) => Array.getSomes([
340
+ Option.some(include("head")),
341
+ Option.some(include("code-block")),
342
+ Option.some(include("table-block")),
343
+ onlyIf(dialect.components, include("component")),
344
+ Option.some(include("quote")),
345
+ Option.some(include("inline"))
346
+ ]);
347
+ const buildGrammar = (dialect) => {
348
+ const info = DIALECTS[dialect];
349
+ return {
350
+ name: dialect,
351
+ displayName: info.displayName,
352
+ scopeName: `text.${dialect}`,
353
+ aliases: [info.extension],
354
+ fileTypes: [info.extension],
355
+ patterns: Array.map(linePatterns(info), encode),
356
+ repository: Record.map({
357
+ head: head(info),
358
+ ...REPOSITORY
359
+ }, encode)
360
+ };
361
+ };
362
+ //#endregion
363
+ //#region src/index.ts
364
+ /**
365
+ * TextMate grammars for Cosense notation: `.csn` and, with component lines, `.csnx`.
366
+ *
367
+ * Each export is a Shiki `LanguageRegistration` as it stands, and the same objects are
368
+ * published as `.tmLanguage.json` for editors that read TextMate grammars.
369
+ */
370
+ const cosense = buildGrammar("cosense");
371
+ const cosenseX = buildGrammar("cosense-x");
372
+ //#endregion
373
+ export { SCOPES, cosense, cosenseX };
374
+
375
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["Arr","Rec"],"sources":["../src/scopes.ts","../src/grammar.ts","../src/index.ts"],"sourcesContent":["/**\n * A scope this grammar gives. Every one ends in `.cosense`, which is how a theme or a\n * reader tells them from the scopes of a language embedded next to Cosense.\n */\nexport type Scope = `${string}.cosense`\n\n/**\n * The TextMate scope each Cosense notation is drawn as.\n *\n * Keys are the token names @cosense-toolbox/lsp reasons in, so the two\n * highlighters describe the same things; the parity test holds them to it. Each scope\n * starts with a name themes already colour (`markup.bold`, `string.other.link`, ...), so\n * any Shiki or VS Code theme draws Cosense without knowing about it.\n */\nexport const SCOPES = {\n title: \"markup.heading.cosense\",\n link: \"string.other.link.internal.cosense\",\n projectLink: \"string.other.link.project.cosense\",\n externalLink: \"markup.underline.link.external.cosense\",\n hashtag: \"entity.name.tag.hashtag.cosense\",\n code: \"markup.inline.raw.cosense\",\n codeBlock: \"markup.raw.block.cosense\",\n formula: \"constant.other.formula.cosense\",\n icon: \"string.other.link.icon.cosense\",\n // Only the `>` marker, as the language server marks it. The whole line also carries\n // `markup.quote.cosense` so themes can tint it.\n quote: \"punctuation.definition.quote.begin.cosense\",\n bold: \"markup.bold.cosense\",\n italic: \"markup.italic.cosense\",\n strike: \"markup.strikethrough.cosense\",\n underline: \"markup.underline.cosense\",\n image: \"markup.underline.link.image.cosense\",\n table: \"markup.other.table.cosense\",\n // `[** x]` and louder: still bold to a theme, but a level apart for anyone who wants it.\n bold2: \"markup.bold.level2.cosense\",\n bold3: \"markup.bold.level3.cosense\",\n // `.csnx` only: the tag that opens a component line, read as JSX. The whole line also\n // carries `meta.tag.component.cosense`.\n component: \"support.class.component.cosense\",\n attribute: \"entity.other.attribute-name.cosense\",\n attributeValue: \"string.quoted.attribute-value.cosense\",\n expression: \"meta.embedded.expression.cosense\",\n frontmatter: \"comment.block.frontmatter.cosense\",\n} as const satisfies Record<string, Scope>\n\nexport type Notation = keyof typeof SCOPES\n","/**\n * The Cosense grammar, built from code rather than written as JSON: emphasis alone takes\n * one rule per combination of markers, and those are generated.\n *\n * The rules follow @cosense-toolbox/parser, and where they cannot (TextMate reads line by\n * line with regexes, the parser reads the whole page) they approximate it. The parity test\n * measures how close that is against the language server.\n *\n * Rules are first built as a tagged union (`Pattern`) and only then encoded as TextMate\n * JSON. The union is what keeps the grammar honest: a rule is a match or a region and never\n * half of each, every `include` names a repository entry that exists, and every scope ends\n * in `.cosense`. A typo in any of those fails to compile instead of silently colouring nothing.\n */\nimport { Array as Arr, Brand, Match, Option, pipe, Record as Rec } from \"effect\"\n\nimport { type Scope, SCOPES } from \"./scopes\"\n\n// --- The output: what Shiki and VS Code read ------------------------------------------\n\n/** A TextMate rule as JSON. Assignable to Shiki's and VS Code's own types. */\nexport interface Rule {\n name?: string\n match?: string\n begin?: string\n end?: string\n captures?: Record<string, Rule>\n beginCaptures?: Record<string, Rule>\n endCaptures?: Record<string, Rule>\n patterns?: Rule[]\n include?: string\n applyEndPatternLast?: boolean\n}\n\n/** A grammar as Shiki takes it (`LanguageRegistration`), which is also a `.tmLanguage.json`. */\nexport interface Grammar {\n name: Dialect\n displayName: string\n scopeName: `text.${Dialect}`\n aliases: string[]\n fileTypes: string[]\n patterns: Rule[]\n repository: Record<string, Rule>\n}\n\n// --- Regexes ----------------------------------------------------------------------------\n\n/** A regex in Oniguruma syntax. Branded so a scope or a label cannot be passed as one. */\ntype Regex = string & Brand.Brand<\"Regex\">\nconst Regex = Brand.nominal<Regex>()\n\n/** A regex written raw: backslashes are the regex's, not the string literal's. */\nconst re = (strings: TemplateStringsArray, ...parts: ReadonlyArray<Regex | number>): Regex =>\n Regex(String.raw(strings, ...parts))\n\nconst oneOf = (alternatives: Arr.NonEmptyReadonlyArray<Regex>): Regex =>\n Regex(alternatives.map((alternative) => `(?:${alternative})`).join(\"|\"))\n\n/** Indentation, as the parser counts it: full-width spaces (U+3000) indent too. */\nconst INDENT = re`[ \\t\\u3000]*`\n\nconst IMAGE_EXT = re`\\.(?i:png|jpe?g|gif|webp|svg|bmp|avif)`\n\n/**\n * A URL that is drawn as an image, inside a bracket (so without `[`, `]` or spaces):\n * a Gyazo page, a path ending in an image extension before any query, or a fragment\n * ending in one (`#.svg`, Cosense's way of saying \"this is an image\").\n */\nconst IMAGE_URL = oneOf([\n re`(?i:https?://(?:i\\.)?gyazo\\.com/[0-9a-f]{20,})[^\\s\\[\\]]*`,\n re`(?i:https?)://[^\\s\\[\\]?#]*${IMAGE_EXT}(?:[?#][^\\s\\[\\]]*)?`,\n re`(?i:https?)://[^\\s\\[\\]#]*#[^\\s\\[\\]]*${IMAGE_EXT}`,\n])\n\nconst URL = re`(?i:https?)://[^\\s\\[\\]]+`\n\n/** What a bracket must end with somewhere, or it is plain text and not worth a rule. */\nconst CLOSES = re`(?=.*\\])`\n\n/** A bracket closes on its own line or not at all. */\nconst CLOSE_ON_LINE = re`\\]|(?=$)`\n\nconst MARKERS = re`[*/\\-_]`\n\nconst NOT_STAR = re`[/\\-_]`\n\n/**\n * A line that opens with a component tag (`.csnx`), as the language server tells one:\n * `<` or `</`, a capitalised name, then a space or the end of the tag.\n */\nconst COMPONENT_LINE = re`\\s*</?[A-Z][A-Za-z0-9_.]*(?:[^\\S\\n]|/?>)`\n\n// --- Rules, typed -----------------------------------------------------------------------\n\ntype RepositoryKey =\n | \"head\"\n | \"frontmatter\"\n | \"code-block\"\n | \"table-block\"\n | \"component\"\n | \"component-tag\"\n | \"expression\"\n | \"quote\"\n | \"inline\"\n | \"inline-in-emphasis\"\n | \"nested-bracket\"\n | \"bare-bracket\"\n\n/** `0` is the whole match; this grammar never needs more than three groups. */\ntype CaptureGroup = \"0\" | \"1\" | \"2\" | \"3\"\n\ninterface Capture {\n readonly scopes: ReadonlyArray<Scope>\n readonly patterns: ReadonlyArray<Pattern>\n}\n\ntype Captures = Readonly<Partial<Record<CaptureGroup, Capture>>>\n\ninterface Include {\n readonly _tag: \"Include\"\n readonly key: RepositoryKey\n}\n\n/** A single-line match. */\ninterface Single {\n readonly _tag: \"Single\"\n readonly scopes: ReadonlyArray<Scope>\n readonly regex: Regex\n readonly captures: Captures\n}\n\n/** From `begin` to `end`, with `patterns` tried in between. */\ninterface Region {\n readonly _tag: \"Region\"\n readonly scopes: ReadonlyArray<Scope>\n readonly begin: Regex\n readonly end: Regex\n readonly beginCaptures: Captures\n readonly endCaptures: Captures\n readonly patterns: ReadonlyArray<Pattern>\n /** Try `patterns` before `end` when both match at the same place. */\n readonly endPatternLast: boolean\n}\n\n/** Several patterns tried in order, with no scope of their own. */\ninterface Group {\n readonly _tag: \"Group\"\n readonly patterns: ReadonlyArray<Pattern>\n}\n\ntype Pattern = Include | Single | Region | Group\n\nconst include = (key: RepositoryKey): Pattern => ({ _tag: \"Include\", key })\n\nconst single = (\n regex: Regex,\n options: { readonly scopes?: ReadonlyArray<Scope>; readonly captures?: Captures } = {},\n): Pattern => ({\n _tag: \"Single\",\n regex,\n scopes: options.scopes ?? [],\n captures: options.captures ?? {},\n})\n\nconst region = (options: {\n readonly begin: Regex\n readonly end: Regex\n readonly scopes?: ReadonlyArray<Scope>\n readonly beginCaptures?: Captures\n readonly endCaptures?: Captures\n readonly patterns?: ReadonlyArray<Pattern>\n readonly endPatternLast?: boolean\n}): Pattern => ({\n _tag: \"Region\",\n begin: options.begin,\n end: options.end,\n scopes: options.scopes ?? [],\n beginCaptures: options.beginCaptures ?? {},\n endCaptures: options.endCaptures ?? {},\n patterns: options.patterns ?? [],\n endPatternLast: options.endPatternLast ?? false,\n})\n\nconst group = (patterns: ReadonlyArray<Pattern>): Pattern => ({ _tag: \"Group\", patterns })\n\nconst scoped = (...scopes: ReadonlyArray<Scope>): Capture => ({ scopes, patterns: [] })\n\nconst readAs = (...patterns: ReadonlyArray<Pattern>): Capture => ({ scopes: [], patterns })\n\nconst onlyIf = <A>(condition: boolean, value: A): Option.Option<A> =>\n condition ? Option.some(value) : Option.none()\n\n// --- Inline notation --------------------------------------------------------------------\n\ntype Weight = 0 | 1 | 2 | 3\n\n/** One combination of emphasis markers: how many `*`, and which of `/`, `-`, `_`. */\ninterface Emphasis {\n readonly stars: Weight\n readonly italic: boolean\n readonly strike: boolean\n readonly underline: boolean\n}\n\nconst WEIGHTS: ReadonlyArray<Weight> = [0, 1, 2, 3]\n\nconst SWITCHES = [false, true] as const\n\n/** Every combination of markers, including none at all (which `emphasisRules` drops). */\nconst MARKER_SETS: ReadonlyArray<Emphasis> = pipe(\n WEIGHTS,\n Arr.flatMap((stars) => Arr.map(SWITCHES, (italic) => ({ stars, italic }))),\n Arr.flatMap((set) => Arr.map(SWITCHES, (strike) => ({ ...set, strike }))),\n Arr.flatMap((set) => Arr.map(SWITCHES, (underline) => ({ ...set, underline }))),\n)\n\n/** `[*** x]` and louder are one size: Cosense caps the size, and so does the parser. */\nconst WEIGHT_SCOPE: { readonly [W in Weight]: Option.Option<Scope> } = {\n 0: Option.none(),\n 1: Option.some(SCOPES.bold),\n 2: Option.some(SCOPES.bold2),\n 3: Option.some(SCOPES.bold3),\n}\n\n/** All the scopes a run carries, since Cosense applies every marker: `[-* x]` is both. */\nconst emphasisScopes = (emphasis: Emphasis): ReadonlyArray<Scope> =>\n Arr.getSomes([\n onlyIf(emphasis.underline, SCOPES.underline),\n onlyIf(emphasis.strike, SCOPES.strike),\n onlyIf(emphasis.italic, SCOPES.italic),\n WEIGHT_SCOPE[emphasis.stars],\n ])\n\n/** A lookahead over the marker run (`[-** x]` -> `-**`) that holds for exactly this many `*`. */\nconst starCondition = (stars: Weight): Regex =>\n Match.value(stars).pipe(\n Match.when(0, () => re`(?!${MARKERS}*?\\*)`),\n Match.when(3, () => re`(?=(?:${NOT_STAR}*\\*){3})`),\n Match.orElse((count) => re`(?=(?:${NOT_STAR}*\\*){${count}}${NOT_STAR}*\\s)`),\n )\n\nconst markerCondition = (marker: Regex, wanted: boolean): Regex =>\n wanted ? re`(?=${MARKERS}*?${marker})` : re`(?!${MARKERS}*?${marker})`\n\n/** Lookaheads over the marker run that hold only for this combination. */\nconst markerConditions = (emphasis: Emphasis): Regex =>\n Regex(\n [\n starCondition(emphasis.stars),\n markerCondition(re`/`, emphasis.italic),\n markerCondition(re`\\-`, emphasis.strike),\n markerCondition(re`_`, emphasis.underline),\n ].join(\"\"),\n )\n\n/**\n * `[<markers> body]`, one rule per combination. The body is read again for links and icons\n * but not for emphasis, which does not nest in Cosense (`[* [* x]]` is bold around a link).\n */\nconst emphasisRules: ReadonlyArray<Pattern> = Arr.filterMap(MARKER_SETS, (emphasis) =>\n pipe(\n emphasisScopes(emphasis),\n Option.liftPredicate((scopes) => scopes.length > 0),\n Option.map((scopes) =>\n region({\n scopes,\n begin: re`\\[${markerConditions(emphasis)}(?=${MARKERS}+\\s.*\\])${MARKERS}+\\s+`,\n end: CLOSE_ON_LINE,\n patterns: [include(\"inline-in-emphasis\"), include(\"nested-bracket\")],\n }),\n ),\n ),\n)\n\n/** Anything up to, but not over, a `]]`. */\nconst UNTIL_DOUBLE_CLOSE = re`(?:(?!\\]\\]).)`\n\n/** `[[x]]`: an image when x is one, bold otherwise. Closes on the first `]]`, depth or not. */\nconst strongRules: ReadonlyArray<Pattern> = [\n single(\n re`\\[\\[(?:${IMAGE_URL}|${UNTIL_DOUBLE_CLOSE}*?${IMAGE_EXT}(?:[?#]${UNTIL_DOUBLE_CLOSE}*)?)\\]\\]`,\n { scopes: [SCOPES.image] },\n ),\n single(re`\\[\\[(${UNTIL_DOUBLE_CLOSE}+)\\]\\]`, {\n scopes: [SCOPES.bold],\n captures: { 1: readAs(include(\"inline-in-emphasis\")) },\n }),\n]\n\n/** `[$ x^2]`. Its body is TeX, not notation, and may hold brackets of its own. */\nconst formulaRule: Pattern = region({\n scopes: [SCOPES.formula],\n begin: re`\\[(?=\\$)${CLOSES}`,\n end: CLOSE_ON_LINE,\n patterns: [include(\"bare-bracket\")],\n})\n\n/**\n * Brackets whose body holds no brackets, tried in the parser's order: an icon before a\n * URL before an image path before a project link, and anything left is a page link.\n */\nconst simpleTargetRules = (allowImagePath: boolean): ReadonlyArray<Pattern> =>\n Arr.getSomes([\n Option.some(single(re`\\[[^\\[\\]]+\\.icon(?:\\*\\d+)?\\]`, { scopes: [SCOPES.icon] })),\n // Only URLs, and one of them an image: the image, linking to another URL if there is one.\n Option.some(\n single(re`\\[\\s*(?:${URL}\\s+)*(?:${IMAGE_URL})(?=[\\s\\]])(?:\\s+${URL})*\\s*\\]`, {\n scopes: [SCOPES.image],\n }),\n ),\n // Any other URL makes a link, labelled or not.\n Option.some(single(re`\\[[^\\[\\]]*${URL}[^\\[\\]]*\\]`, { scopes: [SCOPES.externalLink] })),\n // `[a.png]`, but not inside emphasis: there Cosense links to a page of that name.\n onlyIf(allowImagePath, single(re`\\[[^\\[\\]]*${IMAGE_EXT}\\]`, { scopes: [SCOPES.image] })),\n Option.some(single(re`\\[/[^\\[\\]]*\\]`, { scopes: [SCOPES.projectLink] })),\n Option.some(single(re`\\[(?=[^\\[\\]]*[^\\s\\[\\]])[^\\[\\]]+\\]`, { scopes: [SCOPES.link] })),\n ])\n\n/** What follows the bracketed notation in both contexts, in the parser's order. */\nconst unbracketedRules: ReadonlyArray<Pattern> = [\n single(Regex(\"`[^`]*`\"), { scopes: [SCOPES.code] }),\n // `#tag`, only at the start or after a space: a `#` inside a word is just a character.\n single(re`(?:^|(?<=[ \\t\\u3000]))#[^\\s\\[\\]#]+`, { scopes: [SCOPES.hashtag] }),\n // A URL outside brackets is always a link, even to an image.\n single(re`(?i:https?)://[^\\s\\]]+`, { scopes: [SCOPES.externalLink] }),\n]\n\n// --- Lines and blocks -------------------------------------------------------------------\n\n/**\n * A block is its header and every line indented deeper. Deeper is approximated as\n * \"the header's indentation, then more\", which is exact when a page indents one way.\n */\nconst block = (keyword: \"code\" | \"table\", scope: Scope, patterns: ReadonlyArray<Pattern>) =>\n region({\n scopes: [scope],\n begin: re`^(${INDENT})(${Regex(keyword)}:)(.+)$`,\n beginCaptures: {\n 2: scoped(\"keyword.other.block.cosense\"),\n 3: scoped(\"entity.name.section.block.cosense\"),\n },\n end: re`^(?!\\1[ \\t\\u3000])`,\n patterns,\n })\n\n/**\n * The first line is the title, unless the file opens with YAML: then it is the first line\n * after the fence. Nothing on the title line is notation. In `.csnx` a first line that is\n * a component tag is that tag, and the page has no title line.\n */\nconst head = (dialect: DialectInfo): Pattern => {\n const notComponent = dialect.components ? re`(?!${COMPONENT_LINE})` : re``\n return group([\n region({\n begin: re`\\A(?=---[ \\t]*$)`,\n patterns: [include(\"frontmatter\")],\n // Ends on the line after the fence either way; only a title line is captured.\n end: dialect.components ? re`^(?:(?=${COMPONENT_LINE})|(.*)$)` : re`^(.*)$`,\n endCaptures: { 1: scoped(SCOPES.title) },\n // At the very start the fence and the title line both match: the fence wins.\n endPatternLast: true,\n }),\n single(re`\\A${notComponent}.*$`, { scopes: [SCOPES.title] }),\n ])\n}\n\n/** Every other entry the grammar can `include`. The mapped type makes a missing one an error. */\nconst REPOSITORY: { readonly [K in Exclude<RepositoryKey, \"head\">]: Pattern } = {\n frontmatter: region({\n scopes: [SCOPES.frontmatter],\n begin: re`\\A---[ \\t]*$`,\n end: re`^---[ \\t]*$`,\n }),\n \"code-block\": block(\"code\", SCOPES.codeBlock, []),\n \"table-block\": block(\"table\", SCOPES.table, [\n single(re`\\t`, { scopes: [\"punctuation.separator.table-cell.cosense\"] }),\n ]),\n // A component tag owns its line: nothing on it is Cosense notation, and the text after\n // the tag is plain.\n component: region({\n scopes: [\"meta.tag.component.cosense\"],\n begin: re`^(?=${COMPONENT_LINE})`,\n end: re`$`,\n patterns: [include(\"component-tag\")],\n }),\n // The tag itself, read as JSX.\n \"component-tag\": region({\n begin: re`^(\\s*)(</?)([A-Z][A-Za-z0-9_.]*)`,\n beginCaptures: {\n 2: scoped(\"punctuation.definition.tag.begin.cosense\"),\n 3: scoped(SCOPES.component),\n },\n end: re`(/?>)|(?=$)`,\n endCaptures: { 1: scoped(\"punctuation.definition.tag.end.cosense\") },\n patterns: [\n single(re`[A-Za-z_:][A-Za-z0-9_:.\\-]*`, { scopes: [SCOPES.attribute] }),\n single(re`=`, { scopes: [\"punctuation.separator.key-value.cosense\"] }),\n single(re`\"[^\"]*(?:\"|$)|'[^']*(?:'|$)`, { scopes: [SCOPES.attributeValue] }),\n include(\"expression\"),\n ],\n }),\n // `{...}`: a JavaScript expression. Braces nest; a few literals are told apart.\n expression: region({\n scopes: [SCOPES.expression],\n begin: re`\\{`,\n end: re`\\}|(?=$)`,\n beginCaptures: { 0: scoped(\"punctuation.section.embedded.begin.cosense\") },\n endCaptures: { 0: scoped(\"punctuation.section.embedded.end.cosense\") },\n patterns: [\n include(\"expression\"),\n single(re`\"[^\"]*\"|'[^']*'`, { scopes: [\"string.quoted.cosense\"] }),\n single(re`\\b\\d+(?:\\.\\d+)?\\b`, { scopes: [\"constant.numeric.cosense\"] }),\n single(re`\\b(?:true|false|null|undefined)\\b`, { scopes: [\"constant.language.cosense\"] }),\n ],\n }),\n quote: region({\n scopes: [\"markup.quote.cosense\"],\n begin: re`^${INDENT}(>[> ]*)`,\n beginCaptures: { 1: scoped(SCOPES.quote) },\n end: re`$`,\n patterns: [include(\"inline\")],\n }),\n inline: group([\n ...strongRules,\n formulaRule,\n ...emphasisRules,\n ...simpleTargetRules(true),\n ...unbracketedRules,\n ]),\n \"inline-in-emphasis\": group([\n ...strongRules,\n formulaRule,\n ...simpleTargetRules(false),\n ...unbracketedRules,\n ]),\n // A bracket that is no notation, but whose `]` must not close the one around it:\n // in `[* a [] b]` the emphasis runs to the last `]`, as the parser counts depth.\n \"nested-bracket\": region({\n begin: re`\\[`,\n end: CLOSE_ON_LINE,\n patterns: [include(\"inline-in-emphasis\"), include(\"nested-bracket\")],\n }),\n \"bare-bracket\": region({\n begin: re`\\[`,\n end: CLOSE_ON_LINE,\n patterns: [include(\"bare-bracket\")],\n }),\n}\n\n// --- Encoding ---------------------------------------------------------------------------\n\n/** A field only when there is something to put in it: TextMate reads absent and empty alike. */\nconst field = <K extends keyof Rule>(key: K, value: Option.Option<Rule[K]>): Partial<Rule> =>\n Option.match(value, { onNone: () => ({}), onSome: (v) => ({ [key]: v }) })\n\nconst nonEmpty = <A>(items: ReadonlyArray<A>): Option.Option<ReadonlyArray<A>> =>\n Option.liftPredicate(items, (list) => list.length > 0)\n\n/** TextMate gives a rule several scopes by separating them with spaces. */\nconst nameField = (scopes: ReadonlyArray<Scope>): Partial<Rule> =>\n field(\n \"name\",\n Option.map(nonEmpty(scopes), (list) => list.join(\" \")),\n )\n\nconst patternsField = (patterns: ReadonlyArray<Pattern>): Partial<Rule> =>\n field(\"patterns\", Option.map(nonEmpty(patterns), Arr.map(encode)))\n\nconst encodeCaptures = (captures: Captures): Option.Option<Record<string, Rule>> =>\n pipe(\n Object.entries(captures),\n Arr.map(\n ([group, capture]) =>\n [group, { ...nameField(capture.scopes), ...patternsField(capture.patterns) }] as const,\n ),\n nonEmpty,\n Option.map(Rec.fromEntries),\n )\n\nconst encode = (pattern: Pattern): Rule =>\n Match.value(pattern).pipe(\n Match.tag(\"Include\", ({ key }) => ({ include: `#${key}` })),\n Match.tag(\"Group\", ({ patterns }) => patternsField(patterns)),\n Match.tag(\"Single\", (rule) => ({\n ...nameField(rule.scopes),\n match: rule.regex,\n ...field(\"captures\", encodeCaptures(rule.captures)),\n })),\n Match.tag(\"Region\", (rule) => ({\n ...nameField(rule.scopes),\n begin: rule.begin,\n end: rule.end,\n ...field(\"beginCaptures\", encodeCaptures(rule.beginCaptures)),\n ...field(\"endCaptures\", encodeCaptures(rule.endCaptures)),\n ...patternsField(rule.patterns),\n ...field(\"applyEndPatternLast\", onlyIf(rule.endPatternLast, true)),\n })),\n Match.exhaustive,\n )\n\n// --- Dialects ---------------------------------------------------------------------------\n\n/** `.csn`, and `.csnx`, which also reads a line that is one component tag. */\nexport type Dialect = \"cosense\" | \"cosense-x\"\n\ninterface DialectInfo {\n readonly displayName: string\n readonly extension: string\n readonly components: boolean\n}\n\nconst DIALECTS: { readonly [D in Dialect]: DialectInfo } = {\n cosense: { displayName: \"Cosense\", extension: \"csn\", components: false },\n \"cosense-x\": { displayName: \"Cosense X\", extension: \"csnx\", components: true },\n}\n\n/** What a line can be, in the order the parser decides it. */\nconst linePatterns = (dialect: DialectInfo): ReadonlyArray<Pattern> =>\n Arr.getSomes([\n Option.some(include(\"head\")),\n Option.some(include(\"code-block\")),\n Option.some(include(\"table-block\")),\n onlyIf(dialect.components, include(\"component\")),\n Option.some(include(\"quote\")),\n Option.some(include(\"inline\")),\n ])\n\nexport const buildGrammar = (dialect: Dialect): Grammar => {\n const info = DIALECTS[dialect]\n return {\n name: dialect,\n displayName: info.displayName,\n scopeName: `text.${dialect}`,\n aliases: [info.extension],\n fileTypes: [info.extension],\n patterns: Arr.map(linePatterns(info), encode),\n repository: Rec.map({ head: head(info), ...REPOSITORY }, encode),\n }\n}\n","/**\n * TextMate grammars for Cosense notation: `.csn` and, with component lines, `.csnx`.\n *\n * Each export is a Shiki `LanguageRegistration` as it stands, and the same objects are\n * published as `.tmLanguage.json` for editors that read TextMate grammars.\n */\nimport { buildGrammar, type Grammar } from \"./grammar\"\n\nexport type { Dialect, Grammar, Rule } from \"./grammar\"\nexport { type Notation, SCOPES } from \"./scopes\"\n\nexport const cosense: Grammar = buildGrammar(\"cosense\")\n\nexport const cosenseX: Grammar = buildGrammar(\"cosense-x\")\n"],"mappings":"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"}