diff --git a/.taprc b/.taprc new file mode 100644 index 0000000..a9e30ee --- /dev/null +++ b/.taprc @@ -0,0 +1,5 @@ +branches: 95 +functions: 95 +lines: 95 +statements: 95 +ts: true diff --git a/index.ts b/index.ts deleted file mode 100644 index f431d49..0000000 --- a/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { table } from './utils/common'; -export * from './utils/common'; - -export type ListItems = Array<{ text: string; depth: number } | string>; - -export default class Markdown { - LINES: string[]; - - constructor() { - this.LINES = []; - } - - render() { - return this.LINES.join('\n'); - } - - addLine(data = '\n'): this { - this.LINES.push(`${data}`); - return this; - } - - text(data: string | number): this { - this.addLine(`\n${data}\n`); - return this; - } - - header(title: string, n = 1): this { - this.addLine(`${'#'.repeat(n)} ${title}`); - return this; - } - - image(filepath: string): this { - const str = `\n\n`; - this.addLine(str); - return this; - } - - table(columns = [], labels = [], datarows = []): this { - this.addLine(table(columns, labels, datarows)); - return this; - } - - list(items: ListItems = [], numbered = false): this { - if (!Array.isArray(items)) { - throw new TypeError('List items should be an array of strings'); - } - for (const item of items) { - let parsed; - if (numbered) parsed = `${i + 1}. ${item}`; - parsed = ` ${item}`; - this.addLine(parsed); - } - return this; - } - - tasks(items: string[] = []): this { - if (!Array.isArray(items)) { - throw new TypeError('List items should be an array of strings'); - } - for (const item of items) { - this.addLine(`- [ ] ${item}`); - } - - return this; - } -} diff --git a/package-lock.json b/package-lock.json index a9e7360..3e75404 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "@scdev/declarative-markdown", "version": "1.0.0-0", "license": "MIT", - "dependencies": { - "@types/tap": "^15.0.5" - }, "devDependencies": { "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", @@ -787,7 +784,9 @@ "node_modules/@types/node": { "version": "16.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", - "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==" + "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==", + "dev": true, + "peer": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -799,14 +798,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/tap": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/tap/-/tap-15.0.5.tgz", - "integrity": "sha512-MaP+EgZNFAGRvVjHWv8ldrLvYBn4PnmAlzY7IL3/RPAPkOXdggTSTgLFONbnTpdQTe8+ixYGAySKAm9ESlZm1A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/acorn": { "version": "7.4.1", "dev": true, @@ -10702,7 +10693,9 @@ "@types/node": { "version": "16.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", - "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==" + "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==", + "dev": true, + "peer": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -10712,14 +10705,6 @@ "version": "4.0.0", "dev": true }, - "@types/tap": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/tap/-/tap-15.0.5.tgz", - "integrity": "sha512-MaP+EgZNFAGRvVjHWv8ldrLvYBn4PnmAlzY7IL3/RPAPkOXdggTSTgLFONbnTpdQTe8+ixYGAySKAm9ESlZm1A==", - "requires": { - "@types/node": "*" - } - }, "acorn": { "version": "7.4.1", "dev": true diff --git a/package.json b/package.json index 66c1922..9584306 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "A simple and configurable Next.js API handler wrapper that avoids pointless boilerplate", "main": "index.mjs", "scripts": { - "build": "tsc index.ts", + "build": "tsc", "test": "tap --ts", + "coverage": "tap --coverage-report=html", "prepare": "husky install" }, "keywords": [ @@ -42,8 +43,5 @@ "tap": "^15.0.10", "ts-node": "^10.2.1", "typescript": "^4.4.3" - }, - "dependencies": { - "@types/tap": "^15.0.5" } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..768a034 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,118 @@ +import { table, LB, CR } from './utils/common'; +export * from './utils/common'; + +export type ListItems = Array; +export type ListItem = { text: string; depth?: number }; +export type TaskItem = { text: string; checked?: boolean }; + +export default class Markdown { + LINES: string[]; + + constructor(title: string) { + this.LINES = []; + if (!title) { + throw new TypeError('Must Provide a title for this markdown'); + } + this.header(title, 1); + } + + tableOfContent(onTop = true) { + let list = []; + if (!this.LINES.length) return; + + this.header('Table of Contents', 2); + + for (const line of this.LINES) { + if (line.startsWith('#')) { + const depth = line.split('#').filter((c) => c === '').length - 1; + const text = line.match(/^\#+\s?(.*)$/)?.[1]; + if (!text && !depth) continue; + list.push({ text, depth }); + } + } + + this.list(list); + + if (onTop) { + const lng = this.LINES.length; + const toc = this.LINES.splice(lng - 2, 2); + this.LINES.splice(1, 0, ...toc); + } + + return this; + } + + render() { + return this.LINES.join(CR); + } + + addLine(data = LB): this { + this.LINES.push(`${data}`); + return this; + } + + paragraph(data: string | number): this { + this.addLine(`${data}`); + return this; + } + + header(title: string, n = 1): this { + this.addLine(`${'#'.repeat(n)} ${title}`); + return this; + } + + image(filepath: string, altText?: string): this { + const str = `![${altText || filepath}](${filepath})`; + this.addLine(str); + return this; + } + + table(columns = [], rows = [], fmtFnc?): this { + this.addLine(table(columns, rows, fmtFnc)); + return this; + } + + list(items: ListItems = [], numbered = false): this { + if (!Array.isArray(items)) { + throw new TypeError( + 'List items should be an array of { text: string, depth: number}' + ); + } + + let list = []; + + for (let i = 0; i < items.length; i++) { + let parsed; + const { text, depth } = items[i]; + if (numbered) parsed = `${i + 1}. ${text}`; + else { + parsed = `${!depth ? '' : ' '.repeat(depth)}- ${text}`; + } + list.push(parsed); + } + + this.addLine(list.join('\n')); + + return this; + } + + tasks(items: ListItems = []): this { + if (!Array.isArray(items)) { + throw new TypeError( + 'List items should be an array of { text: string, checked: boolean }' + ); + } + + let list = []; + + for (let i = 0; i < items.length; i++) { + const { text, checked } = items[i]; + const parsed = `- [${checked ? 'X' : ' '}] ${text}`; + list.push(parsed); + } + + this.addLine(list.join('\n')); + + return this; + } +} diff --git a/utils/common.ts b/src/utils/common.ts similarity index 63% rename from utils/common.ts rename to src/utils/common.ts index ec15b01..766e7d2 100644 --- a/utils/common.ts +++ b/src/utils/common.ts @@ -1,11 +1,14 @@ +export const CR = '\n\n'; +export const LB = '\n'; + export const table = ( headers: string[] = [], rows: string[] = [], fmtFnc = (rowValue: any) => rowValue ) => { let t = ''; - t += `| ${headers.join(' | ')} |\n|${' --- |'.repeat(headers.length)}\n`; - t += `| ${rows.map(fmtFnc).join(' | ')} |\n`; + t += `| ${headers.join(' | ')} |${LB}|${' --- |'.repeat(headers.length)}`; + t += `${LB}| ${rows.map(fmtFnc).join(' | ')} |`; return t; }; @@ -24,17 +27,21 @@ export const bold = (text = '') => { }; export const link = (text, url) => { + if (!text && !url) return ''; return `[${text}](${url})`; }; -export const quote = (text) => { - return `> ${text}`; +export const inlineCode = (text) => { + if (!text) return ''; + return '`' + text + '`'; }; -export const code = (text, language = '') => { - return '```' + language + '\n' + text + '\n```'; +export const quote = (text) => { + if (!text) return ''; + return `${LB}> ${text}`; }; -export const inlineCode = (text) => { - return '`' + text + '`'; +export const code = (text, language = '') => { + if (!text) return ''; + return CR + '```' + language + LB + text + LB + '```'; }; diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 0000000..1207b63 --- /dev/null +++ b/test/common.ts @@ -0,0 +1,59 @@ +import tap from 'tap'; +import { + table, + link, + code, + inlineCode, + quote, + italic, + bold, +} from '../src/utils/common'; + +tap.test('italic', (t) => { + t.equal(italic('ok'), '*ok*'); + t.equal(italic(null), ''); + t.end(); +}); + +tap.test('bold', (t) => { + t.equal(bold('ok'), '**ok**'); + t.equal(bold(null), ''); + t.end(); +}); + +tap.test('quote', (t) => { + t.equal(quote('ok'), '\n> ok'); + t.equal(quote(null), ''); + t.end(); +}); + +tap.test('link', (t) => { + const l = 'http://google.com'; + const txt = 'link'; + t.equal(link(txt, l), `[${txt}](${l})`); + t.equal(link(null, null), ''); + t.end(); +}); + +tap.test('code', (t) => { + const codeBlock = "alert('x')"; + t.equal(inlineCode(codeBlock), '`' + codeBlock + '`'); + t.equal(inlineCode(null), ''); + t.equal(code(codeBlock), '\n\n```\n' + codeBlock + '\n```'); + t.equal( + code(codeBlock, 'javascript'), + '\n\n```javascript\n' + codeBlock + '\n```' + ); + t.equal(code(null), ''); + t.end(); +}); + +tap.test('table', (t) => { + const headers = ['id', 'name']; + const rows = ['1', 'Ajeje']; + let out = '| id | name |\n'; + out += '| --- | --- |\n'; + out += '| 1 | Ajeje |'; + t.equal(table(headers, rows), out); + t.end(); +}); diff --git a/test/fixtures/output.md b/test/fixtures/output.md new file mode 100644 index 0000000..384fb71 --- /dev/null +++ b/test/fixtures/output.md @@ -0,0 +1,66 @@ +# Declarative Markdown Generator + +## Table of Contents + +- Declarative Markdown Generator + - Paragraphs + - Table + - List + - Numbered List + - Task List + - images + - Table of Contents + +## Paragraphs + +My _Italic_ text and the **bold** one + +Let's add a [link](http://google.com), why not a quote: + +> I've become death, destructor of worlds + +Do you want to see my fancy `alert('x')`, but I've a better example here: + +```go +package main + func main(){} +``` + +## Table + +| id | name | +| --- | ------ | +| 1 | Simone | + +## List + +- list1 + - nested + - nested2 +- list2 + - nested + - nested2 + +## Numbered List + +1. list1 +2. nested +3. nested2 +4. list2 +5. nested +6. nested2 + +## Task List + +- [x] list1 +- [ ] nested +- [ ] nested2 +- [ ] list2 +- [ ] nested +- [ ] nested2 + +## images + +![http://ajeje.com/image.png](http://ajeje.com/image.png) + +![ALTTEXT](http://ajeje.com/image.png) diff --git a/test/index.ts b/test/index.ts index e414584..85fda5c 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,53 +1,84 @@ +// @ts-n import tap from 'tap'; -import { - table, - link, - code, - inlineCode, - quote, - italic, - bold, -} from '../utils/common'; +import Markdown, { italic, bold, link, quote, inlineCode, code } from '../src'; +import fs from 'fs/promises'; -tap.test('italic', (t) => { - t.equal(italic('ok'), '*ok*'); - t.end(); -}); +tap.test('Shoud generate Markdown', async (t) => { + t.throws(() => { + // @ts-ignore + return new Markdown(); + }); -tap.test('bold', (t) => { - t.equal(bold('ok'), '**ok**'); - t.end(); -}); + const mkd = new Markdown('Declarative Markdown Generator'); + t.equal(mkd.render(), '# Declarative Markdown Generator'); -tap.test('quote', (t) => { - t.equal(quote('ok'), '> ok'); - t.end(); -}); + mkd + .header('Paragraphs', 2) + .paragraph(`My ${italic('Italic')} text and the ${bold('bold')} one`) + .paragraph( + `Let's add a ${link( + 'link', + 'http://google.com' + )}, why not a quote: ${quote("I've become death, destructor of worlds")}` + ) + .paragraph( + `Do you want to see my fancy ${inlineCode( + "alert('x')" + )}, but I've a better example here: ${code( + 'package main\n func main(){}', + 'go' + )}` + ) + .header('Table', 2) + .table(['id', 'name'], ['1', 'Simone']) + .header('List', 2) + .list([ + { text: 'list1', depth: 0 }, + { text: 'nested', depth: 1 }, + { text: 'nested2', depth: 1 }, + { text: 'list2' }, + { text: 'nested', depth: 1 }, + { text: 'nested2', depth: 1 }, + ]) + .header('Numbered List', 2) + .list( + [ + { text: 'list1' }, + { text: 'nested' }, + { text: 'nested2' }, + { text: 'list2' }, + { text: 'nested' }, + { text: 'nested2' }, + ], + true + ) + .header('Task List', 2) + .tasks([ + { text: 'list1', checked: true }, + { text: 'nested' }, + { text: 'nested2' }, + { text: 'list2' }, + { text: 'nested' }, + { text: 'nested2' }, + ]) + .header('images', 2) + .image('http://ajeje.com/image.png') + .image('http://ajeje.com/image.png', 'ALTTEXT') + .tableOfContent(); -tap.test('link', (t) => { - const l = 'http://google.com'; - const txt = 'link'; - t.equal(link(txt, link), `[${txt}](${link})`); - t.end(); -}); + await fs.writeFile('./test/fixtures/output.md', mkd.render()); -tap.test('code', (t) => { - const codeBlock = "alert('x')"; - t.equal(inlineCode(codeBlock), '`' + codeBlock + '`'); - t.equal(code(codeBlock), '```\n' + codeBlock + '\n```'); - t.equal( - code(codeBlock, 'javascript'), - '```javascript\n' + codeBlock + '\n```' - ); + // const fixtures = await fs.readFile('./test/fixtures/output.md', 'utf-8'); + // t.equal(mkd.render(), fixtures); t.end(); }); -tap.test('table', (t) => { - const headers = ['id', 'name']; - const rows = ['1', 'Ajeje']; - let out = '| id | name |\n'; - out += '| --- | --- |\n'; - out += '| 1 | Ajeje |\n'; - t.equal(table(headers, rows), out); +tap.test('Check throws', (t) => { + const mkd = new Markdown('Throw test'); + + // @ts-ignore + t.throws(() => mkd.list('test')); + // @ts-ignore + t.throws(() => mkd.tasks('test')); t.end(); }); diff --git a/tsconfig.json b/tsconfig.json index daa0930..9d1656a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,7 @@ "target": "ES6", "esModuleInterop": true, "module": "commonjs" - } + }, + "exclude": ["test/**"], + "include": ["src"] }