Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 :
3 : import { ConnectorType } from '../../connections/connector_info.js';
4 : import { SNIPPET_ENGINE_CONNECTORS } from './snippet.js';
5 :
6 : /// One snippet file, `<engine>/<category>/<name>.sql`, a leading comment describes the snippet.
7 : /// The files of the engines with the same category and name are the variants of one snippet.
8 : export interface SnippetFile {
9 : /// The path of the file below the snippet folder
10 : path: string;
11 : /// The id of the snippet, `<category>/<name>`
12 : id: string;
13 : /// The category of the snippet, the folder of the file
14 : category: string;
15 : /// The connectors that the body of the file runs on, those of its engine
16 : connectors: readonly ConnectorType[];
17 : /// The text of the file, with its description
18 : body: string;
19 : }
20 :
21 1 : const encoder = new TextEncoder();
22 :
23 : /// The comments of a snippet file
24 : interface SnippetComments {
25 : /// The comment tag blocks, a snippet has none
26 : tags: dashql.CommentTag[];
27 : /// The ordinary comment blocks that don't lead the file, a snippet has none
28 : inner: number;
29 : /// The number of statements
30 : statements: number;
31 : }
32 :
33 : /// Read the comment blocks of a text with the DashQL parser
34 1 : function readComments(core: dashql.DashQL, text: string, bytes: Uint8Array): SnippetComments {
35 1 : const catalog = core.createCatalog();
36 1 : const script = core.createScript(catalog);
37 1 : try {
38 1 : script.insertTextAt(0, text);
39 1 : script.parse();
40 1 : const parsed = script.getParsed();
41 1 : const reader = parsed.read();
42 : // The comments before the first statement describe the snippet
43 1 : const statements = reader.statementsLength();
44 1 : const bodyBegin = statements > 0 ? reader.statements(0)!.statementSpan()!.offset() : bytes.length;
45 1 : const block = new dashql.buffers.parser.CommentBlock();
46 1 : let inner = 0;
47 1 : for (let i = 0; i < reader.commentBlocksLength(); ++i) {
48 1 : reader.commentBlocks(i, block);
49 1 : if (block.blockType() === dashql.buffers.parser.CommentBlockType.COMMENT && block.textSpan()!.offset() >= bodyBegin) ++inner;
50 1 : }
51 1 : return { tags: dashql.readCommentTags(reader, bytes), inner, statements };
52 1 : } finally {
53 1 : script.destroy();
54 1 : catalog.destroy();
55 1 : }
56 1 : }
57 :
58 : /// Parse a snippet file, throws if the path or the comments are invalid
59 1 : export function parseSnippetFile(core: dashql.DashQL, path: string, raw: string): SnippetFile {
60 1 : const pathMatch = /^([a-z0-9_-]+)\/([a-z0-9_-]+)\/([a-z0-9_-]+)\.sql$/.exec(path);
61 1 : if (pathMatch == null) {
62 1 : throw new Error(`invalid snippet path: ${path}`);
63 : }
64 1 : const [, engine, category, name] = pathMatch;
65 1 : const engineConnectors = SNIPPET_ENGINE_CONNECTORS.get(engine);
66 1 : if (engineConnectors === undefined) {
67 1 : throw new Error(`unknown engine folder "${engine}" of snippet ${path}`);
68 : }
69 :
70 1 : const bytes = encoder.encode(raw);
71 1 : const { tags, inner, statements } = readComments(core, raw, bytes);
72 : // The formatter of the preview moves comments within a statement, explanations belong into the description
73 1 : if (inner > 0) {
74 1 : throw new Error(`snippet ${path} has comments after its description, move them into the description`);
75 : }
76 1 : if (tags.length > 0) {
77 1 : throw new Error(`snippet ${path} has a @${tags[0].name} tag, snippets only have a description`);
78 : }
79 : // The description stays in the applied text, a snippet needs statements besides it
80 1 : if (statements === 0) {
81 1 : throw new Error(`snippet ${path} has no body`);
82 : }
83 1 : return {
84 1 : path,
85 1 : id: `${category}/${name}`,
86 1 : category,
87 1 : connectors: engineConnectors,
88 1 : body: raw.trim(),
89 1 : };
90 1 : }
|