@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,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Example: Make Code Blocks Collapsible
5
+ *
6
+ * This example demonstrates how to use mq to transform code blocks
7
+ * into collapsible sections.
8
+ */
9
+
10
+ import { fromMarkdown } from 'mdast-util-from-markdown';
11
+ import { toMarkdown } from 'mdast-util-to-markdown';
12
+ import { visit } from 'unist-util-visit';
13
+
14
+ // Example markdown content
15
+ const markdown = `# Code Examples
16
+
17
+ ## JavaScript
18
+
19
+ \`\`\`javascript
20
+ function hello() {
21
+ console.log('Hello, world!');
22
+ }
23
+ \`\`\`
24
+
25
+ ## PHP
26
+
27
+ \`\`\`php
28
+ function hello() {
29
+ echo 'Hello, world!';
30
+ }
31
+ \`\`\`
32
+ `;
33
+
34
+ // Parse markdown to AST
35
+ const ast = fromMarkdown(markdown);
36
+
37
+ // Clone the AST to avoid mutating the original
38
+ const transformedAst = { ...ast };
39
+
40
+ // Transform code blocks to collapsible sections
41
+ visit(transformedAst, 'code', (node) => {
42
+ // Convert code node to HTML node with details/summary
43
+ node.type = 'html';
44
+ node.value = `<details>\n<summary>Click to view code example</summary>\n\n\`\`\`${node.lang || ''}\n${node.value}\`\`\`\n</details>`;
45
+ delete node.lang;
46
+ });
47
+
48
+ // Serialize back to markdown
49
+ const result = toMarkdown(transformedAst);
50
+
51
+ console.log('Original Markdown:');
52
+ console.log('=================');
53
+ console.log(markdown);
54
+ console.log('\n');
55
+
56
+ console.log('Transformed Markdown:');
57
+ console.log('===================');
58
+ console.log(result);
59
+
60
+ // How to run this example:
61
+ // node examples/make-collapsible.js
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Example: Query Headings
3
+ *
4
+ * This example demonstrates how to use mdoc to query headings
5
+ * from markdown content.
6
+ */
7
+
8
+ import { fromMarkdown } from 'mdast-util-from-markdown';
9
+ import { visit } from 'unist-util-visit';
10
+ import { toString } from 'mdast-util-to-string';
11
+
12
+ // Example markdown content
13
+ const markdown = `# Sample Documentation
14
+
15
+ This is a sample documentation file to demonstrate heading queries.
16
+
17
+ ## Introduction
18
+
19
+ This section introduces the topic.
20
+
21
+ ## Getting Started
22
+
23
+ This section helps users get started.
24
+
25
+ ### Installation
26
+
27
+ Installation instructions.
28
+
29
+ ### Configuration
30
+
31
+ Configuration instructions.
32
+
33
+ ## Advanced Usage
34
+
35
+ This section covers advanced usage scenarios.
36
+ `;
37
+
38
+ // Parse markdown to AST
39
+ const ast = fromMarkdown(markdown);
40
+
41
+ // Query all headings
42
+ console.log('All headings:');
43
+ console.log('============');
44
+ visit(ast, 'heading', (node) => {
45
+ console.log(`${node.depth} ${toString(node)}`);
46
+ });
47
+ console.log('\n');
48
+
49
+ // Query only h2 headings
50
+ console.log('Only h2 headings:');
51
+ console.log('===============');
52
+ visit(ast, 'heading', (node) => {
53
+ if (node.depth === 2) {
54
+ console.log(toString(node));
55
+ }
56
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Example: TOC Generator
3
+ *
4
+ * This example demonstrates how to use the mdoc TOC generation
5
+ * functionality to enhance markdown documentation.
6
+ */
7
+
8
+ import { generateTOC } from '../index.js';
9
+
10
+ // Example markdown content
11
+ const markdown = `# Sample Documentation
12
+
13
+ This is a sample documentation file to demonstrate TOC generation.
14
+
15
+ ## Introduction
16
+
17
+ This section introduces the topic.
18
+
19
+ ## Getting Started
20
+
21
+ This section helps users get started.
22
+
23
+ ## Advanced Usage
24
+
25
+ This section covers advanced usage scenarios.
26
+
27
+ ## Troubleshooting
28
+
29
+ This section helps users troubleshoot common issues.
30
+ `;
31
+
32
+ // Generate TOC with descriptive links
33
+ const enhancedMarkdown = generateTOC(markdown, {
34
+ descriptive: true
35
+ });
36
+
37
+ console.log('Original Markdown:');
38
+ console.log('=================');
39
+ console.log(markdown);
40
+ console.log('\n');
41
+
42
+ console.log('Enhanced Markdown with TOC:');
43
+ console.log('=========================');
44
+ console.log(enhancedMarkdown);
package/lib/core.js ADDED
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Core utilities for markdown query and transform operations
3
+ */
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { fromMarkdown } from 'mdast-util-from-markdown';
8
+ import { toMarkdown } from 'mdast-util-to-markdown';
9
+ import { toString } from 'mdast-util-to-string';
10
+ import { visit } from 'unist-util-visit';
11
+ import yaml from 'js-yaml';
12
+
13
+ // Export all functions at the top for easier discovery
14
+ export {
15
+ readStdin,
16
+ parseQuery,
17
+ filterNodes,
18
+ constructObject,
19
+ formatResult
20
+ };
21
+
22
+ export default {
23
+ readStdin,
24
+ parseQuery,
25
+ filterNodes,
26
+ constructObject,
27
+ formatResult
28
+ };
29
+
30
+ /**
31
+ * Read from stdin
32
+ *
33
+ * @returns {Promise<string>} Content read from stdin
34
+ */
35
+ async function readStdin() {
36
+ const chunks = [];
37
+ process.stdin.setEncoding('utf8');
38
+
39
+ for await (const chunk of process.stdin) {
40
+ chunks.push(chunk);
41
+ }
42
+
43
+ return chunks.join('');
44
+ }
45
+
46
+ /**
47
+ * Parse a query into parts
48
+ *
49
+ * Splits a query string by pipe character and trims each part.
50
+ * Used internally by queryMarkdown to break down complex queries.
51
+ *
52
+ * @example
53
+ * Parse a simple query
54
+ * const parts = parseQuery('.headings[]');
55
+ *
56
+ * @example
57
+ * Parse a complex query with pipes
58
+ * const parts = parseQuery('.headings[] | select(.level == 2) | {text}');
59
+ *
60
+ * @param {string} query - Query string to parse
61
+ * @returns {Array} Array of query parts
62
+ */
63
+ function parseQuery(query) {
64
+ if (!query) return [];
65
+
66
+ // Handle edge case with escaped pipes
67
+ const parts = [];
68
+ let currentPart = '';
69
+ let inQuotes = false;
70
+
71
+ for (let i = 0; i < query.length; i++) {
72
+ const char = query[i];
73
+
74
+ if (char === '"' && (i === 0 || query[i-1] !== '\\')) {
75
+ inQuotes = !inQuotes;
76
+ currentPart += char;
77
+ } else if (char === '|' && !inQuotes) {
78
+ parts.push(currentPart.trim());
79
+ currentPart = '';
80
+ } else {
81
+ currentPart += char;
82
+ }
83
+ }
84
+
85
+ if (currentPart.trim()) {
86
+ parts.push(currentPart.trim());
87
+ }
88
+
89
+ return parts;
90
+ }
91
+
92
+ /**
93
+ * Filter nodes based on an expression
94
+ *
95
+ * Filters an array of nodes based on a query expression. Supports various filtering operations
96
+ * including equality checks, contains, and startsWith.
97
+ *
98
+ * @example
99
+ * Filter headings by level
100
+ * const level2Headings = filterNodes(headings, '.level == 2');
101
+ *
102
+ * @example
103
+ * Filter links by URL pattern
104
+ * const externalLinks = filterNodes(links, '.href | startswith("https://")');
105
+ *
106
+ * @example
107
+ * Filter links to specific domain
108
+ * const udxLinks = filterNodes(links, '.href | contains("udx.io")');
109
+ *
110
+ * @param {Array} nodes - Array of nodes to filter
111
+ * @param {string} expr - Filter expression
112
+ * @returns {Array} Filtered array of nodes
113
+ */
114
+ function filterNodes(nodes, expr) {
115
+ if (!expr.startsWith('select(')) return nodes;
116
+
117
+ // Extract the condition from select()
118
+ const condition = expr.match(/select\((.*)\)/)?.[1];
119
+ if (!condition) return nodes;
120
+
121
+ // Handle common filter patterns
122
+ if (condition.includes(' == ')) {
123
+ // Equality check: .prop == value
124
+ const [propPath, valueStr] = condition.split(' == ').map(s => s.trim());
125
+ const prop = propPath.startsWith('.') ? propPath.slice(1) : propPath;
126
+
127
+ // Parse value (handle quoted strings)
128
+ let value;
129
+ if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
130
+ value = valueStr.slice(1, -1);
131
+ } else if (valueStr === 'true' || valueStr === 'false') {
132
+ value = valueStr === 'true';
133
+ } else if (!isNaN(Number(valueStr))) {
134
+ value = Number(valueStr);
135
+ } else {
136
+ value = valueStr;
137
+ }
138
+
139
+ // Apply filter
140
+ return nodes.filter(node => {
141
+ const propValue = prop.split('.').reduce((obj, p) => obj && obj[p], node);
142
+
143
+ // Special case for filtering headings by level or undefined properties
144
+ if (propValue === undefined) {
145
+ return false; // Skip items with undefined properties
146
+ }
147
+
148
+ return propValue === value;
149
+ });
150
+ } else if (condition.includes(' != ')) {
151
+ // Inequality check: .prop != value
152
+ const [propPath, valueStr] = condition.split(' != ').map(s => s.trim());
153
+ const prop = propPath.startsWith('.') ? propPath.slice(1) : propPath;
154
+
155
+ // Parse value (handle quoted strings)
156
+ let value;
157
+ if (valueStr.startsWith('"') && valueStr.endsWith('"')) {
158
+ value = valueStr.slice(1, -1);
159
+ } else if (valueStr === 'true' || valueStr === 'false') {
160
+ value = valueStr === 'true';
161
+ } else if (!isNaN(Number(valueStr))) {
162
+ value = Number(valueStr);
163
+ } else {
164
+ value = valueStr;
165
+ }
166
+
167
+ // Apply filter
168
+ return nodes.filter(node => {
169
+ const propValue = prop.split('.').reduce((obj, p) => obj && obj[p], node);
170
+ return propValue !== value;
171
+ });
172
+ } else if (condition.includes(' | contains(')) {
173
+ // Contains check: .prop | contains("value")
174
+ const [propPath, containsExpr] = condition.split(' | ').map(s => s.trim());
175
+ const prop = propPath.startsWith('.') ? propPath.slice(1) : propPath;
176
+
177
+ // More robust regex to extract the value inside contains()
178
+ const valueMatch = containsExpr.match(/contains\(["]([^"]*)["]\)/);
179
+ const valueStr = valueMatch && valueMatch[1] ? valueMatch[1] : '';
180
+
181
+ // Apply filter with improved error handling
182
+ return nodes.filter(node => {
183
+ try {
184
+ const propValue = prop.split('.').reduce((obj, p) => obj && obj[p] !== undefined ? obj[p] : null, node);
185
+ return propValue !== null && String(propValue).includes(valueStr);
186
+ } catch (err) {
187
+ // Silently skip items that cause errors
188
+ return false;
189
+ }
190
+ });
191
+ } else if (condition.includes(' | startswith(')) {
192
+ // StartsWith check: .prop | startswith("value")
193
+ const [propPath, startsWithExpr] = condition.split(' | ').map(s => s.trim());
194
+ const prop = propPath.startsWith('.') ? propPath.slice(1) : propPath;
195
+ const valueStr = startsWithExpr.match(/startswith\("(.*)"\)/)?.[1];
196
+
197
+ if (!valueStr) return nodes;
198
+
199
+ // Apply filter
200
+ return nodes.filter(node => {
201
+ const propValue = prop.split('.').reduce((obj, p) => obj && obj[p], node);
202
+ return typeof propValue === 'string' && propValue.startsWith(valueStr);
203
+ });
204
+ } else if (condition.includes(' > ') || condition.includes(' < ') ||
205
+ condition.includes(' >= ') || condition.includes(' <= ')) {
206
+ // Numeric comparison: .prop > value
207
+ let operator, parts;
208
+ if (condition.includes(' > ')) {
209
+ operator = '>';
210
+ parts = condition.split(' > ');
211
+ } else if (condition.includes(' < ')) {
212
+ operator = '<';
213
+ parts = condition.split(' < ');
214
+ } else if (condition.includes(' >= ')) {
215
+ operator = '>=';
216
+ parts = condition.split(' >= ');
217
+ } else if (condition.includes(' <= ')) {
218
+ operator = '<=';
219
+ parts = condition.split(' <= ');
220
+ }
221
+
222
+ const [propPath, valueStr] = parts.map(s => s.trim());
223
+ const prop = propPath.startsWith('.') ? propPath.slice(1) : propPath;
224
+ const value = Number(valueStr);
225
+
226
+ if (isNaN(value)) return nodes;
227
+
228
+ // Apply filter
229
+ return nodes.filter(node => {
230
+ const propValue = prop.split('.').reduce((obj, p) => obj && obj[p], node);
231
+ switch (operator) {
232
+ case '>': return propValue > value;
233
+ case '<': return propValue < value;
234
+ case '>=': return propValue >= value;
235
+ case '<=': return propValue <= value;
236
+ default: return false;
237
+ }
238
+ });
239
+ }
240
+
241
+ // Default: return unfiltered nodes
242
+ return nodes;
243
+ }
244
+
245
+ /**
246
+ * Construct an object from a pattern
247
+ *
248
+ * Creates objects with specific properties from an array of nodes based on a pattern.
249
+ * Used internally by queryMarkdown to support object construction operations.
250
+ *
251
+ * @example
252
+ * Extract only text and level properties from headings
253
+ * const headingInfo = constructObject(headings, '{text,level}');
254
+ *
255
+ * @example
256
+ * Extract only href property from links
257
+ * const linkUrls = constructObject(links, '{href}');
258
+ *
259
+ * @param {Array} nodes - Array of nodes to extract properties from
260
+ * @param {string} pattern - Pattern string in format '{prop1,prop2,...}'
261
+ * @returns {Array} Array of objects with specified properties
262
+ */
263
+ function constructObject(nodes, pattern) {
264
+ if (!pattern.startsWith('{') || !pattern.endsWith('}')) return nodes;
265
+
266
+ // Extract properties from pattern {prop1,prop2,...}
267
+ const propsStr = pattern.slice(1, -1);
268
+ const props = propsStr.split(',').map(p => p.trim());
269
+
270
+ // Apply to each node
271
+ return nodes.map(node => {
272
+ const result = {};
273
+ props.forEach(prop => {
274
+ // Handle nested properties: 'meta.author'
275
+ if (prop.includes('.')) {
276
+ const parts = prop.split('.');
277
+ let value = node;
278
+ for (const part of parts) {
279
+ value = value && value[part];
280
+ }
281
+ result[prop] = value;
282
+ } else {
283
+ result[prop] = node[prop];
284
+ }
285
+ });
286
+ return result;
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Format result based on specified format
292
+ *
293
+ * @param {any} result - The result to format
294
+ * @param {string} format - Format to use (json, yaml, markdown)
295
+ * @returns {string} Formatted result
296
+ */
297
+ function formatResult(result, format = 'markdown') {
298
+ try {
299
+ // Handle different types of results
300
+ if (typeof result === 'string') {
301
+ return result;
302
+ }
303
+
304
+ // For empty results, return empty string
305
+ if (Array.isArray(result) && result.length === 0) {
306
+ return '';
307
+ }
308
+
309
+ switch (format.toLowerCase()) {
310
+ case 'json':
311
+ return JSON.stringify(result, null, 2);
312
+ case 'yaml':
313
+ return yaml.dump(result);
314
+ case 'markdown':
315
+ default:
316
+ if (typeof result === 'object' && result.type === 'root') {
317
+ // If it's an AST, convert to markdown
318
+ return toMarkdown(result);
319
+ } else if (Array.isArray(result)) {
320
+ // If it's an array of AST nodes, convert each to markdown
321
+ if (result.some(item => item && item.type)) {
322
+ return result.map(node => {
323
+ if (node && node.type) {
324
+ // Handle single AST node
325
+ const tempAst = { type: 'root', children: [node] };
326
+ return toMarkdown(tempAst);
327
+ } else {
328
+ // Handle plain object
329
+ return JSON.stringify(node, null, 2);
330
+ }
331
+ }).join('\n\n');
332
+ } else {
333
+ // Regular array of objects
334
+ return JSON.stringify(result, null, 2);
335
+ }
336
+ } else {
337
+ // Default to JSON for other object types
338
+ return JSON.stringify(result, null, 2);
339
+ }
340
+ }
341
+ } catch (error) {
342
+ console.error('Error formatting result:', error);
343
+ return String(result);
344
+ }
345
+ }
346
+
347
+ // This default export is already declared at the top of the file
@@ -0,0 +1,125 @@
1
+ /**
2
+ * mCurl integration utilities for Markdown Query
3
+ *
4
+ * Provides functions for integrating mq with mcurl
5
+ */
6
+
7
+ import { fromMarkdown } from 'mdast-util-from-markdown';
8
+ import { toMarkdown } from 'mdast-util-to-markdown';
9
+ import _ from 'lodash';
10
+ import { queryMarkdown } from '../operations/query.js';
11
+ import { transformMarkdown } from '../operations/transform.js';
12
+ import { analyzeDocument } from '../operations/analysis.js';
13
+
14
+ /**
15
+ * Main markdown handler for mCurl integration
16
+ *
17
+ * Handles markdown content for mcurl integration, allowing mq queries,
18
+ * transformations, and analysis to be applied to fetched markdown content
19
+ *
20
+ * @param {Object} response - Response object from mcurl
21
+ * @param {Object} options - Options object with mqQuery, mqTransform, and mqAnalyze properties
22
+ * @returns {string} Processed markdown content
23
+ */
24
+ function markdownHandler(response, options) {
25
+ // Only process markdown content
26
+ if (!response.headers['content-type']?.includes('text/markdown') &&
27
+ !response.url?.endsWith('.md')) {
28
+ return response.body;
29
+ }
30
+
31
+ // Get markdown content from response
32
+ const markdown = response.body;
33
+
34
+ try {
35
+ // Parse markdown to AST
36
+ const ast = fromMarkdown(markdown);
37
+
38
+ // Apply query if provided
39
+ if (options.mqQuery) {
40
+ const result = queryMarkdown(ast, options.mqQuery);
41
+ return formatResult(result, options.mqFormat || 'markdown');
42
+ }
43
+
44
+ // Apply transform if provided
45
+ if (options.mqTransform) {
46
+ return transformMarkdown(ast, options.mqTransform);
47
+ }
48
+
49
+ // Apply analysis if requested
50
+ if (options.mqAnalyze) {
51
+ return analyzeDocument(ast);
52
+ }
53
+
54
+ // Return unchanged markdown if no operation specified
55
+ return markdown;
56
+ } catch (error) {
57
+ console.error(`Error processing markdown: ${error.message}`);
58
+ return markdown;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Register the markdown handler with mCurl
64
+ *
65
+ * Registers the mq markdown handler with mcurl to enable processing
66
+ * of markdown content fetched by mcurl
67
+ *
68
+ * @returns {boolean} True if registration successful, false otherwise
69
+ */
70
+ function registerMarkdownHandler() {
71
+ try {
72
+ // Get mcurl module path
73
+ const mcurlPath = '../../mcurl/index.js';
74
+
75
+ // Try to import mcurl
76
+ import(mcurlPath).then(mcurl => {
77
+ // Register handler
78
+ if (mcurl && mcurl.registerContentHandler) {
79
+ mcurl.registerContentHandler('text/markdown', markdownHandler);
80
+ mcurl.registerContentHandler('text/x-markdown', markdownHandler);
81
+ return true;
82
+ }
83
+ return false;
84
+ }).catch(error => {
85
+ console.error(`Error importing mcurl: ${error.message}`);
86
+ return false;
87
+ });
88
+ } catch (error) {
89
+ console.error(`Error registering markdown handler: ${error.message}`);
90
+ return false;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Format result based on specified format
96
+ *
97
+ * @param {any} result - The result to format
98
+ * @param {string} format - Format to use (json, yaml, markdown)
99
+ * @returns {string} Formatted result
100
+ */
101
+ function formatResult(result, format = 'markdown') {
102
+ switch (format.toLowerCase()) {
103
+ case 'json':
104
+ return JSON.stringify(result, null, 2);
105
+ case 'yaml':
106
+ return yaml.dump(result);
107
+ case 'markdown':
108
+ default:
109
+ if (typeof result === 'string') {
110
+ return result;
111
+ } else if (Array.isArray(result)) {
112
+ return result.map(item =>
113
+ typeof item === 'string' ? item : JSON.stringify(item, null, 2)
114
+ ).join('\n\n');
115
+ } else {
116
+ return JSON.stringify(result, null, 2);
117
+ }
118
+ }
119
+ }
120
+
121
+ export {
122
+ markdownHandler,
123
+ registerMarkdownHandler,
124
+ formatResult
125
+ };