mirror of https://github.com/actions/toolkit
Merge pull request #1014 from actions/robherley/md-summaries
feat: @actions/core extensions for markdown summarypull/1059/head
commit
91f9153ca8
|
@ -0,0 +1,277 @@
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
|
import path from 'path'
|
||||||
|
import {markdownSummary, SUMMARY_ENV_VAR} from '../src/markdown-summary'
|
||||||
|
|
||||||
|
const testFilePath = path.join(__dirname, 'test', 'test-summary.md')
|
||||||
|
|
||||||
|
async function assertSummary(expected: string): Promise<void> {
|
||||||
|
const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'})
|
||||||
|
expect(file).toEqual(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixtures = {
|
||||||
|
text: 'hello world 🌎',
|
||||||
|
code: `func fork() {
|
||||||
|
for {
|
||||||
|
go fork()
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
list: ['foo', 'bar', 'baz', '💣'],
|
||||||
|
table: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
data: 'foo',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'bar',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'baz',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'tall',
|
||||||
|
rowspan: '3'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
['one', 'two', 'three'],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
data: 'wide',
|
||||||
|
colspan: '3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
label: 'open me',
|
||||||
|
content: '🎉 surprise'
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
src: 'https://github.com/actions.png',
|
||||||
|
alt: 'actions logo',
|
||||||
|
options: {
|
||||||
|
width: '32',
|
||||||
|
height: '32'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
text: 'Where the world builds software',
|
||||||
|
cite: 'https://github.com/about'
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
text: 'GitHub',
|
||||||
|
href: 'https://github.com/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('@actions/core/src/markdown-summary', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
process.env[SUMMARY_ENV_VAR] = testFilePath
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
markdownSummary.emptyBuffer()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.promises.unlink(testFilePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws if summary env var is undefined', async () => {
|
||||||
|
process.env[SUMMARY_ENV_VAR] = undefined
|
||||||
|
const write = markdownSummary.addRaw(fixtures.text).write()
|
||||||
|
|
||||||
|
await expect(write).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws if summary file does not exist', async () => {
|
||||||
|
await fs.promises.unlink(testFilePath)
|
||||||
|
const write = markdownSummary.addRaw(fixtures.text).write()
|
||||||
|
|
||||||
|
await expect(write).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||||
|
await markdownSummary.addRaw(fixtures.text).write()
|
||||||
|
await assertSummary(`# ${fixtures.text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overwrites text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'})
|
||||||
|
await markdownSummary.addRaw(fixtures.text).write({overwrite: true})
|
||||||
|
await assertSummary(fixtures.text)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends text with EOL to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||||
|
await markdownSummary.addRaw(fixtures.text, true).write()
|
||||||
|
await assertSummary(`# ${fixtures.text}${os.EOL}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('chains appends text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
await markdownSummary
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.write()
|
||||||
|
await assertSummary([fixtures.text, fixtures.text, fixtures.text].join(''))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empties buffer after write', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
await markdownSummary.addRaw(fixtures.text).write()
|
||||||
|
await assertSummary(fixtures.text)
|
||||||
|
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns summary buffer as string', () => {
|
||||||
|
markdownSummary.addRaw(fixtures.text)
|
||||||
|
expect(markdownSummary.stringify()).toEqual(fixtures.text)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('return correct values for isEmptyBuffer', () => {
|
||||||
|
markdownSummary.addRaw(fixtures.text)
|
||||||
|
expect(markdownSummary.isEmptyBuffer()).toBe(false)
|
||||||
|
|
||||||
|
markdownSummary.emptyBuffer()
|
||||||
|
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears a buffer and summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'})
|
||||||
|
await markdownSummary.clear()
|
||||||
|
await assertSummary('')
|
||||||
|
expect(markdownSummary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds EOL', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addEOL()
|
||||||
|
.write()
|
||||||
|
await assertSummary(fixtures.text + os.EOL)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a code block without language', async () => {
|
||||||
|
await markdownSummary.addCodeBlock(fixtures.code).write()
|
||||||
|
const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a code block with a language', async () => {
|
||||||
|
await markdownSummary.addCodeBlock(fixtures.code, 'go').write()
|
||||||
|
const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an unordered list', async () => {
|
||||||
|
await markdownSummary.addList(fixtures.list).write()
|
||||||
|
const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an ordered list', async () => {
|
||||||
|
await markdownSummary.addList(fixtures.list, true).write()
|
||||||
|
const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a table', async () => {
|
||||||
|
await markdownSummary.addTable(fixtures.table).write()
|
||||||
|
const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a details element', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addDetails(fixtures.details.label, fixtures.details.content)
|
||||||
|
.write()
|
||||||
|
const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with alt text', async () => {
|
||||||
|
await markdownSummary.addImage(fixtures.img.src, fixtures.img.alt).write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with custom dimensions', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||||
|
.write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with custom dimensions', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||||
|
.write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds headings h1...h6', async () => {
|
||||||
|
for (const i of [1, 2, 3, 4, 5, 6]) {
|
||||||
|
markdownSummary.addHeading('heading', i)
|
||||||
|
}
|
||||||
|
await markdownSummary.write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds h1 if heading level not specified', async () => {
|
||||||
|
await markdownSummary.addHeading('heading').write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses h1 if heading level is garbage or out of range', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addHeading('heading', 'foobar')
|
||||||
|
.addHeading('heading', 1337)
|
||||||
|
.addHeading('heading', -1)
|
||||||
|
.addHeading('heading', Infinity)
|
||||||
|
.write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a separator', async () => {
|
||||||
|
await markdownSummary.addSeparator().write()
|
||||||
|
const expected = `<hr>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a break', async () => {
|
||||||
|
await markdownSummary.addBreak().write()
|
||||||
|
const expected = `<br>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a quote', async () => {
|
||||||
|
await markdownSummary.addQuote(fixtures.quote.text).write()
|
||||||
|
const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a quote with citation', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addQuote(fixtures.quote.text, fixtures.quote.cite)
|
||||||
|
.write()
|
||||||
|
const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a link with href', async () => {
|
||||||
|
await markdownSummary
|
||||||
|
.addLink(fixtures.link.text, fixtures.link.href)
|
||||||
|
.write()
|
||||||
|
const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
})
|
|
@ -359,3 +359,8 @@ export function getState(name: string): string {
|
||||||
export async function getIDToken(aud?: string): Promise<string> {
|
export async function getIDToken(aud?: string): Promise<string> {
|
||||||
return await OidcClient.getIDToken(aud)
|
return await OidcClient.getIDToken(aud)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown summary exports
|
||||||
|
*/
|
||||||
|
export {markdownSummary} from './markdown-summary'
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
import {EOL} from 'os'
|
||||||
|
import {constants, promises} from 'fs'
|
||||||
|
const {access, appendFile, writeFile} = promises
|
||||||
|
|
||||||
|
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
|
||||||
|
export const SUMMARY_DOCS_URL =
|
||||||
|
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary'
|
||||||
|
|
||||||
|
export type SummaryTableRow = (SummaryTableCell | string)[]
|
||||||
|
|
||||||
|
export interface SummaryTableCell {
|
||||||
|
/**
|
||||||
|
* Cell content
|
||||||
|
*/
|
||||||
|
data: string
|
||||||
|
/**
|
||||||
|
* Render cell as header
|
||||||
|
* (optional) default: false
|
||||||
|
*/
|
||||||
|
header?: boolean
|
||||||
|
/**
|
||||||
|
* Number of columns the cell extends
|
||||||
|
* (optional) default: '1'
|
||||||
|
*/
|
||||||
|
colspan?: string
|
||||||
|
/**
|
||||||
|
* Number of rows the cell extends
|
||||||
|
* (optional) default: '1'
|
||||||
|
*/
|
||||||
|
rowspan?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryImageOptions {
|
||||||
|
/**
|
||||||
|
* The width of the image in pixels. Must be an integer without a unit.
|
||||||
|
* (optional)
|
||||||
|
*/
|
||||||
|
width?: string
|
||||||
|
/**
|
||||||
|
* The height of the image in pixels. Must be an integer without a unit.
|
||||||
|
* (optional)
|
||||||
|
*/
|
||||||
|
height?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryWriteOptions {
|
||||||
|
/**
|
||||||
|
* Replace all existing content in summary file with buffer contents
|
||||||
|
* (optional) default: false
|
||||||
|
*/
|
||||||
|
overwrite?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkdownSummary {
|
||||||
|
private _buffer: string
|
||||||
|
private _filePath?: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._buffer = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||||
|
* Also checks r/w permissions.
|
||||||
|
*
|
||||||
|
* @returns step summary file path
|
||||||
|
*/
|
||||||
|
private async filePath(): Promise<string> {
|
||||||
|
if (this._filePath) {
|
||||||
|
return this._filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathFromEnv = process.env[SUMMARY_ENV_VAR]
|
||||||
|
if (!pathFromEnv) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports markdown summaries.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(pathFromEnv, constants.R_OK | constants.W_OK)
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._filePath = pathFromEnv
|
||||||
|
return this._filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps content in an HTML tag, adding any HTML attributes
|
||||||
|
*
|
||||||
|
* @param {string} tag HTML tag to wrap
|
||||||
|
* @param {string | null} content content within the tag
|
||||||
|
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||||
|
*
|
||||||
|
* @returns {string} content wrapped in HTML element
|
||||||
|
*/
|
||||||
|
private wrap(
|
||||||
|
tag: string,
|
||||||
|
content: string | null,
|
||||||
|
attrs: {[attribute: string]: string} = {}
|
||||||
|
): string {
|
||||||
|
const htmlAttrs = Object.entries(attrs)
|
||||||
|
.map(([key, value]) => ` ${key}="${value}"`)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return `<${tag}${htmlAttrs}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<${tag}${htmlAttrs}>${content}</${tag}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||||
|
*
|
||||||
|
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||||
|
*
|
||||||
|
* @returns {Promise<MarkdownSummary>} markdown summary instance
|
||||||
|
*/
|
||||||
|
async write(options?: SummaryWriteOptions): Promise<MarkdownSummary> {
|
||||||
|
const overwrite = !!options?.overwrite
|
||||||
|
const filePath = await this.filePath()
|
||||||
|
const writeFunc = overwrite ? writeFile : appendFile
|
||||||
|
await writeFunc(filePath, this._buffer, {encoding: 'utf8'})
|
||||||
|
return this.emptyBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the summary buffer and wipes the summary file
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
async clear(): Promise<MarkdownSummary> {
|
||||||
|
return this.emptyBuffer().write({overwrite: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current summary buffer as a string
|
||||||
|
*
|
||||||
|
* @returns {string} string of summary buffer
|
||||||
|
*/
|
||||||
|
stringify(): string {
|
||||||
|
return this._buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the summary buffer is empty
|
||||||
|
*
|
||||||
|
* @returns {boolen} true if the buffer is empty
|
||||||
|
*/
|
||||||
|
isEmptyBuffer(): boolean {
|
||||||
|
return this._buffer.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the summary buffer without writing to summary file
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
emptyBuffer(): MarkdownSummary {
|
||||||
|
this._buffer = ''
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds raw text to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text content to add
|
||||||
|
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addRaw(text: string, addEOL = false): MarkdownSummary {
|
||||||
|
this._buffer += text
|
||||||
|
return addEOL ? this.addEOL() : this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the operating system-specific end-of-line marker to the buffer
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addEOL(): MarkdownSummary {
|
||||||
|
return this.addRaw(EOL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML codeblock to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} code content to render within fenced code block
|
||||||
|
* @param {string} lang (optional) language to syntax highlight code
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addCodeBlock(code: string, lang?: string): MarkdownSummary {
|
||||||
|
const attrs = {
|
||||||
|
...(lang && {lang})
|
||||||
|
}
|
||||||
|
const element = this.wrap('pre', this.wrap('code', code), attrs)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML list to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string[]} items list of items to render
|
||||||
|
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addList(items: string[], ordered = false): MarkdownSummary {
|
||||||
|
const tag = ordered ? 'ol' : 'ul'
|
||||||
|
const listItems = items.map(item => this.wrap('li', item)).join('')
|
||||||
|
const element = this.wrap(tag, listItems)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML table to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {SummaryTableCell[]} rows table rows
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addTable(rows: SummaryTableRow[]): MarkdownSummary {
|
||||||
|
const tableBody = rows
|
||||||
|
.map(row => {
|
||||||
|
const cells = row
|
||||||
|
.map(cell => {
|
||||||
|
if (typeof cell === 'string') {
|
||||||
|
return this.wrap('td', cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {header, data, colspan, rowspan} = cell
|
||||||
|
const tag = header ? 'th' : 'td'
|
||||||
|
const attrs = {
|
||||||
|
...(colspan && {colspan}),
|
||||||
|
...(rowspan && {rowspan})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.wrap(tag, data, attrs)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
return this.wrap('tr', cells)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const element = this.wrap('table', tableBody)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collapsable HTML details element to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} label text for the closed state
|
||||||
|
* @param {string} content collapsable content
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addDetails(label: string, content: string): MarkdownSummary {
|
||||||
|
const element = this.wrap('details', this.wrap('summary', label) + content)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML image tag to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} src path to the image you to embed
|
||||||
|
* @param {string} alt text description of the image
|
||||||
|
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addImage(
|
||||||
|
src: string,
|
||||||
|
alt: string,
|
||||||
|
options?: SummaryImageOptions
|
||||||
|
): MarkdownSummary {
|
||||||
|
const {width, height} = options || {}
|
||||||
|
const attrs = {
|
||||||
|
...(width && {width}),
|
||||||
|
...(height && {height})
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = this.wrap('img', null, {src, alt, ...attrs})
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML section heading element
|
||||||
|
*
|
||||||
|
* @param {string} text heading text
|
||||||
|
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addHeading(text: string, level?: number | string): MarkdownSummary {
|
||||||
|
const tag = `h${level}`
|
||||||
|
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
|
||||||
|
? tag
|
||||||
|
: 'h1'
|
||||||
|
const element = this.wrap(allowedTag, text)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addSeparator(): MarkdownSummary {
|
||||||
|
const element = this.wrap('hr', null)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML line break (<br>) to the summary buffer
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addBreak(): MarkdownSummary {
|
||||||
|
const element = this.wrap('br', null)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML blockquote to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text quote text
|
||||||
|
* @param {string} cite (optional) citation url
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addQuote(text: string, cite?: string): MarkdownSummary {
|
||||||
|
const attrs = {
|
||||||
|
...(cite && {cite})
|
||||||
|
}
|
||||||
|
const element = this.wrap('blockquote', text, attrs)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML anchor tag to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text link text/content
|
||||||
|
* @param {string} href hyperlink
|
||||||
|
*
|
||||||
|
* @returns {MarkdownSummary} markdown summary instance
|
||||||
|
*/
|
||||||
|
addLink(text: string, href: string): MarkdownSummary {
|
||||||
|
const element = this.wrap('a', text, {href})
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleton export
|
||||||
|
export const markdownSummary = new MarkdownSummary()
|
Loading…
Reference in New Issue