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:
parent
9fd56be9cd
commit
9ff4cd6d05
8 changed files with 315 additions and 0 deletions
7
packages/tools/markdown-formatter/CHANGELOG.md
Normal file
7
packages/tools/markdown-formatter/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# @tpmjs/markdown-formatter
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Add markdown-formatter package with text conversion and table formatting tools
|
||||
56
packages/tools/markdown-formatter/README.md
Normal file
56
packages/tools/markdown-formatter/README.md
Normal 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
|
||||
35
packages/tools/markdown-formatter/package.json
Normal file
35
packages/tools/markdown-formatter/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
packages/tools/markdown-formatter/src/index.ts
Normal file
7
packages/tools/markdown-formatter/src/index.ts
Normal 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';
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
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),
|
||||
};
|
||||
},
|
||||
});
|
||||
9
packages/tools/markdown-formatter/tsconfig.json
Normal file
9
packages/tools/markdown-formatter/tsconfig.json
Normal 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
16
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue