refrakt-md 0.5.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.
@@ -0,0 +1,96 @@
1
+ import * as path from 'path';
2
+ import * as vscode from 'vscode';
3
+ import {
4
+ LanguageClient,
5
+ TransportKind,
6
+ type LanguageClientOptions,
7
+ type ServerOptions,
8
+ } from 'vscode-languageclient/node';
9
+ import { RuneInspectorProvider } from './inspector';
10
+
11
+ let client: LanguageClient;
12
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
13
+
14
+ export function activate(context: vscode.ExtensionContext) {
15
+ const serverModule = path.join(__dirname, 'server.js');
16
+
17
+ const serverOptions: ServerOptions = {
18
+ run: { module: serverModule, transport: TransportKind.stdio },
19
+ debug: { module: serverModule, transport: TransportKind.stdio },
20
+ };
21
+
22
+ const clientOptions: LanguageClientOptions = {
23
+ documentSelector: [{ scheme: 'file', language: 'markdown' }],
24
+ synchronize: {
25
+ fileEvents: vscode.workspace.createFileSystemWatcher('**/*.md'),
26
+ },
27
+ };
28
+
29
+ client = new LanguageClient(
30
+ 'refrakt-language-server',
31
+ 'refrakt.md Language Server',
32
+ serverOptions,
33
+ clientOptions,
34
+ );
35
+
36
+ // Rune Inspector tree view
37
+ const inspectorProvider = new RuneInspectorProvider();
38
+ const treeView = vscode.window.createTreeView('refraktRuneInspector', {
39
+ treeDataProvider: inspectorProvider,
40
+ });
41
+ context.subscriptions.push(treeView);
42
+
43
+ // Request rune inspection from the language server
44
+ function requestInspection() {
45
+ const editor = vscode.window.activeTextEditor;
46
+ if (!editor || editor.document.languageId !== 'markdown') {
47
+ inspectorProvider.refresh(null);
48
+ return;
49
+ }
50
+
51
+ const uri = editor.document.uri.toString();
52
+ const position = editor.selection.active;
53
+
54
+ client.sendRequest('refrakt/inspectRune', {
55
+ uri,
56
+ position: { line: position.line, character: position.character },
57
+ }).then(
58
+ (result) => inspectorProvider.refresh(result as any),
59
+ () => inspectorProvider.refresh(null),
60
+ );
61
+ }
62
+
63
+ // Debounced cursor change handler
64
+ function onCursorChange() {
65
+ if (debounceTimer) clearTimeout(debounceTimer);
66
+ debounceTimer = setTimeout(requestInspection, 300);
67
+ }
68
+
69
+ // Wire up after client is ready
70
+ client.start().then(() => {
71
+ context.subscriptions.push(
72
+ vscode.window.onDidChangeTextEditorSelection(onCursorChange),
73
+ vscode.window.onDidChangeActiveTextEditor(onCursorChange),
74
+ );
75
+
76
+ // Initial inspection
77
+ requestInspection();
78
+ });
79
+
80
+ // Commands
81
+ context.subscriptions.push(
82
+ vscode.commands.registerCommand('refrakt.inspectRune', requestInspection),
83
+ vscode.commands.registerCommand('refrakt.copyInspectorNode', (node) => {
84
+ if (node) {
85
+ const json = inspectorProvider.getNodeJson(node);
86
+ vscode.env.clipboard.writeText(json);
87
+ }
88
+ }),
89
+ );
90
+ }
91
+
92
+ export function deactivate(): Thenable<void> | undefined {
93
+ if (debounceTimer) clearTimeout(debounceTimer);
94
+ if (!client) return undefined;
95
+ return client.stop();
96
+ }
@@ -0,0 +1,215 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ interface InspectorResult {
4
+ runeName: string;
5
+ stages: {
6
+ ast: object;
7
+ transform: object;
8
+ serialized: object;
9
+ identity: object | null;
10
+ };
11
+ identityError?: string;
12
+ }
13
+
14
+ export class InspectorNode {
15
+ constructor(
16
+ public readonly label: string,
17
+ public readonly description: string,
18
+ public readonly children: InspectorNode[],
19
+ public readonly data: unknown,
20
+ public readonly icon?: string,
21
+ ) {}
22
+
23
+ get collapsibleState(): vscode.TreeItemCollapsibleState {
24
+ return this.children.length > 0
25
+ ? vscode.TreeItemCollapsibleState.Expanded
26
+ : vscode.TreeItemCollapsibleState.None;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Build tree nodes from a JSON-like object, recursively.
32
+ */
33
+ function buildObjectTree(obj: unknown, parentLabel?: string): InspectorNode[] {
34
+ if (obj === null || obj === undefined) {
35
+ return [new InspectorNode(parentLabel ?? 'null', 'null', [], obj)];
36
+ }
37
+
38
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
39
+ return [new InspectorNode(parentLabel ?? String(obj), String(obj), [], obj)];
40
+ }
41
+
42
+ if (Array.isArray(obj)) {
43
+ return obj.map((item, i) => {
44
+ const label = getNodeLabel(item, i);
45
+ const desc = getNodeDescription(item);
46
+ const children = (typeof item === 'object' && item !== null)
47
+ ? buildObjectChildren(item)
48
+ : [];
49
+ return new InspectorNode(label, desc, children, item);
50
+ });
51
+ }
52
+
53
+ if (typeof obj === 'object') {
54
+ return buildObjectChildren(obj as Record<string, unknown>);
55
+ }
56
+
57
+ return [];
58
+ }
59
+
60
+ function buildObjectChildren(obj: Record<string, unknown>): InspectorNode[] {
61
+ const nodes: InspectorNode[] = [];
62
+
63
+ for (const [key, value] of Object.entries(obj)) {
64
+ if (value === undefined) continue;
65
+
66
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
67
+ nodes.push(new InspectorNode(key, String(value), [], value));
68
+ } else if (Array.isArray(value)) {
69
+ const children = value.map((item, i) => {
70
+ const label = getNodeLabel(item, i);
71
+ const desc = getNodeDescription(item);
72
+ const childChildren = (typeof item === 'object' && item !== null)
73
+ ? buildObjectChildren(item as Record<string, unknown>)
74
+ : [];
75
+ return new InspectorNode(label, desc, childChildren, item);
76
+ });
77
+ nodes.push(new InspectorNode(key, `[${value.length}]`, children, value));
78
+ } else if (typeof value === 'object') {
79
+ const children = buildObjectChildren(value as Record<string, unknown>);
80
+ nodes.push(new InspectorNode(key, '', children, value));
81
+ }
82
+ }
83
+
84
+ return nodes;
85
+ }
86
+
87
+ /**
88
+ * Get a human-readable label for a tree node.
89
+ */
90
+ function getNodeLabel(item: unknown, index: number): string {
91
+ if (item === null || item === undefined) return `[${index}]`;
92
+ if (typeof item !== 'object') return String(item);
93
+
94
+ const obj = item as Record<string, unknown>;
95
+
96
+ // Serialized tag nodes
97
+ if (obj.$$mdtype === 'Tag' && obj.name) {
98
+ const attrs = obj.attributes as Record<string, unknown> | undefined;
99
+ if (attrs?.typeof) return `${obj.name} [typeof="${attrs.typeof}"]`;
100
+ if (attrs?.class) return `${obj.name}.${String(attrs.class).split(' ').join('.')}`;
101
+ if (attrs?.['data-name']) return `${obj.name} [${attrs['data-name']}]`;
102
+ if (attrs?.property) return `${obj.name} [property="${attrs.property}"]`;
103
+ return String(obj.name);
104
+ }
105
+
106
+ // AST child summary
107
+ if (obj.type) {
108
+ if (obj.tag) return `${obj.type}: ${obj.tag}`;
109
+ return String(obj.type);
110
+ }
111
+
112
+ // Text nodes
113
+ if (obj.text) return `"${String(obj.text).slice(0, 40)}"`;
114
+
115
+ return `[${index}]`;
116
+ }
117
+
118
+ /**
119
+ * Get a short description string for a tree node.
120
+ */
121
+ function getNodeDescription(item: unknown): string {
122
+ if (item === null || item === undefined) return '';
123
+ if (typeof item !== 'object') return '';
124
+
125
+ const obj = item as Record<string, unknown>;
126
+ const attrs = obj.attributes as Record<string, unknown> | undefined;
127
+
128
+ if (attrs?.typeof) return String(attrs.typeof);
129
+ if (attrs?.class) return String(attrs.class);
130
+ if (attrs?.['data-name']) return String(attrs['data-name']);
131
+
132
+ return '';
133
+ }
134
+
135
+ const STAGE_ICONS: Record<string, vscode.ThemeIcon> = {
136
+ ast: new vscode.ThemeIcon('symbol-structure'),
137
+ transform: new vscode.ThemeIcon('symbol-method'),
138
+ serialized: new vscode.ThemeIcon('json'),
139
+ identity: new vscode.ThemeIcon('paintcan'),
140
+ };
141
+
142
+ export class RuneInspectorProvider implements vscode.TreeDataProvider<InspectorNode> {
143
+ private _onDidChangeTreeData = new vscode.EventEmitter<InspectorNode | undefined | null>();
144
+ readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
145
+
146
+ private rootNodes: InspectorNode[] = [];
147
+ private currentResult: InspectorResult | null = null;
148
+
149
+ refresh(result: InspectorResult | null): void {
150
+ this.currentResult = result;
151
+ this.rootNodes = result ? this.buildRootNodes(result) : [];
152
+ this._onDidChangeTreeData.fire(undefined);
153
+ }
154
+
155
+ getTreeItem(element: InspectorNode): vscode.TreeItem {
156
+ const item = new vscode.TreeItem(element.label, element.collapsibleState);
157
+ item.description = element.description;
158
+ item.tooltip = new vscode.MarkdownString(
159
+ '```json\n' + JSON.stringify(element.data, null, 2) + '\n```'
160
+ );
161
+ item.contextValue = 'inspectorNode';
162
+
163
+ if (element.icon && STAGE_ICONS[element.icon]) {
164
+ item.iconPath = STAGE_ICONS[element.icon];
165
+ }
166
+
167
+ return item;
168
+ }
169
+
170
+ getChildren(element?: InspectorNode): InspectorNode[] {
171
+ if (!element) {
172
+ if (this.rootNodes.length === 0) {
173
+ return [new InspectorNode('No rune at cursor', 'Move cursor inside a rune tag', [], null)];
174
+ }
175
+ return this.rootNodes;
176
+ }
177
+ return element.children;
178
+ }
179
+
180
+ getNodeJson(node: InspectorNode): string {
181
+ return JSON.stringify(node.data, null, 2);
182
+ }
183
+
184
+ private buildRootNodes(result: InspectorResult): InspectorNode[] {
185
+ const stages: InspectorNode[] = [];
186
+
187
+ // AST stage
188
+ const astChildren = buildObjectChildren(result.stages.ast as Record<string, unknown>);
189
+ const astNode = new InspectorNode('AST', result.runeName, astChildren, result.stages.ast, 'ast');
190
+ stages.push(astNode);
191
+
192
+ // Transform stage
193
+ const transformChildren = buildObjectChildren(result.stages.transform as Record<string, unknown>);
194
+ const transformNode = new InspectorNode('Transform', '', transformChildren, result.stages.transform, 'transform');
195
+ stages.push(transformNode);
196
+
197
+ // Serialized stage
198
+ const serializedChildren = buildObjectChildren(result.stages.serialized as Record<string, unknown>);
199
+ const serializedNode = new InspectorNode('Serialized', '', serializedChildren, result.stages.serialized, 'serialized');
200
+ stages.push(serializedNode);
201
+
202
+ // Identity Transform stage
203
+ if (result.stages.identity) {
204
+ const identityChildren = buildObjectChildren(result.stages.identity as Record<string, unknown>);
205
+ const identityNode = new InspectorNode('Identity Transform', '', identityChildren, result.stages.identity, 'identity');
206
+ stages.push(identityNode);
207
+ } else {
208
+ const errorMsg = result.identityError ?? 'Theme not available';
209
+ const identityNode = new InspectorNode('Identity Transform', errorMsg, [], null, 'identity');
210
+ stages.push(identityNode);
211
+ }
212
+
213
+ return stages;
214
+ }
215
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "scopeName": "text.html.markdown.refrakt",
4
+ "injectionSelector": "L:text.html.markdown, L:meta.paragraph.markdown, L:markup.list.markdown, L:markup.quote.markdown",
5
+ "patterns": [
6
+ { "include": "#rune-closing-tag" },
7
+ { "include": "#rune-tag" }
8
+ ],
9
+ "repository": {
10
+ "rune-closing-tag": {
11
+ "name": "meta.tag.rune.closing.refrakt",
12
+ "match": "(\\{%)\\s*(/)([a-zA-Z][a-zA-Z0-9-]*)\\s*(%\\})",
13
+ "captures": {
14
+ "1": { "name": "punctuation.definition.tag.begin.rune.refrakt" },
15
+ "2": { "name": "punctuation.definition.tag.close.rune.refrakt" },
16
+ "3": { "name": "entity.name.tag.rune.refrakt" },
17
+ "4": { "name": "punctuation.definition.tag.end.rune.refrakt" }
18
+ }
19
+ },
20
+ "rune-tag": {
21
+ "name": "meta.tag.rune.refrakt",
22
+ "begin": "(\\{%)\\s*([a-zA-Z][a-zA-Z0-9-]*)",
23
+ "end": "(/?)(%\\})",
24
+ "beginCaptures": {
25
+ "1": { "name": "punctuation.definition.tag.begin.rune.refrakt" },
26
+ "2": { "name": "entity.name.tag.rune.refrakt" }
27
+ },
28
+ "endCaptures": {
29
+ "1": { "name": "punctuation.definition.tag.self-close.rune.refrakt" },
30
+ "2": { "name": "punctuation.definition.tag.end.rune.refrakt" }
31
+ },
32
+ "patterns": [
33
+ { "include": "#rune-attributes" }
34
+ ]
35
+ },
36
+ "rune-attributes": {
37
+ "patterns": [
38
+ { "include": "#rune-attribute-quoted" },
39
+ { "include": "#rune-attribute-boolean" },
40
+ { "include": "#rune-attribute-number" },
41
+ { "include": "#rune-attribute-unquoted" }
42
+ ]
43
+ },
44
+ "rune-attribute-quoted": {
45
+ "match": "([a-zA-Z][a-zA-Z0-9-]*)(=)(\"[^\"]*\")",
46
+ "captures": {
47
+ "1": { "name": "entity.other.attribute-name.rune.refrakt" },
48
+ "2": { "name": "punctuation.separator.key-value.rune.refrakt" },
49
+ "3": { "name": "string.quoted.double.rune.refrakt" }
50
+ }
51
+ },
52
+ "rune-attribute-boolean": {
53
+ "match": "([a-zA-Z][a-zA-Z0-9-]*)(=)(true|false)(?=\\s|%|/)",
54
+ "captures": {
55
+ "1": { "name": "entity.other.attribute-name.rune.refrakt" },
56
+ "2": { "name": "punctuation.separator.key-value.rune.refrakt" },
57
+ "3": { "name": "constant.language.boolean.rune.refrakt" }
58
+ }
59
+ },
60
+ "rune-attribute-number": {
61
+ "match": "([a-zA-Z][a-zA-Z0-9-]*)(=)([0-9]+(?:\\.[0-9]+)?)(?=\\s|%|/)",
62
+ "captures": {
63
+ "1": { "name": "entity.other.attribute-name.rune.refrakt" },
64
+ "2": { "name": "punctuation.separator.key-value.rune.refrakt" },
65
+ "3": { "name": "constant.numeric.rune.refrakt" }
66
+ }
67
+ },
68
+ "rune-attribute-unquoted": {
69
+ "match": "([a-zA-Z][a-zA-Z0-9-]*)(=)([^\\s\"%}/]+)",
70
+ "captures": {
71
+ "1": { "name": "entity.other.attribute-name.rune.refrakt" },
72
+ "2": { "name": "punctuation.separator.key-value.rune.refrakt" },
73
+ "3": { "name": "string.unquoted.rune.refrakt" }
74
+ }
75
+ }
76
+ }
77
+ }