Skip to content

Commit

Permalink
Add option to include tables with parameters and I/O
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoineGautier committed Oct 17, 2024
1 parent 1bbda19 commit 2c6eab6
Show file tree
Hide file tree
Showing 6 changed files with 12,189 additions and 181 deletions.
7 changes: 4 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ parser.addArgument(
['-o', '--output'],
{
help: 'Specify output format.',
choices: ['raw-json', 'json', 'modelica', 'semantic', 'cxf', 'doc'],
choices: ['raw-json', 'json', 'modelica', 'semantic', 'cxf', 'doc', 'doc+'],
defaultValue: 'json'
}
)
Expand Down Expand Up @@ -136,10 +136,11 @@ if (args.output === 'modelica') {
if (args.output === 'cxf' && args.cxfCore && args.elementary) {
ce.getCxfCore(args.file, args.directory, args.prettyPrint)
}
if (args.output === 'doc') {
if (args.output === 'doc' || args.output === 'doc+') {
const unitData = JSON.parse(
fs.readFileSync(path.join(__dirname, 'units-si.json'), 'utf8'))
dc.buildDoc(jsons[0], jsons, unitData, args.directory)
const includeVariables = (args.output === 'doc+')
dc.buildDoc(jsons[0], jsons, unitData, args.directory, includeVariables)
}
})
}
Expand Down
199 changes: 146 additions & 53 deletions lib/cdlDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ const utilM2j = require('../lib/util.js')
// Object to persist the library paths
const libPath = {}

/**
* Creates an HTML table from an array of objects.
*
* @param {*} data
* @returns {string}
*/
function createTable (data) {
if (data.length === 0) return ''
const keys = Object.keys(data[0])
const borderStyle = 'border:1px solid black; border-collapse: collapse;'
const tableWidth = 600 // in pixels
const table = `<table style='width:${tableWidth}px; ${borderStyle}'><thead><tr>` +
`${keys.map(k => {
const columnWidth = ((k === 'type' || k === 'unit')
? 0.1
: k === 'description'
? 0.6
: 0.15) * tableWidth
return `<th style='width:${columnWidth}px; ${borderStyle}'>${k[0].toUpperCase() + k.substring(1)}</th>`
}).join('')}</tr></thead><tbody>`
return table + data.map(
row => `<tr>${keys.map(k =>
`<td style='${borderStyle}'>${row[k]}</td>`)
.join('')}</tr>`).join('') + '</tbody></table>'
}

/**
* Determines whether a document element should be included in the documentation.
* The element must be included if:
Expand Down Expand Up @@ -276,14 +302,12 @@ function processHref ($, documentation) {
for (const docElement of documentation) {
// Modify the href attribute pointing to another section of the documentation
if (docElement.fullClassName === href) {
const anchorId = createAnchorId(
docElement.headingNum,
docElement.headingText
)
const anchorId = createAnchorId(docElement.headingText, docElement.headingNum)
$(this).before('Section&nbsp;')
$(this)
.attr('href', `#${anchorId}`)
.attr('style', 'white-space: nowrap;')
.text(`Section ${docElement.headingNum}`)
.text(`${docElement.headingNum}`)
return
}
}
Expand Down Expand Up @@ -396,10 +420,11 @@ function createSectionNum (documentation) {
* the previous heading number.
*
* @param {number} headingIdx - The current heading index.
* @param {string} prevHeadingNum - The previous heading number in dot-separated format.
* @param {string} [prevHeadingNum='0'] - The previous heading number in dot-separated format.
*
* @returns {string} The new heading number in dot-separated format.
*/
function createHeadingNum (headingIdx, prevHeadingNum) {
function createHeadingNum (headingIdx, prevHeadingNum = '0') {
const prevHeadingSplit = prevHeadingNum.split('.')
if (headingIdx <= prevHeadingSplit.length) {
return [
Expand All @@ -409,33 +434,19 @@ function createHeadingNum (headingIdx, prevHeadingNum) {
} else return [...prevHeadingSplit, 1].join('.')
}

/**
* Creates a nomenclature for the sorted documentation array by adding heading
* indices and numbers.
*
* @param {Array<Object>} documentation - The array of documentation objects to process.
*/
function createNomenclature (documentation) {
let headingNum = '0'
documentation.forEach((el) => {
el.headingIdx = (el.section?.replace(/\.$/, '').match(/\./g) || []).length + 1
el.headingNum = createHeadingNum(el.headingIdx, headingNum)
headingNum = el.headingNum
})
}

/**
* Creates an anchor ID from a heading number and heading text.
*
* @param {number} headingNum - The heading number to prepend for uniqueness.
* @param {string} headingText - The heading text to convert into an anchor ID.
* @param {number} [headingNum] - The heading number to prepend for uniqueness.
* Will be randomnly generated if null.
* @param {number} [maxLen=30] - The maximum length of the resulting anchor ID.
* @returns {string} The generated anchor ID.
*/
function createAnchorId (headingNum, headingText, maxLen = 30) {
function createAnchorId (headingText, headingNum, maxLen = 30) {
/* We prepend with 'headingNum' to guarantee unicity.
We truncate to 30 characters due to MS Word limitation. */
return (headingNum + headingText)
return ((headingNum ?? math.floor(math.random() * 1e6)) + headingText)
.toLowerCase()
.replace(/\s+/g, '-')
.slice(0, maxLen)
Expand All @@ -445,17 +456,21 @@ function createAnchorId (headingNum, headingText, maxLen = 30) {
* Creates an HTML heading element with an anchor.
*
* @param {number} headingIdx - The level of the heading (e.g., 1 for `<h1>`, 2 for `<h2>`, etc.).
* @param {string} headingNum - The number or identifier for the heading (e.g., "1", "1.1").
* @param {string} headingText - The text content of the heading.
* @param {string} [headingNum] - The number or identifier for the heading (e.g., "1", "1.1").
* @param {string} [anchorId] - The anchor ID for the heading.
* Will be internally created if null.
* @returns {string} The HTML string for the heading with an anchor.
*/
function createHeading (headingIdx, headingNum, headingText) {
function createHeading (headingIdx, headingText, headingNum, anchorId) {
/* We use MS Word syntax for creating an anchor with <a name=...>
instead of the more common syntax with <section id=...> */
const anchorId = createAnchorId(headingNum, headingText)
return `<h${headingIdx}><a name=${anchorId}></a>` +
'<![if !supportLists]><span style=\'mso-list:Ignore\'>' +
`${headingNum}.&nbsp;</span><![endif]>` +
const myAnchorId = anchorId ?? createAnchorId(headingText, headingNum)
return `<h${headingIdx}><a name=${myAnchorId}></a>` + (
headingNum
? '<![if !supportLists]><span style=\'mso-list:Ignore\'>' +
`${headingNum}.&nbsp;</span><![endif]>`
: '') +
`${headingText}</h${headingIdx}>\n`
}

Expand All @@ -482,6 +497,54 @@ function processUniqueElements (documentation) {
})
}

function createSectionsBlockVars (variables) {
if (Object.keys(variables).length === 0) return {}
const blockAnchorIds = {}
const blockNames = Object.keys(variables).toSorted()
let htmlBlockVars = "<br clear=all style='page-break-before:always'>" +
createHeading(1, 'Appendix A. Block Variables')
blockNames.forEach(blockName => {
blockAnchorIds[blockName] = createAnchorId(blockName)
htmlBlockVars += createHeading(
2, blockName, /* headingNum= */ undefined, blockAnchorIds[blockName])
for (const typeVar of ['parameters', 'inputs', 'outputs']) {
if (variables[blockName][typeVar]?.length === 0) continue
variables[blockName][typeVar].forEach(v => {
v.unit = v.unit
? v.unit.replace(/"/g, '').replace(/1/, '-')
: '-'
})
htmlBlockVars += `<h3>${typeVar[0].toUpperCase() + typeVar.substring(1)}</h3>`
htmlBlockVars += createTable(variables[blockName][typeVar])
}
})
return { blockAnchorIds, htmlBlockVars }
}

function getDocWithSectionBlockVars (docElement, blockAnchorIds) {
// Parse document: this will create boilerplate tags (<body>, <head>) if not present
const $ = cheerio.load(
(docElement.cdlAnnotation?.Documentation?.info ??
docElement.documentationInfo)
.replace(/^\\*"|\\*"$/g, '') // Remove enclosing double quotes: "simple_expression": "\"<html>...</html>\""
)
// Get heading indices
const headings = []
$('h1, h2, h3, h4, h5, h6').map((_, el) =>
headings.push(Number(el.name.replace('h', ''))))
const minHeadingIdx = headings.length === 0 ? 1 : math.min(...headings)

// Add subsection with link to block variables
if (blockAnchorIds[docElement.fullClassName]) {
$('body').append(createHeading(minHeadingIdx, 'Block Variables'))
$('body').append('<p>For block parameters, input and output connectors see Section ' +
`<a href="#${blockAnchorIds[docElement.fullClassName]}">${docElement.fullClassName}</a> ` +
'in Appendix A.</p>')
}

return `${$('body').html()}`
}

/**
* Modifies the documentation information of a given documentation element.
*
Expand All @@ -491,12 +554,14 @@ function processUniqueElements (documentation) {
* @param {Object} evalContext - The evaluation context used for evaluating parameter expressions.
* @param {Object} unitContext - The unit context used for unit assignment and/or conversion.
* @param {Object} unitData - The data containing unit conversion information.
* @param {string} lastHeadingNum - The last heading number in the heading nomenclature (pass '0' at first call).
* @param {Object} [blockAnchorIds] - The data containing unit conversion information.
* @returns {string} - The modified HTML string with updated headings and code elements.
*/
function modifyInfo (docElement, evalContext, unitContext, unitData) {
function modifyInfo (docElement, evalContext, unitContext, unitData, lastHeadingNum, blockAnchorIds) {
let htmlStr = docElement.cdlAnnotation?.Documentation?.info ??
docElement.documentationInfo
// Remove double quotes surrounding strings: "simple_expression": "\"<html>...</html>\""
// Remove enclosing double quotes: "simple_expression": "\"<html>...</html>\""
htmlStr = htmlStr.replace(/^\\*"|\\*"$/g, '')
// Convert inner escaped double quotes into non-escaped double quotes
htmlStr = htmlStr.replace(/\\+"/g, '"')
Expand All @@ -506,28 +571,37 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
// Process CDL visibility toggles
processCdlToggle($, evalContext, docElement.instance?.name)

// Shift index of existing headings
// Add properties to create new heading based on component description string
docElement.headingText =
(docElement.instance?.descriptionString ?? docElement.descriptionString)
?.replace(/^\\*"|\\*"$/g, '')
docElement.headingIdx = (docElement.section?.replace(/\.$/, '').match(/\./g) || []).length + 1
docElement.headingNum = createHeadingNum(docElement.headingIdx, lastHeadingNum)
lastHeadingNum = docElement.headingNum

// Shift heading indices
const headings = []
$('h1, h2, h3, h4, h5, h6').map((_, el) =>
headings.push(Number(el.name.replace('h', ''))))
if (headings.length > 0) {
const headingOffset = docElement.headingIdx - math.min(headings) + 1
let headingNum = docElement.headingNum
const minHeadingIdx = headings.length === 0 ? 1 : math.min(...headings)

if (headings.length > 0 || blockAnchorIds?.[docElement.fullClassName]) {
const headingOffset = docElement.headingIdx - minHeadingIdx + 1
$('h1, h2, h3, h4, h5, h6').replaceWith((_, el) => {
const headingIdx = Number(el.name.replace('h', '')) + headingOffset
headingNum = createHeadingNum(headingIdx, headingNum)
return createHeading(headingIdx, headingNum, $(el).text())
lastHeadingNum = createHeadingNum(headingIdx, lastHeadingNum)
return createHeading(headingIdx, $(el).text(), lastHeadingNum)
})
}

// Insert new heading and tag as section with anchor
docElement.headingText =
(docElement.instance?.descriptionString ??
docElement.descriptionString)?.replace(/^\\*"|\\*"$/g, '')
$('body').prepend(createHeading(
docElement.headingIdx,
docElement.headingNum,
docElement.headingText
))
$('body').prepend(
createHeading(
docElement.headingIdx,
docElement.headingText,
docElement.headingNum
)
)

// Modify each code element into: <code>expression</code> (value unit, adjustable)
$('code').map(function () {
Expand All @@ -547,7 +621,7 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
return this
})

return `${$('body').html()}`
return { html: `${$('body').html()}`, lastHeadingNum }
}

/**
Expand All @@ -557,39 +631,58 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
* @param {Object} jsons - The JSON data of all used classes.
* @param {Object} unitData - The data containing unit conversion information.
* @param {string} outputDir - The directory where the documentation will be saved.
* @param {boolean} [includeVariables=false] - Whether to include block variables in the documentation.
* @param {string} [title='Sequence of Operation'] - The title of the documentation.
*
* @returns {void}
*/
function buildDoc (classObj, jsons, unitData, outputDir, title = 'Sequence of Operation') {
function buildDoc (classObj, jsons, unitData, outputDir, includeVariables = false, title = 'Sequence of Operation') {
// First extract parameters and documentation of all components
const paramAndDoc = expressionEvaluation.getParametersAndBindings(
classObj, jsons, /* fetchDoc= */ true)
classObj, jsons, /* fetchDoc= */ true, /* fetchVariables= */ includeVariables)
const evalContext = paramAndDoc.parameters.reduce((a, v) =>
({ ...a, [v.name]: v.value }), {})
const unitContext = paramAndDoc.parameters.reduce((a, v) =>
({ ...a, [v.name]: { unit: v.unit, displayUnit: v.displayUnit } }), {})
const documentation = paramAndDoc.documentation
.filter(docElement => shallBeDocumented(docElement, evalContext, paramAndDoc.documentation))

// Add subsection with a link to the block variables
const { blockAnchorIds, htmlBlockVars } = createSectionsBlockVars(paramAndDoc.variables)
if (includeVariables) {
documentation.forEach(el => {
const newDoc = getDocWithSectionBlockVars(el, blockAnchorIds)
if (el.cdlAnnotation?.Documentation?.info != null) {
el.cdlAnnotation.Documentation.info = newDoc
} else if (el.documentationInfo != null) {
el.documentationInfo = newDoc
}
})
}

// Create temporary section numbers: final section numbers are created after sorting
createSectionNum(documentation)
// Sort the documentation sections
documentation.sort(sortDocSections.bind(documentation))
// Process elements annotated with `unique=true`
processUniqueElements(documentation)
// Create a heading nomenclature for the sorted documentation array
createNomenclature(documentation)

// Modify the documentation information of each component and create the HTML file
let lastHeadingNum = '0'
const $ = cheerio.load(`<title>${title}</title>`)
documentation.forEach(el => {
$('body').append(modifyInfo(el, evalContext, unitContext, unitData))
const newInfo = modifyInfo(
el, evalContext, unitContext, unitData, lastHeadingNum, blockAnchorIds)
$('body').append(newInfo.html)
lastHeadingNum = newInfo.lastHeadingNum
})

// Modify href attributes pointing to other sections of the documentation
processHref($, documentation)

// Include sections with block variables documentation
$('body').append(htmlBlockVars)

// Create output directory and 'img' subfolder if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
Expand Down
Loading

0 comments on commit 2c6eab6

Please sign in to comment.