@open-feishu-card/language-server 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var parser = require('@open-feishu-card/parser');
5
+ var compiler = require('@open-feishu-card/compiler');
6
+ var validator = require('@open-feishu-card/validator');
7
+ var linter = require('@open-feishu-card/linter');
8
+
9
+ function toLspRange(r) {
10
+ return {
11
+ start: { line: r.start.line - 1, character: r.start.column - 1 },
12
+ end: { line: r.end.line - 1, character: r.end.column - 1 }
13
+ };
14
+ }
15
+ function analyze(source) {
16
+ const result = parser.parse(source);
17
+ const vdiags = validator.validateAst(result);
18
+ const ldiags = linter.lintAst(result.ast);
19
+ const diagnostics = [
20
+ ...vdiags.map((d) => ({
21
+ range: toLspRange(d.range),
22
+ message: d.message,
23
+ severity: d.severity === "error" ? 1 : d.severity === "warning" ? 2 : d.severity === "information" ? 3 : 4,
24
+ source: "open-feishu-card",
25
+ code: d.code
26
+ })),
27
+ ...ldiags.map((d) => ({
28
+ range: toLspRange(d.range),
29
+ message: `[${d.rule}] ${d.message}`,
30
+ severity: d.severity === "warning" ? 2 : 3,
31
+ source: "open-feishu-card",
32
+ code: d.rule
33
+ }))
34
+ ];
35
+ return { diagnostics, ast: result.ast };
36
+ }
37
+ var CARD_TOP_LEVEL_KEYS = [
38
+ { label: "schema", kind: 14, detail: '\u5FC5\u586B\uFF0C\u56FA\u5B9A "2.0"' },
39
+ { label: "config", kind: 14, detail: "\u5361\u7247\u914D\u7F6E\uFF08update_multi / enable_forward / style \u7B49\uFF09" },
40
+ { label: "header", kind: 14, detail: "\u5361\u7247\u5934\uFF08title / subtitle / template / ud_icon\uFF09" },
41
+ { label: "body", kind: 14, detail: "\u5361\u7247\u6B63\u6587 { elements: [...] }" },
42
+ { label: "footnote", kind: 14, detail: "\u5361\u7247\u811A\u6CE8" }
43
+ ];
44
+ var ELEMENT_TAGS = [
45
+ "markdown",
46
+ "div",
47
+ "column_set",
48
+ "column",
49
+ "hr",
50
+ "image",
51
+ "button",
52
+ "overflow",
53
+ "select_static",
54
+ "select_multi",
55
+ "input",
56
+ "date_picker",
57
+ "picker_datetime",
58
+ "checker",
59
+ "chart",
60
+ "table",
61
+ "iframe",
62
+ "person",
63
+ "person_list",
64
+ "interactive_container",
65
+ "tag",
66
+ "carousel",
67
+ "grid"
68
+ ];
69
+ var ELEMENT_TAG_COMPLETIONS = ELEMENT_TAGS.map((t) => ({
70
+ label: t,
71
+ kind: 22,
72
+ // Struct
73
+ detail: `\u98DE\u4E66\u5361\u7247\u7EC4\u4EF6 ${t}`,
74
+ insertText: t
75
+ }));
76
+ var COMMON_PROPS = [
77
+ { label: "text_align", kind: 10, detail: "\u6587\u672C\u5BF9\u9F50\uFF1Aleft | center | right" },
78
+ { label: "padding", kind: 10, detail: '\u5185\u8FB9\u8DDD\uFF0C\u5982 "12px" \u6216 "12px 8px"' },
79
+ { label: "margin", kind: 10, detail: "\u5916\u8FB9\u8DDD" },
80
+ { label: "background_style", kind: 10, detail: "\u80CC\u666F\u6837\u5F0F" },
81
+ { label: "behaviors", kind: 10, detail: '\u70B9\u51FB\u884C\u4E3A\u6570\u7EC4 [{type:"open_url", default_url}]' },
82
+ { label: "icon", kind: 10, detail: "\u524D\u7F6E\u56FE\u6807\u5143\u7D20" },
83
+ { label: "text_size", kind: 10, detail: "\u5B57\u53F7\uFF1Anormal | small | large" },
84
+ { label: "text_color", kind: 10, detail: "\u6587\u672C\u989C\u8272\uFF0C\u5982 default / green / red" }
85
+ ];
86
+ function complete(source, offset) {
87
+ const before = source.slice(0, offset);
88
+ const tagCtx = /"tag"\s*:\s*"([^"]*)$/.exec(before);
89
+ if (tagCtx) return ELEMENT_TAG_COMPLETIONS;
90
+ return [...CARD_TOP_LEVEL_KEYS, ...COMMON_PROPS];
91
+ }
92
+ var HOVER_DOCS = {
93
+ schema: '\u5361\u7247 JSON \u7ED3\u6784\u7248\u672C\uFF0C2.0 \u56FA\u5B9A\u4E3A "2.0"\u3002',
94
+ config: "\u5361\u7247\u7EA7\u914D\u7F6E\uFF1Aupdate_multi\uFF08\u591A\u6B21\u66F4\u65B0\uFF09\u3001enable_forward\u3001style\u3001locale \u7B49\u3002",
95
+ header: "\u5361\u7247\u5934\u90E8\uFF1Atitle\u3001subtitle\u3001template\uFF08\u989C\u8272\u6A21\u677F\uFF09\u3001ud_icon\u3001text_tag_list\u3002",
96
+ body: "\u5361\u7247\u6B63\u6587\u5BB9\u5668\uFF0Celements \u4E3A\u7EC4\u4EF6\u6570\u7EC4\u3002",
97
+ elements: "\u7EC4\u4EF6\u6570\u7EC4\uFF0C\u6BCF\u9879\u662F { tag, ...\u5C5E\u6027 }\u3002",
98
+ tag: "\u7EC4\u4EF6\u7C7B\u578B\u6807\u8BC6\uFF0C\u5982 markdown / column_set / button / chart\u3002",
99
+ markdown: "Markdown \u7EC4\u4EF6\uFF1Acontent \u652F\u6301 md \u8BED\u6CD5\u4E0E\u98DE\u4E66\u6269\u5C55\u6807\u7B7E\uFF08<font>\u3001<person> \u7B49\uFF09\u3002",
100
+ column_set: "\u591A\u5217\u5E03\u5C40\u5BB9\u5668\uFF0Cflex_mode \u63A7\u5236 \u5747\u5206/\u5B9A\u5BBD/\u81EA\u9002\u5E94\u3002",
101
+ update_multi: "config \u5B57\u6BB5\uFF1A\u540C\u4E00\u5361\u7247\u662F\u5426\u5141\u8BB8\u591A\u6B21\u66F4\u65B0\u53E0\u52A0\u3002"
102
+ };
103
+ function hover(source, offset) {
104
+ const before = source.slice(0, offset);
105
+ const after = source.slice(offset);
106
+ const m = /"?([a-z_]*)$/.exec(before);
107
+ const rest = /^([a-z_]*)/.exec(after)?.[1] ?? "";
108
+ const key = (m?.[1] ?? "") + rest;
109
+ const doc = key && HOVER_DOCS[key];
110
+ if (!doc) return void 0;
111
+ return { contents: { language: "markdown", value: `**${key}** \u2014 ${doc}` } };
112
+ }
113
+ function quickFixes(source, range) {
114
+ const actions = [];
115
+ const { diagnostics} = analyze(source);
116
+ const inRange = diagnostics.filter(
117
+ (d) => (d.code === "deprecated-prop" || d.code === "unrecognized-key") && d.range.start.line >= range.start.line && d.range.end.line <= range.end.line
118
+ );
119
+ for (const d of inRange) {
120
+ actions.push({
121
+ title: `\u5220\u9664\u5C5E\u6027\uFF08${d.message.split(":")[0]}\uFF09`,
122
+ kind: "quickfix",
123
+ isPreferred: true,
124
+ edits: [
125
+ {
126
+ // 覆盖 key(含引号与冒号前的空白),删除后留空
127
+ range: {
128
+ start: { line: d.range.start.line, character: d.range.start.character - 1 },
129
+ end: { line: d.range.start.line, character: d.range.start.character + `${d.message.split('"')[1] ?? ""}`.length + 1 }
130
+ },
131
+ newText: ""
132
+ }
133
+ ]
134
+ });
135
+ }
136
+ return actions;
137
+ }
138
+ function sanitizeAllAction(source) {
139
+ const { ast, data } = { ast: parser.parse(source).ast, data: parser.parse(source).data };
140
+ if (!ast || data === void 0) return void 0;
141
+ const { stripped, card } = compiler.sanitize(data, { mode: "strip" });
142
+ if (stripped.length === 0) return void 0;
143
+ const lines = source.split("\n");
144
+ const lastLine = lines.length - 1;
145
+ return {
146
+ title: `open-feishu-card: \u51C0\u5316\u5361\u7247\uFF08\u5265\u79BB ${stripped.length} \u4E2A\u975E\u6807\u5C5E\u6027\uFF09`,
147
+ kind: "source.fixAll",
148
+ isPreferred: true,
149
+ edits: [
150
+ {
151
+ range: {
152
+ start: { line: 0, character: 0 },
153
+ end: { line: lastLine, character: lines[lastLine].length }
154
+ },
155
+ newText: JSON.stringify(card, null, 2) + "\n"
156
+ }
157
+ ]
158
+ };
159
+ }
160
+
161
+ // src/rpc.ts
162
+ var Connection = class {
163
+ constructor(input, output) {
164
+ this.input = input;
165
+ this.output = output;
166
+ }
167
+ input;
168
+ output;
169
+ buffer = "";
170
+ nextId = 1;
171
+ pending = /* @__PURE__ */ new Map();
172
+ requestHandlers = /* @__PURE__ */ new Map();
173
+ notificationHandlers = /* @__PURE__ */ new Map();
174
+ onRequest(method, handler) {
175
+ this.requestHandlers.set(method, handler);
176
+ }
177
+ onNotification(method, handler) {
178
+ this.notificationHandlers.set(method, handler);
179
+ }
180
+ sendNotification(method, params) {
181
+ this.write({ jsonrpc: "2.0", method, params });
182
+ }
183
+ sendRequest(method, params) {
184
+ const id = this.nextId++;
185
+ return new Promise((resolve) => {
186
+ this.pending.set(id, { resolve });
187
+ this.write({ jsonrpc: "2.0", id, method, params });
188
+ });
189
+ }
190
+ write(msg) {
191
+ const body = JSON.stringify(msg);
192
+ this.output.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
193
+ \r
194
+ ${body}`);
195
+ }
196
+ listen() {
197
+ this.input.setEncoding("utf8");
198
+ this.input.on("data", (chunk) => {
199
+ this.buffer += chunk;
200
+ this.tryParse();
201
+ });
202
+ this.input.on("close", () => process.exit(0));
203
+ }
204
+ tryParse() {
205
+ for (; ; ) {
206
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
207
+ if (headerEnd < 0) return;
208
+ const header = this.buffer.slice(0, headerEnd);
209
+ const m = /Content-Length:\s*(\d+)/i.exec(header);
210
+ if (!m) {
211
+ this.buffer = this.buffer.slice(headerEnd + 4);
212
+ continue;
213
+ }
214
+ const length = Number(m[1]);
215
+ const bodyStart = headerEnd + 4;
216
+ if (this.buffer.length < bodyStart + length) return;
217
+ const body = this.buffer.slice(bodyStart, bodyStart + length);
218
+ this.buffer = this.buffer.slice(bodyStart + length);
219
+ try {
220
+ this.handleMessage(JSON.parse(body));
221
+ } catch {
222
+ }
223
+ }
224
+ }
225
+ handleMessage(msg) {
226
+ if (typeof msg.method === "string") {
227
+ if (msg.id !== void 0) {
228
+ const handler = this.requestHandlers.get(msg.method);
229
+ Promise.resolve(handler ? handler(msg.params) : null).then((result) => this.write({ jsonrpc: "2.0", id: msg.id, result })).catch(
230
+ (e) => this.write({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } })
231
+ );
232
+ } else {
233
+ this.notificationHandlers.get(msg.method)?.(msg.params);
234
+ }
235
+ } else if (typeof msg.id === "number") {
236
+ this.pending.get(msg.id)?.resolve(msg.result);
237
+ this.pending.delete(msg.id);
238
+ }
239
+ }
240
+ };
241
+ function createConnection(input, output) {
242
+ return new Connection(input, output);
243
+ }
244
+
245
+ // src/server.ts
246
+ var connection = createConnection(process.stdin, process.stdout);
247
+ var documents = /* @__PURE__ */ new Map();
248
+ function pushDiagnostics(doc) {
249
+ const { diagnostics } = analyze(doc.text);
250
+ connection.sendNotification("textDocument/publishDiagnostics", {
251
+ uri: doc.uri,
252
+ version: doc.version,
253
+ diagnostics
254
+ });
255
+ }
256
+ connection.onRequest("initialize", () => ({
257
+ capabilities: {
258
+ textDocumentSync: 1,
259
+ // full
260
+ completionProvider: { triggerCharacters: ['"', ":", " "] },
261
+ hoverProvider: true,
262
+ codeActionProvider: { codeActionKinds: ["quickfix", "source.fixAll"] }
263
+ },
264
+ serverInfo: { name: "open-feishu-card-language-server", version: "0.1.0" }
265
+ }));
266
+ connection.onNotification("textDocument/didOpen", (params) => {
267
+ documents.set(params.textDocument.uri, {
268
+ uri: params.textDocument.uri,
269
+ text: params.textDocument.text,
270
+ version: params.textDocument.version
271
+ });
272
+ pushDiagnostics(documents.get(params.textDocument.uri));
273
+ });
274
+ connection.onNotification("textDocument/didChange", (params) => {
275
+ const doc = documents.get(params.textDocument.uri);
276
+ if (!doc) return;
277
+ doc.text = params.contentChanges?.[params.contentChanges.length - 1]?.text ?? doc.text;
278
+ doc.version = params.textDocument.version;
279
+ pushDiagnostics(doc);
280
+ });
281
+ connection.onNotification("textDocument/didClose", (params) => {
282
+ documents.delete(params.textDocument.uri);
283
+ });
284
+ connection.onRequest("textDocument/completion", (params) => {
285
+ const doc = documents.get(params.textDocument.uri);
286
+ if (!doc) return { isIncomplete: false, items: [] };
287
+ const offset = positionToOffset(doc.text, params.position);
288
+ return { isIncomplete: false, items: complete(doc.text, offset) };
289
+ });
290
+ connection.onRequest("textDocument/hover", (params) => {
291
+ const doc = documents.get(params.textDocument.uri);
292
+ if (!doc) return null;
293
+ const offset = positionToOffset(doc.text, params.position);
294
+ return hover(doc.text, offset) ?? null;
295
+ });
296
+ connection.onRequest("textDocument/codeAction", (params) => {
297
+ const doc = documents.get(params.textDocument.uri);
298
+ if (!doc) return [];
299
+ const fixes = quickFixes(doc.text, params.range);
300
+ const all = sanitizeAllAction(doc.text);
301
+ if (all) fixes.push(all);
302
+ return fixes.map((f) => ({
303
+ title: f.title,
304
+ kind: f.kind,
305
+ isPreferred: f.isPreferred,
306
+ edit: { changes: { [params.textDocument.uri]: f.edits } }
307
+ }));
308
+ });
309
+ connection.onRequest("shutdown", () => null);
310
+ connection.onNotification("exit", () => process.exit(0));
311
+ function positionToOffset(text, pos) {
312
+ let offset = 0;
313
+ let line = 0;
314
+ while (line < pos.line && offset < text.length) {
315
+ if (text[offset] === "\n") line++;
316
+ offset++;
317
+ }
318
+ return offset + pos.character;
319
+ }
320
+ connection.listen();
package/dist/server.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ #!/usr/bin/env node
1
2
  export {};
2
3
  //# sourceMappingURL=server.d.ts.map