1
0
Fork 0

html element wrapper method for md summary

pull/1014/head
Rob Herley 2022-03-01 20:36:04 -05:00
parent d496b07cc0
commit 518ef1b79e
No known key found for this signature in database
GPG Key ID: D1602042C3543B06
1 changed files with 113 additions and 49 deletions

View File

@ -1,8 +1,27 @@
import * as fs from 'fs' import {constants, promises} from 'fs'
import {promisify} from 'util' const {access, appendFile, writeFile} = promises
const exists = promisify(fs.exists) export interface TableCell {
const appendFile = promisify(fs.appendFile) /**
* 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 class MarkdownSummary { export class MarkdownSummary {
static ENV_VAR = 'GITHUB_STEP_SUMMARY' static ENV_VAR = 'GITHUB_STEP_SUMMARY'
@ -24,24 +43,56 @@ export class MarkdownSummary {
`Unable to find environment variable for ${MarkdownSummary.ENV_VAR}` `Unable to find environment variable for ${MarkdownSummary.ENV_VAR}`
) )
} }
if (!(await exists(filePath))) {
throw new Error(`Missing summary file at path: ${filePath}`) try {
await access(filePath, constants.R_OK | constants.W_OK)
} catch {
throw new Error(`Unable to access summary file: ${filePath}`)
} }
return filePath return filePath
} }
/** /**
* Writes any text in the buffer to the summary file * Wraps content in an html tag, adding any HTML attributes
*
* @param tag HTML tag to wrap
* @param content content within the tag
* @param attrs key value list of html attributes to add
*/
private wrap(
tag: string,
content: string,
attrs: {[key: string]: string} = {}
): string {
const htmlAttrs = Object.entries(attrs)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')
return `<${tag}${htmlAttrs && htmlAttrs.padStart(1)}>${content}</${tag}>`
}
/**
* Writes text in the buffer to the summary buffer file, will append by default
*
* @param {boolean} [overwrite=false] (optional) replace existing content in summary file with buffer contents, default: false
* *
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
async write(): Promise<MarkdownSummary> { async write(overwrite = false): Promise<MarkdownSummary> {
const filePath = await this.filePath() const filePath = await this.filePath()
await appendFile(filePath, this.buffer, {encoding: 'utf8'}) const writeFunc = overwrite ? writeFile : appendFile
this.clear() await writeFunc(filePath, this.buffer, {encoding: 'utf8'})
return this.clearBuffer()
}
return this /**
* If the summary buffer is empty
*
* @returns {boolen} true if the buffer is empty
*/
isEmptyBuffer(): boolean {
return this.buffer.length === 0
} }
/** /**
@ -49,13 +100,13 @@ export class MarkdownSummary {
* *
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
clear(): MarkdownSummary { clearBuffer(): MarkdownSummary {
this.buffer = '' this.buffer = ''
return this return this
} }
/** /**
* Adds a newline to the summary * Adds a newline to the summary buffer
* *
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
@ -65,31 +116,35 @@ export class MarkdownSummary {
} }
/** /**
* Adds text to the summary * Adds raw text to the summary buffer
*
* @param {string} text content to add * @param {string} text content to add
* @param {boolean} [newline=true] whether or not to add a newline *
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
add(text: string, newline = true): MarkdownSummary { add(text: string): MarkdownSummary {
this.buffer += text this.buffer += text
return newline ? this.addNewline() : this
}
/**
* Adds a markdown codeblock to the summary
*
* @param {string} code content to render within fenced code block
* @param {string} [language=''] optional language to syntax highlight code
*
* @returns {MarkdownSummary} markdown summary instance
*/
addCodeBlock(code: string, language = ''): MarkdownSummary {
this.buffer += `\`\`\`${language}\n${code}\n\`\`\`\n`
return this return this
} }
/** /**
* Adds an HTML list to the summary * 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.add(element).addNewline()
}
/**
* Adds an HTML list to the summary buffer
* *
* @param {string[]} items list of items to render * @param {string[]} items list of items to render
* @param {boolean} [ordered=false] if the rendered list should be ordered or not (default: false) * @param {boolean} [ordered=false] if the rendered list should be ordered or not (default: false)
@ -97,35 +152,44 @@ export class MarkdownSummary {
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addList(items: string[], ordered = false): MarkdownSummary { addList(items: string[], ordered = false): MarkdownSummary {
const listType = `${ordered ? 'o' : 'u'}l` const tag = ordered ? 'ol' : 'ul'
const listElems = items.map(e => `<li>${e}</li>\n`).join() const listItems = items.map(item => this.wrap('li', item)).join('')
this.buffer += `<${listType}>\n${listElems}</${listType}>\n` const element = this.wrap(tag, listItems)
return this return this.add(element).addNewline()
} }
/** /**
* Adds an HTML list to the summary * Adds an HTML table to the summary buffer
* *
* @param {{[key: string]: any}[]} rows list of data rows * @param {TableCell[]} rows table rows
* @param {string[]} headers list of keys to use as headers
* *
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any addTable(rows: TableCell[][]): MarkdownSummary {
addTable(rows: {[key: string]: any}[], headers: string[]): MarkdownSummary { const tableBody = rows
const headerElems = headers.map(h => `<th>${h}</th>\n`).join()
const rowElems = rows
.map(row => { .map(row => {
const data = headers.map(h => `<td>${row[h]}</td>\n`).join() const cells = row
return `<tr>${data}</tr>\n` .map(({header, data, colspan, rowspan}) => {
const tag = header ? 'th' : 'td'
const attrs = {
...(colspan && {colspan}),
...(rowspan && {rowspan})
}
return this.wrap(tag, data, attrs)
}) })
.join() .join('')
this.buffer += `<table>\n<tr>${headerElems}</tr>\n${rowElems}</table>\n`
return this return this.wrap('tr', cells)
})
.join('')
const element = this.wrap('table', tableBody)
return this.add(element).addNewline()
} }
/** /**
* Adds a collapsable HTML details element to the summary * Adds a collapsable HTML details element to the summary buffer
* *
* @param {string} label text for the closed state * @param {string} label text for the closed state
* @param {string} content collapsable content * @param {string} content collapsable content
@ -133,7 +197,7 @@ export class MarkdownSummary {
* @returns {MarkdownSummary} markdown summary instance * @returns {MarkdownSummary} markdown summary instance
*/ */
addDetails(label: string, content: string): MarkdownSummary { addDetails(label: string, content: string): MarkdownSummary {
this.buffer += `<details><summary>${label}</summary>\n\n${content}</details>\n` const element = this.wrap('details', this.wrap('summary', label) + content)
return this return this.add(element).addNewline()
} }
} }