From 9ff4cd6d05f013331e673f3c54c98cd01bb792aa Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Fri, 5 Dec 2025 01:31:03 +1000 Subject: [PATCH] feat: add markdown-formatter package with defensive patterns - Create @tpmjs/markdown-formatter with 2 tools: - markdownToPlainText: Convert markdown to plain text - formatMarkdownTable: Format and align markdown tables - Includes defensive parameter validation - Uses AI SDK v6 beta with Zod 4 schemas - Published v0.2.0 to npm Testing the full end-to-end workflow: - Package creation following generator patterns - Changesets for version management - npm publishing - TPMJS registry auto-discovery --- .../tools/markdown-formatter/CHANGELOG.md | 7 ++ packages/tools/markdown-formatter/README.md | 56 ++++++++++ .../tools/markdown-formatter/package.json | 35 ++++++ .../tools/markdown-formatter/src/index.ts | 7 ++ .../src/tools/formatMarkdownTable.ts | 105 ++++++++++++++++++ .../src/tools/markdownToPlainText.ts | 80 +++++++++++++ .../tools/markdown-formatter/tsconfig.json | 9 ++ pnpm-lock.yaml | 16 +++ 8 files changed, 315 insertions(+) create mode 100644 packages/tools/markdown-formatter/CHANGELOG.md create mode 100644 packages/tools/markdown-formatter/README.md create mode 100644 packages/tools/markdown-formatter/package.json create mode 100644 packages/tools/markdown-formatter/src/index.ts create mode 100644 packages/tools/markdown-formatter/src/tools/formatMarkdownTable.ts create mode 100644 packages/tools/markdown-formatter/src/tools/markdownToPlainText.ts create mode 100644 packages/tools/markdown-formatter/tsconfig.json diff --git a/packages/tools/markdown-formatter/CHANGELOG.md b/packages/tools/markdown-formatter/CHANGELOG.md new file mode 100644 index 0000000..03109c6 --- /dev/null +++ b/packages/tools/markdown-formatter/CHANGELOG.md @@ -0,0 +1,7 @@ +# @tpmjs/markdown-formatter + +## 0.2.0 + +### Minor Changes + +- Add markdown-formatter package with text conversion and table formatting tools diff --git a/packages/tools/markdown-formatter/README.md b/packages/tools/markdown-formatter/README.md new file mode 100644 index 0000000..fe03d5d --- /dev/null +++ b/packages/tools/markdown-formatter/README.md @@ -0,0 +1,56 @@ +# @tpmjs/markdown-formatter + +AI SDK tools for formatting and manipulating markdown text. Perfect for cleaning up markdown documents and making tables more readable! + +## Tools + +### markdownToPlainText + +Convert markdown to plain text by removing all formatting. + +```typescript +import { markdownToPlainText } from '@tpmjs/markdown-formatter'; + +const result = await markdownToPlainText.execute({ + markdown: '# Hello **World**\n\nThis is *italic* text.', + preserveLineBreaks: true, +}); +// Result: "Hello World\n\nThis is italic text." +``` + +### formatMarkdownTable + +Format and align markdown table columns for better readability. + +```typescript +import { formatMarkdownTable } from '@tpmjs/markdown-formatter'; + +const result = await formatMarkdownTable.execute({ + table: ` +| Name | Age | City | +|---|---|---| +| Alice | 30 | NYC | +| Bob | 25 | LA | + `, + alignment: 'left', +}); +// Returns a beautifully formatted table with aligned columns +``` + +## Installation + +```bash +npm install @tpmjs/markdown-formatter +``` + +## Features + +- Strip markdown formatting to plain text +- Preserve or remove line breaks +- Format markdown tables with column alignment +- Support for left, center, and right alignment +- Defensive parameter validation + +## License + +MIT diff --git a/packages/tools/markdown-formatter/package.json b/packages/tools/markdown-formatter/package.json new file mode 100644 index 0000000..e0c41a8 --- /dev/null +++ b/packages/tools/markdown-formatter/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tpmjs/markdown-formatter", + "version": "0.2.0", + "description": "AI SDK tools for formatting and manipulating markdown text", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "type-check": "tsc --noEmit" + }, + "keywords": ["tpmjs-tool", "markdown", "formatter", "text-processing"], + "tpmjs": { + "category": "text-analysis", + "frameworks": ["vercel-ai"], + "tools": [ + { + "exportName": "markdownToPlainText", + "description": "Convert markdown to plain text by removing all formatting" + }, + { + "exportName": "formatMarkdownTable", + "description": "Format and align markdown table columns for better readability" + } + ] + }, + "dependencies": { + "ai": "6.0.0-beta.131", + "zod": "^4.1.13" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "typescript": "^5.9.3" + } +} diff --git a/packages/tools/markdown-formatter/src/index.ts b/packages/tools/markdown-formatter/src/index.ts new file mode 100644 index 0000000..b096099 --- /dev/null +++ b/packages/tools/markdown-formatter/src/index.ts @@ -0,0 +1,7 @@ +/** + * TPMJS Markdown Formatter Tools + * Tools for formatting and manipulating markdown text + */ + +export { markdownToPlainText } from './tools/markdownToPlainText.js'; +export { formatMarkdownTable } from './tools/formatMarkdownTable.js'; diff --git a/packages/tools/markdown-formatter/src/tools/formatMarkdownTable.ts b/packages/tools/markdown-formatter/src/tools/formatMarkdownTable.ts new file mode 100644 index 0000000..604e9eb --- /dev/null +++ b/packages/tools/markdown-formatter/src/tools/formatMarkdownTable.ts @@ -0,0 +1,105 @@ +import { tool } from 'ai'; +import { z } from 'zod'; + +const FormatMarkdownTableSchema = z.object({ + table: z.string().min(1, 'Table text cannot be empty').describe('The markdown table to format'), + alignment: z + .enum(['left', 'center', 'right']) + .default('left') + .describe('Text alignment for all columns'), +}); + +export const formatMarkdownTable = tool({ + description: 'Format and align markdown table columns for better readability', + inputSchema: FormatMarkdownTableSchema, + async execute(input: z.infer) { + const { table, alignment } = input; + + // Defensive check: Validate required parameters + if (!table || table.trim().length === 0) { + return { + success: false, + error: 'Missing required parameter: table', + formattedTable: '', + rowCount: 0, + columnCount: 0, + }; + } + + // Split into lines and remove empty lines + const lines = table + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0 && line.startsWith('|')); + + if (lines.length < 2) { + return { + success: false, + error: 'Invalid table format: need at least header and separator rows', + formattedTable: table, + rowCount: lines.length, + columnCount: 0, + }; + } + + // Parse table rows + const rows = lines.map((line) => + line + .split('|') + .slice(1, -1) + .map((cell) => cell.trim()) + ); + + // Calculate column widths + const columnCount = rows[0]?.length || 0; + const columnWidths = new Array(columnCount).fill(0); + + for (const row of rows) { + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const currentWidth = columnWidths[i]; + // Skip separator row when calculating widths + if (cell && !cell.match(/^:?-+:?$/)) { + columnWidths[i] = Math.max(currentWidth || 0, cell.length); + } + } + } + + // Format alignment markers + const alignmentMarkers = columnWidths.map((width) => { + const dashes = '-'.repeat(Math.max(3, width)); + if (alignment === 'center') return `:${dashes}:`; + if (alignment === 'right') return `${dashes}:`; + return dashes; + }); + + // Build formatted table + const formattedRows: string[] = []; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (!row) continue; + + // Check if this is the separator row + if (i === 1 && row.every((cell) => cell.match(/^:?-+:?$/))) { + // Replace with formatted separator + formattedRows.push(`| ${alignmentMarkers.join(' | ')} |`); + } else { + // Format data row + const formattedCells = row.map((cell, j) => { + const width = columnWidths[j] || 0; + return cell.padEnd(width, ' '); + }); + formattedRows.push(`| ${formattedCells.join(' | ')} |`); + } + } + + return { + success: true, + formattedTable: formattedRows.join('\n'), + rowCount: rows.length - 1, // Exclude separator row + columnCount, + alignment, + }; + }, +}); diff --git a/packages/tools/markdown-formatter/src/tools/markdownToPlainText.ts b/packages/tools/markdown-formatter/src/tools/markdownToPlainText.ts new file mode 100644 index 0000000..3fa2725 --- /dev/null +++ b/packages/tools/markdown-formatter/src/tools/markdownToPlainText.ts @@ -0,0 +1,80 @@ +import { tool } from 'ai'; +import { z } from 'zod'; + +const MarkdownToPlainTextSchema = z.object({ + markdown: z + .string() + .min(1, 'Markdown text cannot be empty') + .describe('The markdown text to convert to plain text'), + preserveLineBreaks: z + .boolean() + .default(true) + .describe('Whether to preserve line breaks in the output'), +}); + +export const markdownToPlainText = tool({ + description: 'Convert markdown to plain text by removing all formatting', + inputSchema: MarkdownToPlainTextSchema, + async execute(input: z.infer) { + const { markdown, preserveLineBreaks } = input; + + // Defensive check: Validate required parameters + if (!markdown || markdown.trim().length === 0) { + return { + success: false, + error: 'Missing required parameter: markdown', + plainText: '', + originalLength: 0, + plainTextLength: 0, + }; + } + + // Remove markdown formatting + let plainText = markdown; + + // Remove headers (# ## ###) + plainText = plainText.replace(/^#{1,6}\s+/gm, ''); + + // Remove bold and italic (**bold**, *italic*, __bold__, _italic_) + plainText = plainText.replace(/(\*\*|__)(.*?)\1/g, '$2'); + plainText = plainText.replace(/(\*|_)(.*?)\1/g, '$2'); + + // Remove links [text](url) -> text + plainText = plainText.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); + + // Remove images ![alt](url) -> alt + plainText = plainText.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1'); + + // Remove inline code `code` + plainText = plainText.replace(/`([^`]+)`/g, '$1'); + + // Remove code blocks ```code``` + plainText = plainText.replace(/```[\s\S]*?```/g, ''); + + // Remove horizontal rules (---, ***) + plainText = plainText.replace(/^[\*\-_]{3,}$/gm, ''); + + // Remove blockquotes (>) + plainText = plainText.replace(/^>\s*/gm, ''); + + // Remove list markers (-, *, +, 1.) + plainText = plainText.replace(/^[\s]*[-\*\+]\s+/gm, ''); + plainText = plainText.replace(/^[\s]*\d+\.\s+/gm, ''); + + // Handle line breaks + if (!preserveLineBreaks) { + plainText = plainText.replace(/\n+/g, ' '); + } + + // Clean up extra whitespace + plainText = plainText.replace(/\s+/g, ' ').trim(); + + return { + success: true, + plainText, + originalLength: markdown.length, + plainTextLength: plainText.length, + reductionPercent: Math.round(((markdown.length - plainText.length) / markdown.length) * 100), + }; + }, +}); diff --git a/packages/tools/markdown-formatter/tsconfig.json b/packages/tools/markdown-formatter/tsconfig.json new file mode 100644 index 0000000..66f44b0 --- /dev/null +++ b/packages/tools/markdown-formatter/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a15b0a8..9426475 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,22 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/tools/markdown-formatter: + dependencies: + ai: + specifier: 6.0.0-beta.131 + version: 6.0.0-beta.131(effect@3.18.4)(zod@4.1.13) + zod: + specifier: ^4.1.13 + version: 4.1.13 + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../../config/tsconfig + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/tools/search-registry: dependencies: ai: