@udx/mq 0.1.1

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,151 @@
1
+ /**
2
+ * Operations index for Markdown Query
3
+ *
4
+ * Entry point for all query and transform operations
5
+ */
6
+
7
+ import { visit } from 'unist-util-visit';
8
+ import { toMarkdown } from 'mdast-util-to-markdown';
9
+ import _ from 'lodash';
10
+
11
+ // Import all operations
12
+ import {
13
+ extractHeadings,
14
+ extractCodeBlocks,
15
+ extractLinks,
16
+ generateToc,
17
+ extractSections
18
+ } from './extractors.js';
19
+
20
+ import {
21
+ analyzeDocument,
22
+ showDocumentStructure,
23
+ countDocumentElements
24
+ } from './analysis.js';
25
+
26
+ import {
27
+ makeCodeBlocksCollapsible,
28
+ makeDescriptiveToc,
29
+ addCrossLinks,
30
+ fixHeadingHierarchy,
31
+ moveSection,
32
+ updateTOCNumbers,
33
+ insertTOC,
34
+ convertHTMLToMarkdown
35
+ } from './transformers.js';
36
+
37
+ /**
38
+ * Query operations map
39
+ *
40
+ * Maps operation names to their implementing functions
41
+ */
42
+ const queryOperations = {
43
+ headings: extractHeadings,
44
+ codeblocks: extractCodeBlocks,
45
+ links: extractLinks,
46
+ toc: generateToc,
47
+ sections: extractSections,
48
+ structure: showDocumentStructure,
49
+ analyze: analyzeDocument,
50
+ count: countDocumentElements
51
+ };
52
+
53
+ /**
54
+ * Transform operations map
55
+ *
56
+ * Maps transform operation names to their implementing functions
57
+ */
58
+ const transformOperations = {
59
+ 'collapsible-code': makeCodeBlocksCollapsible,
60
+ 'descriptive-toc': makeDescriptiveToc,
61
+ 'add-cross-links': addCrossLinks,
62
+ 'fix-headings': fixHeadingHierarchy,
63
+ 'move-section': moveSection,
64
+ 'update-toc-numbers': updateTOCNumbers,
65
+ 'insert-toc': insertTOC,
66
+ 'html-to-md': convertHTMLToMarkdown
67
+ };
68
+
69
+ /**
70
+ * Get operation by name
71
+ *
72
+ * Retrieves a query or transform operation by its name
73
+ *
74
+ * @param {string} name - Operation name
75
+ * @param {string} type - Operation type (query or transform)
76
+ * @returns {Function} Operation function
77
+ */
78
+ function getOperation(name, type = 'query') {
79
+ const operations = type === 'query' ? queryOperations : transformOperations;
80
+ return operations[name];
81
+ }
82
+
83
+ /**
84
+ * Register new operation
85
+ *
86
+ * Adds a new operation to the operations map
87
+ *
88
+ * @param {string} name - Operation name
89
+ * @param {Function} fn - Operation implementation
90
+ * @param {string} type - Operation type (query or transform)
91
+ */
92
+ function registerOperation(name, fn, type = 'query') {
93
+ if (type === 'query') {
94
+ queryOperations[name] = fn;
95
+ } else {
96
+ transformOperations[name] = fn;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * List all available operations
102
+ *
103
+ * @param {string} type - Operation type (query, transform, or all)
104
+ * @returns {Object} Map of operation names to descriptions
105
+ */
106
+ function listOperations(type = 'all') {
107
+ const operations = {};
108
+
109
+ if (type === 'query' || type === 'all') {
110
+ Object.keys(queryOperations).forEach(name => {
111
+ operations[name] = `Query operation: ${name}`;
112
+ });
113
+ }
114
+
115
+ if (type === 'transform' || type === 'all') {
116
+ Object.keys(transformOperations).forEach(name => {
117
+ operations[name] = `Transform operation: ${name}`;
118
+ });
119
+ }
120
+
121
+ return operations;
122
+ }
123
+
124
+ /**
125
+ * Execute operation by name
126
+ *
127
+ * @param {string} name - Operation name
128
+ * @param {Object} ast - Markdown AST
129
+ * @param {string} selector - Query selector
130
+ * @param {Array} args - Additional arguments for the operation
131
+ * @param {string} type - Operation type (query or transform)
132
+ * @returns {*} Operation result
133
+ */
134
+ function executeOperation(name, ast, selector, args = [], type = 'query') {
135
+ const operation = getOperation(name, type);
136
+
137
+ if (!operation) {
138
+ throw new Error(`Operation "${name}" not found`);
139
+ }
140
+
141
+ return operation(ast, selector, ...args);
142
+ }
143
+
144
+ export {
145
+ queryOperations,
146
+ transformOperations,
147
+ getOperation,
148
+ registerOperation,
149
+ listOperations,
150
+ executeOperation
151
+ };
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Transformer operations for Markdown Query
3
+ *
4
+ * Functions for transforming markdown content in various ways
5
+ */
6
+
7
+ import { toString } from 'mdast-util-to-string';
8
+ import { visit } from 'unist-util-visit';
9
+ import { toMarkdown } from 'mdast-util-to-markdown';
10
+ import _ from 'lodash';
11
+ import { extractHeadings, extractSections } from './extractors.js';
12
+
13
+ /**
14
+ * Make code blocks collapsible
15
+ *
16
+ * Transforms code blocks in markdown to use HTML details/summary tags for collapsible sections
17
+ *
18
+ * @param {Object} ast - Markdown AST
19
+ * @param {string} selector - Optional selector (not used in this function)
20
+ * @returns {string} Transformed markdown with collapsible code blocks
21
+ */
22
+ function makeCodeBlocksCollapsible(ast, selector) {
23
+ let markdown = toMarkdown(ast);
24
+
25
+ // Find code blocks with regex
26
+ const codeBlockRegex = /```([a-zA-Z]*)\n([\s\S]*?)```/g;
27
+
28
+ // Replace each code block with a collapsible version
29
+ markdown = markdown.replace(codeBlockRegex, (match, language, code) => {
30
+ // Create a summary based on the language
31
+ const summary = language ? `${language} code` : 'Code block';
32
+
33
+ // Create the collapsible HTML
34
+ return `<details>
35
+ <summary>${summary}</summary>
36
+
37
+ \`\`\`${language}
38
+ ${code}
39
+ \`\`\`
40
+ </details>`;
41
+ });
42
+
43
+ return markdown;
44
+ }
45
+
46
+ /**
47
+ * Make TOC descriptive
48
+ *
49
+ * Enhances the table of contents by adding descriptions from the first sentence
50
+ * of paragraphs following each heading
51
+ *
52
+ * @param {Object} ast - Markdown AST
53
+ * @param {string} selector - Optional selector (not used in this function)
54
+ * @returns {Object} Modified AST with heading descriptions
55
+ */
56
+ function makeDescriptiveToc(ast, selector) {
57
+ visit(ast, 'heading', (node) => {
58
+ // Find the next paragraph for description
59
+ const headingIndex = ast.children.indexOf(node);
60
+ if (headingIndex >= 0 && headingIndex < ast.children.length - 1) {
61
+ const nextNode = ast.children[headingIndex + 1];
62
+ if (nextNode.type === 'paragraph') {
63
+ // Add description to heading data
64
+ node.data = node.data || {};
65
+ node.data.description = toString(nextNode).split('.')[0]; // First sentence
66
+ }
67
+ }
68
+ });
69
+
70
+ return ast;
71
+ }
72
+
73
+ /**
74
+ * Add cross-links section
75
+ *
76
+ * Adds a "Related Documents" section with links to other markdown documents
77
+ * after the table of contents section
78
+ *
79
+ * @param {Object} ast - Markdown AST
80
+ * @param {string} selector - Optional selector (not used in this function)
81
+ * @param {Array} args - Array of document paths to link to
82
+ * @returns {Object} Modified AST with cross-links section
83
+ */
84
+ function addCrossLinks(ast, selector, args) {
85
+ const docs = args || [];
86
+
87
+ // Find the TOC section to add after
88
+ let tocIndex = -1;
89
+ visit(ast, 'heading', (node, index) => {
90
+ if (toString(node).toLowerCase() === 'table of contents') {
91
+ tocIndex = index;
92
+ }
93
+ });
94
+
95
+ if (tocIndex >= 0) {
96
+ // Find the next heading after TOC
97
+ let nextHeadingIndex = ast.children.length;
98
+ for (let i = tocIndex + 1; i < ast.children.length; i++) {
99
+ if (ast.children[i].type === 'heading') {
100
+ nextHeadingIndex = i;
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Create cross-links section
106
+ const crossLinksHeading = {
107
+ type: 'heading',
108
+ depth: 3,
109
+ children: [{ type: 'text', value: 'Related Documents' }]
110
+ };
111
+
112
+ const crossLinksList = {
113
+ type: 'list',
114
+ ordered: false,
115
+ children: docs.map(doc => ({
116
+ type: 'listItem',
117
+ children: [{
118
+ type: 'paragraph',
119
+ children: [{
120
+ type: 'link',
121
+ url: doc,
122
+ children: [{
123
+ type: 'text',
124
+ value: doc.replace('.md', '').split('-').map(
125
+ word => word.charAt(0).toUpperCase() + word.slice(1)
126
+ ).join(' ')
127
+ }]
128
+ }]
129
+ }]
130
+ }))
131
+ };
132
+
133
+ // Insert the cross-links section
134
+ ast.children.splice(nextHeadingIndex, 0, crossLinksHeading, crossLinksList);
135
+ }
136
+
137
+ return ast;
138
+ }
139
+
140
+ /**
141
+ * Fix heading hierarchy
142
+ *
143
+ * Ensures that heading levels in a document follow a proper hierarchy without skipping levels
144
+ *
145
+ * @param {Object} ast - Markdown AST
146
+ * @param {string} selector - Optional selector (not used in this function)
147
+ * @returns {Object} Modified AST with corrected heading hierarchy
148
+ */
149
+ function fixHeadingHierarchy(ast, selector) {
150
+ let lastLevel = 0;
151
+ visit(ast, 'heading', (node) => {
152
+ if (node.depth > lastLevel + 1 && lastLevel > 0) {
153
+ node.depth = lastLevel + 1;
154
+ }
155
+ lastLevel = node.depth;
156
+ });
157
+
158
+ return ast;
159
+ }
160
+
161
+ /**
162
+ * Move a section from one position to another
163
+ *
164
+ * Moves a section identified by its title to a new position in the document
165
+ *
166
+ * @param {Object} ast - Markdown AST
167
+ * @param {string} fromTitle - Title of the section to move
168
+ * @param {number} toPosition - Position to move the section to
169
+ * @returns {Object} New AST with the section moved
170
+ */
171
+ function moveSection(ast, fromTitle, toPosition) {
172
+ const sections = extractSections(ast);
173
+ const fromIndex = sections.findIndex(section => section.title === fromTitle);
174
+
175
+ if (fromIndex === -1) {
176
+ throw new Error(`Section "${fromTitle}" not found`);
177
+ }
178
+
179
+ const section = sections.splice(fromIndex, 1)[0];
180
+ sections.splice(toPosition, 0, section);
181
+
182
+ // Rebuild AST from sections
183
+ const newAst = {
184
+ type: 'root',
185
+ children: []
186
+ };
187
+
188
+ sections.forEach(section => {
189
+ newAst.children.push(...section.content);
190
+ });
191
+
192
+ return newAst;
193
+ }
194
+
195
+ /**
196
+ * Update TOC numbers
197
+ *
198
+ * Updates the table of contents with section numbers for better navigation
199
+ *
200
+ * @param {Object} ast - Markdown AST
201
+ * @param {string} selector - Optional selector (not used in this function)
202
+ * @returns {Object} Modified AST with numbered TOC
203
+ */
204
+ function updateTOCNumbers(ast, selector) {
205
+ // Find the TOC section
206
+ let tocIndex = -1;
207
+ visit(ast, 'heading', (node, index) => {
208
+ if (toString(node).toLowerCase() === 'table of contents') {
209
+ tocIndex = index;
210
+ }
211
+ });
212
+
213
+ if (tocIndex >= 0) {
214
+ // Find the list that follows the TOC heading
215
+ let listIndex = -1;
216
+ for (let i = tocIndex + 1; i < ast.children.length; i++) {
217
+ if (ast.children[i].type === 'list') {
218
+ listIndex = i;
219
+ break;
220
+ }
221
+ }
222
+
223
+ if (listIndex >= 0) {
224
+ // Update the list items with numbers
225
+ const list = ast.children[listIndex];
226
+ let currentNumber = 1;
227
+
228
+ visit(list, 'listItem', (node) => {
229
+ // Add number to the first paragraph in the list item
230
+ if (node.children && node.children.length > 0 && node.children[0].type === 'paragraph') {
231
+ const paragraph = node.children[0];
232
+ const firstChild = paragraph.children[0];
233
+
234
+ if (firstChild.type === 'text') {
235
+ firstChild.value = `${currentNumber}. ${firstChild.value}`;
236
+ currentNumber++;
237
+ } else if (firstChild.type === 'link') {
238
+ // Insert a text node with the number before the link
239
+ paragraph.children.unshift({
240
+ type: 'text',
241
+ value: `${currentNumber}. `
242
+ });
243
+ currentNumber++;
244
+ }
245
+ }
246
+ });
247
+ }
248
+ }
249
+
250
+ return ast;
251
+ }
252
+
253
+ /**
254
+ * Insert TOC at the beginning of document
255
+ *
256
+ * Automatically generates and inserts a table of contents at the beginning
257
+ * of the document, after the title heading
258
+ *
259
+ * @param {Object} ast - Markdown AST
260
+ * @param {string} selector - Optional selector (not used in this function)
261
+ * @param {Object} options - Options for TOC generation
262
+ * @returns {Object} Modified AST with TOC inserted
263
+ */
264
+ function insertTOC(ast, selector, options = {}) {
265
+ // Generate TOC content
266
+ const headings = [];
267
+ visit(ast, 'heading', (node) => {
268
+ // Skip level 1 headings (title) and TOC heading itself
269
+ if (node.depth > 1 && toString(node).toLowerCase() !== 'table of contents') {
270
+ headings.push({
271
+ level: node.depth,
272
+ text: toString(node),
273
+ anchor: toString(node).toLowerCase().replace(/[^\w]+/g, '-')
274
+ });
275
+ }
276
+ });
277
+
278
+ // Create TOC heading
279
+ const tocHeading = {
280
+ type: 'heading',
281
+ depth: 2,
282
+ children: [{ type: 'text', value: 'Table of Contents' }]
283
+ };
284
+
285
+ // Create TOC list
286
+ const tocItems = headings.map(heading => {
287
+ const indent = ' '.repeat(heading.level - 2);
288
+ return {
289
+ type: 'listItem',
290
+ children: [{
291
+ type: 'paragraph',
292
+ children: [{
293
+ type: 'link',
294
+ url: `#${heading.anchor}`,
295
+ children: [{ type: 'text', value: heading.text }]
296
+ }]
297
+ }]
298
+ };
299
+ });
300
+
301
+ const tocList = {
302
+ type: 'list',
303
+ ordered: options.ordered || false,
304
+ children: tocItems
305
+ };
306
+
307
+ // Find position to insert TOC (after title)
308
+ let insertPosition = 0;
309
+ if (ast.children.length > 0 && ast.children[0].type === 'heading' && ast.children[0].depth === 1) {
310
+ insertPosition = 1;
311
+ }
312
+
313
+ // Insert TOC
314
+ ast.children.splice(insertPosition, 0, tocHeading, tocList);
315
+
316
+ return ast;
317
+ }
318
+
319
+ /**
320
+ * Convert HTML to Markdown
321
+ *
322
+ * Transforms HTML content to markdown format
323
+ *
324
+ * @param {Object} ast - Markdown AST (not used in this function)
325
+ * @param {string} selector - Optional selector (not used in this function)
326
+ * @param {string} html - HTML content to convert
327
+ * @returns {string} Converted markdown content
328
+ */
329
+ function convertHTMLToMarkdown(ast, selector, html) {
330
+ if (!html) {
331
+ return '';
332
+ }
333
+
334
+ // Simple HTML to markdown conversion
335
+ let markdown = html;
336
+
337
+ // We'll handle HTML entities separately to prevent them from being treated as tags
338
+ const containsEntities = /&lt;|&gt;|&amp;|&quot;|&apos;/.test(markdown);
339
+
340
+ // Convert headings
341
+ markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
342
+ markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
343
+ markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
344
+ markdown = markdown.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n');
345
+ markdown = markdown.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n');
346
+ markdown = markdown.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n');
347
+
348
+ // Convert paragraphs
349
+ markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n');
350
+
351
+ // Convert links
352
+ markdown = markdown.replace(/<a[^>]*href="(.*?)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
353
+
354
+ // Convert strong/bold
355
+ markdown = markdown.replace(/<(strong|b)[^>]*>(.*?)<\/(strong|b)>/gi, '**$2**');
356
+
357
+ // Convert emphasis/italic
358
+ markdown = markdown.replace(/<(em|i)[^>]*>(.*?)<\/(em|i)>/gi, '*$2*');
359
+
360
+ // Convert unordered lists
361
+ markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, content) => {
362
+ return content.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n');
363
+ });
364
+
365
+ // Convert ordered lists
366
+ markdown = markdown.replace(/<ol[^>]*>(.*?)<\/ol>/gis, (match, content) => {
367
+ let index = 1;
368
+ return content.replace(/<li[^>]*>(.*?)<\/li>/gi, (match, item) => {
369
+ return `${index++}. ${item}\n`;
370
+ });
371
+ });
372
+
373
+ // Convert code blocks
374
+ markdown = markdown.replace(/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/gis, '```\n$1\n```\n\n');
375
+
376
+ // Convert inline code
377
+ markdown = markdown.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
378
+
379
+ // Convert blockquotes
380
+ markdown = markdown.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, '> $1\n\n');
381
+
382
+ // Convert horizontal rules
383
+ markdown = markdown.replace(/<hr[^>]*>/gi, '---\n\n');
384
+
385
+ // Remove remaining HTML tags
386
+ markdown = markdown.replace(/<[^>]*>/g, '');
387
+
388
+ // Decode HTML entities after tags are removed
389
+ markdown = markdown
390
+ .replace(/&lt;/g, '<')
391
+ .replace(/&gt;/g, '>')
392
+ .replace(/&amp;/g, '&')
393
+ .replace(/&quot;/g, '"')
394
+ .replace(/&apos;/g, '\'');
395
+
396
+ // Fix extra newlines
397
+ markdown = markdown.replace(/\n{3,}/g, '\n\n');
398
+
399
+ return markdown.trim();
400
+ }
401
+
402
+ export {
403
+ makeCodeBlocksCollapsible,
404
+ makeDescriptiveToc,
405
+ addCrossLinks,
406
+ fixHeadingHierarchy,
407
+ moveSection,
408
+ updateTOCNumbers,
409
+ insertTOC,
410
+ convertHTMLToMarkdown
411
+ };