@actualwave/codemirror-lang-embed-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +134 -0
  2. package/package.json +20 -0
package/index.js ADDED
@@ -0,0 +1,134 @@
1
+ const { parseMixed } = require('@lezer/common');
2
+ const { LanguageSupport } = require('@codemirror/language');
3
+
4
+ // A registry of (tagPath -> Language) matchers, tried in registration order.
5
+ // `tagPath` is the array produced by readIdentifierPath below, e.g. ['sql'],
6
+ // ['gql'], ['styled', 'View'] (for `styled.View` tags), or ['styled'] (for
7
+ // `styled(Component)` — the call collapses to its callee's own path).
8
+ function createTagRegistry() {
9
+ const entries = [];
10
+ return {
11
+ register(matcher, language) {
12
+ entries.push({ matcher, language });
13
+ },
14
+ resolve(tagPath) {
15
+ for (const { matcher, language } of entries) {
16
+ if (matcher(tagPath)) return language;
17
+ }
18
+ return null;
19
+ },
20
+ };
21
+ }
22
+
23
+ // Walks a tag expression's SyntaxNode down to a flat identifier path.
24
+ // Only VariableName / MemberExpression(.prop) / CallExpression(callee) shapes
25
+ // are recognized — anything else (bracket access, computed member, etc.)
26
+ // is rejected by returning null, since tag-name matching is purely textual
27
+ // (see SINGLE_FILE_PROTOTYPES.md open question 2).
28
+ function readIdentifierPath(node, input) {
29
+ switch (node.name) {
30
+ case 'VariableName':
31
+ return [input.read(node.from, node.to)];
32
+ case 'MemberExpression': {
33
+ const object = node.firstChild;
34
+ const property = node.lastChild;
35
+ if (!object || !property) return null;
36
+ if (property.name !== 'PropertyName' && property.name !== 'PrivatePropertyName') {
37
+ // bracket-access form (styled['View']) — not a supported tag shape
38
+ return null;
39
+ }
40
+ const objectPath = readIdentifierPath(object, input);
41
+ if (!objectPath) return null;
42
+ return [...objectPath, input.read(property.from, property.to)];
43
+ }
44
+ case 'CallExpression': {
45
+ // styled(Component)`...` collapses to the callee's own path, so
46
+ // `styled(Foo)` and bare `styled` both match a matcher for ['styled'].
47
+ const callee = node.firstChild;
48
+ return callee ? readIdentifierPath(callee, input) : null;
49
+ }
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // Given a `TemplateString` node (materialized SyntaxNode, i.e. already
56
+ // `.node`), finds its tag identifier path if it's the template of a
57
+ // TaggedTemplateExpression, or null otherwise (plain, untagged templates).
58
+ function getTaggedTemplateTagPath(templateStringNode, input) {
59
+ const tagged = templateStringNode.parent;
60
+ if (!tagged || tagged.name !== 'TaggedTemplateExpression') return null;
61
+ // SyntaxNode accessors (.lastChild etc.) don't return reference-stable
62
+ // objects — compare by range instead of `===` to check this is really
63
+ // the tagged expression's own template (not, in principle, some other
64
+ // child at the same tree position).
65
+ const last = tagged.lastChild;
66
+ if (!last || last.from !== templateStringNode.from || last.to !== templateStringNode.to) {
67
+ return null;
68
+ }
69
+ const tagNode = tagged.firstChild;
70
+ if (!tagNode) return null;
71
+ return readIdentifierPath(tagNode, input);
72
+ }
73
+
74
+ // A `TemplateString` node's own range spans the opening and closing backtick
75
+ // characters, and any `${...}` interpolations appear as `Interpolation` child
76
+ // nodes interleaved with the literal text. Nested DSL parsers only understand
77
+ // the literal text, so the overlay must exclude both the delimiters and any
78
+ // interpolation ranges — otherwise the nested parser sees stray backtick/`${`
79
+ // characters it doesn't recognize and produces spurious error nodes.
80
+ function templateContentRanges(templateStringNode) {
81
+ const ranges = [];
82
+ let pos = templateStringNode.from + 1; // skip opening backtick
83
+ for (let child = templateStringNode.firstChild; child; child = child.nextSibling) {
84
+ if (child.name !== 'Interpolation') continue;
85
+ if (child.from > pos) ranges.push({ from: pos, to: child.from });
86
+ pos = child.to;
87
+ }
88
+ const end = templateStringNode.to - 1; // exclude closing backtick
89
+ if (end > pos) ranges.push({ from: pos, to: end });
90
+ return ranges;
91
+ }
92
+
93
+ // Wraps a JS/TSX LanguageSupport (as returned by @codemirror/lang-javascript's
94
+ // javascript()) so that TaggedTemplateExpression templates whose tag matches
95
+ // an entry in `registry` get parsed by that entry's nested Language.
96
+ function embedTaggedTemplates(languageSupport, registry) {
97
+ const wrappedLanguage = languageSupport.language.configure({
98
+ wrap: parseMixed((node, input) => {
99
+ if (node.name !== 'TemplateString') return null;
100
+ const tagPath = getTaggedTemplateTagPath(node.node, input);
101
+ if (!tagPath) return null;
102
+ const language = registry.resolve(tagPath);
103
+ if (!language) return null;
104
+ return { parser: language.parser, overlay: templateContentRanges(node.node) };
105
+ }),
106
+ });
107
+ return new LanguageSupport(wrappedLanguage, languageSupport.support);
108
+ }
109
+
110
+ // Convenience matcher factory for the common case: a bare identifier tag
111
+ // (e.g. `sql`, `gql`, `css`) with no member/call wrapping.
112
+ function matchTagName(name) {
113
+ return (tagPath) => tagPath.length === 1 && tagPath[0] === name;
114
+ }
115
+
116
+ // Adapts a CM6 `Text` (`state.doc`) to the minimal Lezer `Input` shape
117
+ // (`.read(from, to)`) that readIdentifierPath/getTaggedTemplateTagPath need.
118
+ // Inside a `parseMixed` callback (embedTaggedTemplates above), Lezer already
119
+ // supplies a real Input, so this isn't needed there — it's for call sites
120
+ // walking an already-built tree directly against `state.doc`, e.g. a
121
+ // CompletionSource or a decoration ViewPlugin (see embed-tailwind).
122
+ function docAsInput(doc) {
123
+ return { read: (from, to) => doc.sliceString(from, to) };
124
+ }
125
+
126
+ module.exports = {
127
+ createTagRegistry,
128
+ readIdentifierPath,
129
+ getTaggedTemplateTagPath,
130
+ templateContentRanges,
131
+ embedTaggedTemplates,
132
+ matchTagName,
133
+ docAsInput,
134
+ };
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@actualwave/codemirror-lang-embed-core",
3
+ "version": "0.1.0",
4
+ "description": "Shared parseMixed tag-embedding machinery for tagged-template DSL highlighting (sql/gql/css/etc inside JS/TSX)",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/burdiuz/codemorror-lang-modules.git",
10
+ "directory": "packages/embed-core"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/burdiuz/codemorror-lang-modules/issues"
14
+ },
15
+ "homepage": "https://github.com/burdiuz/codemorror-lang-modules/tree/main/packages/embed-core",
16
+ "peerDependencies": {
17
+ "@codemirror/language": "^6.0.0",
18
+ "@lezer/common": "^1.0.0"
19
+ }
20
+ }