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 16, 2024
1 parent 1bbda19 commit 1c0dc9c
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 35 deletions.
83 changes: 64 additions & 19 deletions lib/cdlDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ 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,10 +300,7 @@ 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)
.attr('href', `#${anchorId}`)
.attr('style', 'white-space: nowrap;')
Expand Down Expand Up @@ -427,15 +448,15 @@ function createNomenclature (documentation) {
/**
* 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.
* @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 ?? '' + headingText)
.toLowerCase()
.replace(/\s+/g, '-')
.slice(0, maxLen)
Expand All @@ -445,17 +466,19 @@ 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").
* @returns {string} The HTML string for the heading with an anchor.
*/
function createHeading (headingIdx, headingNum, headingText) {
function createHeading (headingIdx, headingText, headingNum) {
/* 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 anchorId = createAnchorId(headingText, headingNum)
return `<h${headingIdx}><a name=${anchorId}></a>` + (
headingNum
? '<![if !supportLists]><span style=\'mso-list:Ignore\'>' +
`${headingNum}.&nbsp;</span><![endif]>`
: '') +
`${headingText}</h${headingIdx}>\n`
}

Expand Down Expand Up @@ -516,17 +539,17 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
$('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())
return createHeading(headingIdx, $(el).text(), headingNum)
})
}
// Insert new heading and tag as section with anchor
docElement.headingText =
(docElement.instance?.descriptionString ??
docElement.descriptionString)?.replace(/^\\*"|\\*"$/g, '')
(docElement.instance?.descriptionString ?? docElement.descriptionString)
?.replace(/^\\*"|\\*"$/g, '')
$('body').prepend(createHeading(
docElement.headingIdx,
docElement.headingNum,
docElement.headingText
docElement.headingText,
docElement.headingNum
))

// Modify each code element into: <code>expression</code> (value unit, adjustable)
Expand All @@ -550,6 +573,8 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
return `${$('body').html()}`
}



/**
* Builds the documentation for a given class object and writes it to an HTML file.
*
Expand All @@ -561,7 +586,7 @@ function modifyInfo (docElement, evalContext, unitContext, unitData) {
*
* @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)
Expand Down Expand Up @@ -608,6 +633,26 @@ function buildDoc (classObj, jsons, unitData, outputDir, title = 'Sequence of Op
}
}

// Add block variables in Appendix
if (includeVariables) {
$('body').append("<br clear=all style='page-break-before:always'>")
$('body').append(createHeading(1, 'Appendix A. Block Variables'))
const blockNames = Object.keys(paramAndDoc.variables).toSorted()
blockNames.forEach((blockName, idx) => {
$('body').append(createHeading(2, blockName))
['parameters', 'inputs', 'outputs'].forEach((typeVar, i) => {
if (paramAndDoc.variables[blockName][typeVar]?.length === 0) return
paramAndDoc.variables[blockName][typeVar].forEach(v => {
v.unit = v.unit
? v.unit.replace(/"/g, '').replace(/1/, '-')
: ''
})
$('body').append(`<h3>${typeVar[0].toUpperCase() + typeVar.substring(1)}</h3>`)
$('body').append(createTable(paramAndDoc.variables[blockName][typeVar]))
})
})
}

// Write the HTML file to the output directory
fs.writeFileSync(path.join(outputDir, `${title}.html`), $.html(), 'utf8')

Expand Down
84 changes: 68 additions & 16 deletions lib/expressionEvaluation.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,12 +420,15 @@ function splitExpression (expression, keepOperator = false) {
*
* @todo Store in memory parameters and doc of types already processed to avoid redundant lookups.
*/
function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instance, _bindings) {
const toReturn = { parameters: [], documentation: [] }
function getParametersAndBindings (classObject, jsons, fetchDoc = false, fetchVariables = false, _instance, _bindings) {
const toReturn = { parameters: [], documentation: [], variables: {} }
const components = getComponents(classObject, stringifyExpression)
const componentNames = components.map(({ identifier }) => identifier)
let fullClassName
let componentFullClassName
let classDefinition
const inputs = []
const outputs = []
const parameters = []

function prependIdentifier (identifier) {
return `${_instance ? _instance.name + '.' : ''}${identifier}`
Expand Down Expand Up @@ -475,7 +478,7 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan
})
}

function handleCompositeComponent (toReturn, component, jsons, classDefinition, fetchDoc) {
function handleCompositeComponent (toReturn, component, jsons, classDefinition, fetchDoc, fetchVariables) {
/* Variables in bindings expressions are prepended by the name of the instance
* of the class where the component with bindings is declared.
* Example:
Expand All @@ -487,6 +490,7 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan
classDefinition,
jsons,
fetchDoc,
fetchVariables,
/* _instance= */ {
name: prependIdentifier(component.identifier),
protected: component.protected,
Expand All @@ -498,9 +502,15 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan
)
toReturn.parameters.push(...componentData.parameters)
if (fetchDoc) toReturn.documentation.push(...componentData.documentation)
if (fetchVariables) {
toReturn.variables = {
...toReturn.variables,
...componentData.variables
}
}
}

fullClassName = classObject.within + '.' + getClassIdentifier(
const fullClassName = classObject.within + '.' + getClassIdentifier(
jsonQuery.getProperty(
classObject.class_definition
? pathToClassSpecifier
Expand All @@ -510,17 +520,20 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan
)

// Retrieve the documentation of the class object
// We already exclude protected components and elementary CDL blocks from the documentation
// We already exclude protected components and elementary CDL blocks for the documentation and variables
fetchDoc = fetchDoc &&
!_instance?.protected &&
!/^Buildings\.Controls\.OBC\.CDL/.test(fullClassName)
fetchVariables = fetchVariables &&
!_instance?.protected &&
!/^Buildings\.Controls\.OBC\.CDL/.test(fullClassName)

if (fetchDoc) {
const descriptionString = jsonQuery.getProperty(
[...(classObject.class_definition
? pathToClassSpecifier
: pathToClassSpecifier.slice(-pathToClassSpecifier.length + 2)), // If classObject is already a class definition
'long_class_specifier', 'description_string'],
'long_class_specifier', 'description_string'],
classObject
)
const classAnnotation = jsonQuery.getProperty(
Expand Down Expand Up @@ -551,19 +564,30 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan

// Retrieve the parameters and bindings of the class object
// and the documentation of each composite component

for (const component of components) {
// Local declarations of parameters and constants
if (['parameter', 'constant'].some(el => component.typePrefix?.includes(el))) {
// Local declarations of parameters to store in variables
if (fetchVariables &&
component.typePrefix?.includes('parameter') &&
!toReturn.variables.fullClassName?.parameters
) {
parameters.push({
type: component.typeSpecifier.split('.').slice(-1)[0],
name: component.identifier,
description: component.descriptionString,
unit: component.unit
})
} else if (['parameter', 'constant'].some(el => component.typePrefix?.includes(el))) {
// Local declarations of parameters and constants to store in parameters

// Primitive types
if (primitiveTypes.includes(component.typeSpecifier)) {
handleSimpleAssignment(toReturn, component)
continue
}
fullClassName = lookupClassName(
componentFullClassName = lookupClassName(
component.typeSpecifier, classObject.within, classObject.fullMoFilePath
)
classDefinition = getClassDefinition(fullClassName, jsons, classObject)
classDefinition = getClassDefinition(componentFullClassName, jsons, classObject)
if (classDefinition == null) continue
// Enumeration types
if (classDefinition.class_prefixes?.includes('type')) {
Expand All @@ -573,18 +597,46 @@ function getParametersAndBindings (classObject, jsons, fetchDoc = false, _instan
// Record types
// parameter Record p(q=1) → Store p.q = 1 → need to process as any other composite class instance
if (classDefinition.class_prefixes?.includes('record')) {
handleCompositeComponent(toReturn, component, jsons, classDefinition, /* fetchDoc= */ false)
handleCompositeComponent(toReturn, component, jsons, classDefinition,
/* fetchDoc= */ false, /* fetchDoc= */ false) // No documentation and variables from record definitions
continue
}
} else if ( // Input connectors
fetchVariables &&
!toReturn.variables.fullClassName?.inputs &&
/\.CDL\.Interfaces\..*Input$/.test(component.typeSpecifier)) {
inputs.push({
type: component.typeSpecifier.split('.').slice(-1)[0].replace('Input', ''),
name: component.identifier,
description: component.descriptionString,
unit: component.unit
})
} else if ( // Output connectors
fetchVariables &&
!toReturn.variables.fullClassName?.outputs &&
/\.CDL\.Interfaces\..*Output$/.test(component.typeSpecifier)) {
outputs.push({
type: component.typeSpecifier.split('.').slice(-1)[0].replace('Output', ''),
name: component.identifier,
description: component.descriptionString,
unit: component.unit
})
} else if ( // Instances of non primitive types (composite components) excluding I/O
!(['input', 'output'].some(el => component.typePrefix?.includes(el))) &&
!/\.CDL\.Interfaces\..*(Input|Output)$/.test(component.typeSpecifier) &&
!primitiveTypes.includes(component.type_specifier)
) {
fullClassName = lookupClassName(component.typeSpecifier, classObject.within, classObject.fullMoFilePath)
classDefinition = getClassDefinition(fullClassName, jsons, classObject)
componentFullClassName = lookupClassName(component.typeSpecifier, classObject.within, classObject.fullMoFilePath)
classDefinition = getClassDefinition(componentFullClassName, jsons, classObject)
if (classDefinition == null) continue
handleCompositeComponent(toReturn, component, jsons, classDefinition, fetchDoc)
handleCompositeComponent(toReturn, component, jsons, classDefinition, fetchDoc, fetchVariables)
}
}

if (fetchVariables) {
toReturn.variables = {
...toReturn.variables,
[fullClassName]: { inputs, outputs, parameters }
}
}

Expand Down

0 comments on commit 1c0dc9c

Please sign in to comment.