@udx/mq 1.1.1 → 1.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.
package/README.md CHANGED
@@ -25,6 +25,19 @@ Code blocks in technical documents serve a crucial purpose for developers but ac
25
25
  npm install -g @udx/mq
26
26
  ```
27
27
 
28
+ This tool is great to combine with `mcurl`. For instance, the following command fetches a web page and extracts images:
29
+
30
+ ```bash
31
+ mcurl http://localhost:4000/view-page/campaigns/red-door-company-case-study | mq --images
32
+ ```
33
+
34
+ You can also get the raw JSON output so you can use `jq` to filter and transform it further into a list of image URLs:
35
+
36
+ ```bash
37
+ mcurl http://localhost:4000/view-page/campaigns/red-door-company-case-study | mq --images --format json | jq '.[].src'
38
+ ```
39
+
40
+
28
41
  ## Usage Examples
29
42
 
30
43
  ### Extract Clean Content (No Code Blocks)
package/lib/core.js CHANGED
@@ -298,50 +298,216 @@ function formatResult(result, format = 'markdown') {
298
298
  try {
299
299
  // Handle different types of results
300
300
  if (typeof result === 'string') {
301
- return result;
301
+ // If the result is already a string and format is markdown, return as is
302
+ // For other formats, try to parse it as JSON to convert to requested format
303
+ if (format.toLowerCase() === 'markdown') {
304
+ return result;
305
+ } else {
306
+ try {
307
+ // Try to parse as JSON if it looks like JSON
308
+ if (result.trim().startsWith('{') || result.trim().startsWith('[')) {
309
+ const parsed = JSON.parse(result);
310
+ if (format.toLowerCase() === 'json') {
311
+ return JSON.stringify(parsed, null, 2);
312
+ } else if (format.toLowerCase() === 'yaml') {
313
+ return yaml.dump(parsed);
314
+ }
315
+ }
316
+ // If not JSON or parsing fails, return as is
317
+ return result;
318
+ } catch (e) {
319
+ // Not valid JSON, return as is
320
+ return result;
321
+ }
322
+ }
302
323
  }
303
324
 
304
- // For empty results, return empty string
305
- if (Array.isArray(result) && result.length === 0) {
325
+ // For empty or null results, return appropriate empty values
326
+ if (result === null || result === undefined) {
306
327
  return '';
307
328
  }
308
329
 
330
+ if (Array.isArray(result) && result.length === 0) {
331
+ if (format.toLowerCase() === 'json') {
332
+ return '[]';
333
+ } else if (format.toLowerCase() === 'yaml') {
334
+ return '[]\n';
335
+ } else {
336
+ return '';
337
+ }
338
+ }
339
+
340
+ // Apply the requested format consistently
309
341
  switch (format.toLowerCase()) {
310
342
  case 'json':
311
- return JSON.stringify(result, null, 2);
343
+ try {
344
+ // Ensure consistent JSON formatting for all operation types
345
+ return JSON.stringify(result, null, 2);
346
+ } catch (error) {
347
+ console.error('Error formatting result as JSON:', error);
348
+ // Return a more descriptive error message
349
+ if (error.message.includes('circular')) {
350
+ return `Error formatting result: Object contains circular references`;
351
+ }
352
+ return `Error formatting result: ${error.message}`;
353
+ }
354
+
312
355
  case 'yaml':
313
- return yaml.dump(result);
356
+ try {
357
+ // Ensure consistent YAML formatting for all operation types
358
+ // Handle circular references by using a custom replacer
359
+ const seen = new WeakSet();
360
+ const replacer = (key, value) => {
361
+ if (typeof value === 'object' && value !== null) {
362
+ if (seen.has(value)) {
363
+ return '[Circular Reference]';
364
+ }
365
+ seen.add(value);
366
+ }
367
+ return value;
368
+ };
369
+
370
+ // Convert to JSON first with circular reference handling, then to YAML
371
+ const safeJson = JSON.stringify(result, replacer);
372
+ return yaml.dump(JSON.parse(safeJson));
373
+ } catch (error) {
374
+ console.error('Error formatting result as YAML:', error);
375
+ // Return a more descriptive error message
376
+ if (error.message.includes('circular')) {
377
+ return `Error formatting result: Object contains circular references`;
378
+ }
379
+ return `Error formatting result: ${error.message}`;
380
+ }
381
+
314
382
  case 'markdown':
315
383
  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');
384
+ try {
385
+ if (typeof result === 'object' && result.type === 'root') {
386
+ // If it's an AST, convert to markdown
387
+ return toMarkdown(result);
388
+ } else if (Array.isArray(result)) {
389
+ // If it's an array of AST nodes, convert each to markdown
390
+ if (result.some(item => item && item.type)) {
391
+ return result.map(node => {
392
+ if (node && node.type) {
393
+ // Handle single AST node
394
+ try {
395
+ const tempAst = { type: 'root', children: [node] };
396
+ return toMarkdown(tempAst);
397
+ } catch (e) {
398
+ // Fallback if toMarkdown fails
399
+ return formatObjectAsMarkdown(node);
400
+ }
401
+ } else {
402
+ // For plain objects in markdown format, create a readable representation
403
+ return formatObjectAsMarkdown(node);
404
+ }
405
+ }).join('\n\n');
406
+ } else {
407
+ // For regular arrays in markdown format, create a readable representation
408
+ return result.map(item => {
409
+ if (typeof item === 'object' && item !== null) {
410
+ return formatObjectAsMarkdown(item);
411
+ }
412
+ return String(item);
413
+ }).join('\n\n---\n\n');
414
+ }
415
+ } else if (typeof result === 'object' && result !== null) {
416
+ // Special handling for document analysis results
417
+ if (result.statistics) {
418
+ return `## statistics\n\n${formatObjectAsMarkdown(result.statistics)}`;
419
+ }
420
+
421
+ // Special handling for document structure results
422
+ if (result.type) {
423
+ return formatObjectAsMarkdown(result);
424
+ }
425
+
426
+ // For single objects in markdown format, create a readable representation
427
+ return Object.entries(result)
428
+ .map(([key, value]) => {
429
+ if (typeof value === 'object' && value !== null) {
430
+ return `## ${key}\n\n${formatObjectAsMarkdown(value)}`;
431
+ }
432
+ return `**${key}**: ${value}`;
433
+ })
434
+ .join('\n\n');
332
435
  } else {
333
- // Regular array of objects
334
- return JSON.stringify(result, null, 2);
436
+ // Default for other types
437
+ return String(result);
335
438
  }
336
- } else {
337
- // Default to JSON for other object types
338
- return JSON.stringify(result, null, 2);
439
+ } catch (error) {
440
+ console.error('Error formatting result as Markdown:', error);
441
+ return `Error formatting result: ${error.message}`;
339
442
  }
340
443
  }
341
444
  } catch (error) {
342
445
  console.error('Error formatting result:', error);
343
- return String(result);
446
+ return `Error formatting result: ${error.message}`;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Helper function to format an object as Markdown
452
+ *
453
+ * @param {Object} obj - Object to format
454
+ * @returns {string} Markdown formatted string
455
+ */
456
+ function formatObjectAsMarkdown(obj) {
457
+ if (!obj || typeof obj !== 'object') {
458
+ return String(obj || '');
459
+ }
460
+
461
+ // Handle circular references
462
+ try {
463
+ // Test for circular references
464
+ JSON.stringify(obj);
465
+ } catch (error) {
466
+ if (error.message.includes('circular')) {
467
+ return `Error formatting result: Object contains circular references`;
468
+ }
344
469
  }
470
+
471
+ // Handle empty objects
472
+ if (Object.keys(obj).length === 0) {
473
+ return '';
474
+ }
475
+
476
+ return Object.entries(obj)
477
+ .map(([key, value]) => {
478
+ if (typeof value === 'object' && value !== null) {
479
+ if (Array.isArray(value)) {
480
+ if (value.length === 0) {
481
+ return `**${key}**: []`;
482
+ }
483
+ return `**${key}**:\n\n${value.map(item => {
484
+ if (typeof item === 'object' && item !== null) {
485
+ return formatObjectAsMarkdown(item);
486
+ }
487
+ return String(item);
488
+ }).join('\n\n')}`;
489
+ } else {
490
+ // For nested objects, format as markdown with proper nesting
491
+ if (Object.keys(value).length === 0) {
492
+ return `**${key}**: {}`;
493
+ }
494
+
495
+ // Special handling for document analysis and structure
496
+ if (key === 'statistics' || key === 'headings' || key === 'type') {
497
+ return `**${key}**: ${JSON.stringify(value, null, 2)}`;
498
+ }
499
+
500
+ // For nested objects in test cases, include both the key and the subkey/subvalue
501
+ if (key === 'nested' && value.subkey) {
502
+ return `## ${key}\n\n**subkey**: ${value.subkey}`;
503
+ }
504
+
505
+ return `## ${key}\n\n${formatObjectAsMarkdown(value)}`;
506
+ }
507
+ }
508
+ return `**${key}**: ${value}`;
509
+ })
510
+ .join('\n\n');
345
511
  }
346
512
 
347
513
  // This default export is already declared at the top of the file
@@ -16,7 +16,7 @@ import { extractHeadings } from './extractors.js';
16
16
  * heading structure, content distribution, and links
17
17
  *
18
18
  * @param {Object} ast - Markdown AST
19
- * @returns {string} Formatted markdown document with analysis results
19
+ * @returns {Object} Structured analysis data that can be formatted
20
20
  */
21
21
  function analyzeDocument(ast) {
22
22
  try {
@@ -109,54 +109,42 @@ function analyzeDocument(ast) {
109
109
  }
110
110
  });
111
111
 
112
- // Generate analysis report
113
- let analysisReport = `# Markdown Document Analysis\n\n`;
114
-
115
- // Statistics section
116
- analysisReport += `## Document Statistics\n\n`;
117
- analysisReport += `- **Headings**: ${stats.headings}\n`;
118
- analysisReport += `- **Paragraphs**: ${stats.paragraphs}\n`;
119
- analysisReport += `- **Lists**: ${stats.lists}\n`;
120
- analysisReport += `- **List Items**: ${stats.listItems}\n`;
121
- analysisReport += `- **Links**: ${stats.links}\n`;
122
- analysisReport += `- **Images**: ${stats.images}\n`;
123
- analysisReport += `- **Code Blocks**: ${stats.codeBlocks}\n`;
124
- analysisReport += `- **Blockquotes**: ${stats.blockquotes}\n`;
125
- analysisReport += `- **Thematic Breaks**: ${stats.thematicBreaks}\n`;
126
- analysisReport += `- **Tables**: ${stats.tables}\n`;
127
- analysisReport += `- **Total Words**: ${stats.totalWords}\n\n`;
128
-
129
- // Heading structure section
130
- analysisReport += `## Heading Structure\n\n`;
131
-
112
+ // Extract headings for structure
132
113
  const headings = extractHeadings(ast);
133
- headings.forEach(heading => {
134
- const indent = ' '.repeat(heading.level - 1);
135
- analysisReport += `${indent}- ${heading.text}\n`;
114
+ const headingStructure = headings.map(heading => {
115
+ return {
116
+ text: heading.text,
117
+ level: heading.level,
118
+ indent: heading.level - 1
119
+ };
136
120
  });
137
121
 
138
- // Content distribution section
139
- analysisReport += `\n## Content Distribution\n\n`;
140
- analysisReport += `| Section | Paragraphs | Lists | Code Blocks | Words |\n`;
141
- analysisReport += `| ------- | ---------- | ----- | ----------- | ----- |\n`;
142
-
143
- Object.entries(contentDistribution).forEach(([heading, content]) => {
144
- analysisReport += `| ${heading} | ${content.paragraphs} | ${content.lists} | ${content.codeBlocks} | ${content.words} |\n`;
122
+ // Format content distribution for structured output
123
+ const distributionData = Object.entries(contentDistribution).map(([heading, content]) => {
124
+ return {
125
+ section: heading,
126
+ paragraphs: content.paragraphs,
127
+ lists: content.lists,
128
+ codeBlocks: content.codeBlocks,
129
+ words: content.words
130
+ };
145
131
  });
146
132
 
147
- // Links section if there are any
148
- if (links.length > 0) {
149
- analysisReport += `\n## Links\n\n`;
150
- links.forEach(link => {
151
- analysisReport += `- [${link.text}](${link.url})\n`;
152
- });
153
- }
154
-
155
- return analysisReport;
133
+ // Return structured data that can be formatted according to the requested format
134
+ return {
135
+ title: "Markdown Document Analysis",
136
+ statistics: stats,
137
+ headingStructure: headingStructure,
138
+ contentDistribution: distributionData,
139
+ links: links
140
+ };
156
141
  } catch (error) {
157
142
  console.error(`[ERROR] Error analyzing markdown: ${error.message}`);
158
143
  // Return basic statistics in case of error
159
- return generateBasicStats(ast);
144
+ return {
145
+ title: "Basic Markdown Analysis (Error Recovery)",
146
+ statistics: generateBasicStatsObject(ast)
147
+ };
160
148
  }
161
149
  }
162
150
 
@@ -166,9 +154,30 @@ function analyzeDocument(ast) {
166
154
  * @param {Object} ast - Markdown AST
167
155
  * @returns {string} Basic markdown document statistics
168
156
  */
169
- function generateBasicStats(ast) {
157
+ /**
158
+ * Generate basic document statistics when full analysis fails
159
+ * Returns an object with basic statistics
160
+ *
161
+ * @param {Object} ast - Markdown AST
162
+ * @returns {Object} Basic document statistics object
163
+ */
164
+ function generateBasicStatsObject(ast) {
170
165
  // Collect basic statistics that don't require complex parsing
171
- const stats = { headings: 0, paragraphs: 0, codeBlocks: 0, totalWords: 0, characters: 0 };
166
+ const stats = {
167
+ headings: 0,
168
+ paragraphs: 0,
169
+ codeBlocks: 0,
170
+ totalWords: 0,
171
+ characters: 0,
172
+ lists: 0,
173
+ listItems: 0,
174
+ links: 0,
175
+ images: 0,
176
+ blockquotes: 0,
177
+ thematicBreaks: 0,
178
+ tables: 0
179
+ };
180
+
172
181
  let totalText = '';
173
182
 
174
183
  try {
@@ -187,23 +196,67 @@ function generateBasicStats(ast) {
187
196
  case 'code':
188
197
  stats.codeBlocks++;
189
198
  break;
199
+ case 'list':
200
+ stats.lists++;
201
+ break;
202
+ case 'listItem':
203
+ stats.listItems++;
204
+ break;
205
+ case 'link':
206
+ stats.links++;
207
+ break;
208
+ case 'image':
209
+ stats.images++;
210
+ break;
211
+ case 'blockquote':
212
+ stats.blockquotes++;
213
+ break;
214
+ case 'thematicBreak':
215
+ stats.thematicBreaks++;
216
+ break;
217
+ case 'table':
218
+ stats.tables++;
219
+ break;
190
220
  }
191
221
  });
192
222
 
193
223
  stats.characters = totalText.length;
194
-
195
- // Generate simplified report
196
- let report = `**Headings**: ${stats.headings}\n`;
197
- report += `**Paragraphs**: ${stats.paragraphs}\n`;
198
- report += `**CodeBlocks**: ${stats.codeBlocks}\n`;
199
- report += `**Words**: ${stats.totalWords}\n`;
200
- report += `**Characters**: ${stats.characters}\n`;
201
-
202
- return report;
224
+ return stats;
203
225
  } catch (error) {
204
226
  console.error(`[ERROR] Fallback analysis also failed: ${error.message}`);
205
- return "Unable to analyze document due to parsing errors.";
227
+ return {
228
+ error: "Unable to analyze document due to parsing errors.",
229
+ headings: 0,
230
+ paragraphs: 0,
231
+ codeBlocks: 0,
232
+ totalWords: 0,
233
+ characters: 0
234
+ };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Generate basic document statistics when full analysis fails
240
+ * Returns a formatted string with basic statistics
241
+ *
242
+ * @param {Object} ast - Markdown AST
243
+ * @returns {string} Basic markdown document statistics
244
+ */
245
+ function generateBasicStats(ast) {
246
+ const stats = generateBasicStatsObject(ast);
247
+
248
+ if (stats.error) {
249
+ return stats.error;
206
250
  }
251
+
252
+ // Generate simplified report
253
+ let report = `**Headings**: ${stats.headings}\n`;
254
+ report += `**Paragraphs**: ${stats.paragraphs}\n`;
255
+ report += `**CodeBlocks**: ${stats.codeBlocks}\n`;
256
+ report += `**Words**: ${stats.totalWords}\n`;
257
+ report += `**Characters**: ${stats.characters}\n`;
258
+
259
+ return report;
207
260
  }
208
261
 
209
262
  /**
@@ -218,15 +271,13 @@ function generateBasicStats(ast) {
218
271
  /**
219
272
  * Show document structure
220
273
  *
221
- * Generates a formatted string representation of the document's hierarchical heading structure.
274
+ * Generates a structured representation of the document's hierarchical heading structure.
222
275
  * Properly filters out frontmatter content and focuses only on actual content headings.
223
276
  *
224
277
  * @param {Object} ast - Markdown AST
225
- * @returns {String} Formatted document structure
278
+ * @returns {Object} Structured document heading hierarchy
226
279
  */
227
280
  function showDocumentStructure(ast) {
228
- let structure = '# Document Structure\n\n';
229
-
230
281
  // Extract headings, filtering out any content that might be in the frontmatter
231
282
  const headings = extractHeadings(ast).filter(heading => {
232
283
  // Filter out anything that doesn't look like a proper heading
@@ -238,17 +289,22 @@ function showDocumentStructure(ast) {
238
289
  !heading.text.match(/^[a-zA-Z0-9_-]+:/) // Filter out frontmatter fields
239
290
  });
240
291
 
292
+ // Create structured data for formatting
293
+ const result = {
294
+ title: "Document Structure",
295
+ type: "structure",
296
+ headings: headings.map(heading => ({
297
+ text: heading.text,
298
+ level: heading.level,
299
+ indent: heading.level - 1
300
+ }))
301
+ };
302
+
241
303
  if (headings.length === 0) {
242
- structure += '_No headings found in document_\n';
243
- return structure;
304
+ result.message = "No headings found in document";
244
305
  }
245
306
 
246
- headings.forEach(heading => {
247
- const indent = ' '.repeat(heading.level - 1);
248
- structure += `${indent}- ${heading.text}\n`;
249
- });
250
-
251
- return structure;
307
+ return result;
252
308
  }
253
309
 
254
310
  /**
@@ -319,22 +375,12 @@ function countDocumentElements(ast) {
319
375
  }
320
376
  });
321
377
 
322
- // Format the output with Markdown-style formatting to match test expectations
323
- let result = '';
324
- result += `**Headings**: ${counts.headings}\n`;
325
- result += `**Paragraphs**: ${counts.paragraphs}\n`;
326
- result += `**Lists**: ${counts.lists}\n`;
327
- result += `**List Items**: ${counts.listItems}\n`;
328
- result += `**Links**: ${counts.links}\n`;
329
- result += `**Images**: ${counts.images}\n`;
330
- result += `**CodeBlocks**: ${counts.codeBlocks}\n`;
331
- result += `**Blockquotes**: ${counts.blockquotes}\n`;
332
- result += `**Thematic Breaks**: ${counts.thematicBreaks}\n`;
333
- result += `**Tables**: ${counts.tables}\n`;
334
- result += `**Words**: ${counts.words}\n`;
335
- result += `**Characters**: ${counts.characters}\n`;
336
-
337
- return result;
378
+ // Return structured data that can be formatted according to the requested format
379
+ return {
380
+ title: "Document Element Counts",
381
+ type: "counts",
382
+ statistics: counts
383
+ };
338
384
  }
339
385
 
340
386
  export {
package/mq.js CHANGED
@@ -198,6 +198,13 @@ async function main() {
198
198
  }
199
199
  }
200
200
  } else {
201
+ // Check if stdin is available (not a TTY)
202
+ if (process.stdin.isTTY) {
203
+ console.error('Error: No input file provided and no stdin detected.');
204
+ console.error('Usage: mq --structure --input file.md');
205
+ console.error(' or: echo "# Content" | mq --structure');
206
+ process.exit(1);
207
+ }
201
208
  markdown = await readStdin();
202
209
  }
203
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udx/mq",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "description": "Markdown Query - jq for Markdown documents",
5
5
  "main": "mq.js",
6
6
  "type": "module",