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
This commit is contained in:
Ajax Davis 2025-12-05 01:31:03 +10:00
parent 9fd56be9cd
commit 9ff4cd6d05
8 changed files with 315 additions and 0 deletions

View file

@ -0,0 +1,7 @@
# @tpmjs/markdown-formatter
## 0.2.0
### Minor Changes
- Add markdown-formatter package with text conversion and table formatting tools

View file

@ -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

View file

@ -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"
}
}

View file

@ -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';

View file

@ -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<typeof FormatMarkdownTableSchema>) {
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,
};
},
});

View file

@ -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<typeof MarkdownToPlainTextSchema>) {
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),
};
},
});

View file

@ -0,0 +1,9 @@
{
"extends": "@tpmjs/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

16
pnpm-lock.yaml generated
View file

@ -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: