import fs from "fs"; import path from "path"; import { marked } from "marked"; import { sourceRootDir } from "eez-studio-shared/util"; import * as notification from "eez-studio-ui/notification"; import { ComponentInfo } from "./component-info"; import { getModel } from "./model"; import { MarkdownDescription } from "./doc-markdown"; import { getPropertyDescription, getPropertyGroups, getPropertyName } from "./components/ComponentProperties"; import { getInputDescription } from "./components/ComponentInputs"; import { getOutputDescription } from "./components/ComponentOutputs"; import { isArray } from "eez-studio-shared/util"; class Context { listID = 1; images = new Set(); listLevel: number = 0; imgZIndex: number = 1; constructor( public componentInfo: ComponentInfo, public imageDimensions: Map ) {} getListID() { return "list_" + this.componentInfo.name + this.listID++; } generateImage(href: string) { const imageFileName = path.basename(href); this.images.add(imageFileName); const width = this.imageDimensions.get(imageFileName)!.width; const height = this.imageDimensions.get(imageFileName)!.height; const widthmm = Math.min(Math.round((width * 43.66) / 165), 165); const heightmm = Math.round((widthmm * height) / width); return ` `; } } function lexemesToODT( context: Context, tokens: marked.Token[], listPass: number ) { let odt = ""; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (listPass == 2 && token.type != "list") { continue; } if (token.type == "paragraph") { odt += `${lexemesToODT( context, token.tokens, 0 )}`; } else if (token.type == "text") { const subtokens = (token as any).tokens; if (!isArray(subtokens) || subtokens.length == 0) { odt += token.text; } else if (subtokens.length == 1) { if ( subtokens[0].type == "text" && (!isArray((subtokens[0] as any).tokens) || (subtokens[0] as any).tokens.length == 0) ) { odt += token.text; } else if (subtokens[0].type == "image") { odt += `${lexemesToODT( context, subtokens, 0 )}`; } else { odt += lexemesToODT(context, (token as any).tokens, 0); } } else { odt += lexemesToODT(context, (token as any).tokens, 0); } } else if (token.type == "codespan") { odt += `${token.text}`; } else if (token.type == "code") { odt += token.text .split("\n") .map( line => `${line.replace( / /gi, "" )}` ) .join(); } else if (token.type == "em") { odt += `${token.text}`; } else if (token.type == "strong") { odt += `${token.text}`; } else if (token.type == "space" || token.type == "br") { if ( !( (i > 0 && (tokens[i - 1].type == "space" || tokens[i - 1].type == "paragraph")) || i == tokens.length - 1 ) ) { //odt += ``; } } else if (token.type == "list") { context.listLevel++; const listItems = token.items .map(item => { const images: any[] = []; if ( item.tokens.length > 0 && (context.listLevel <= 1 || listPass == 2) ) { const lastToken = item.tokens[item.tokens.length - 1]; if ( lastToken.type == "text" && isArray((lastToken as any).tokens) && (lastToken as any).tokens.length == 1 && (lastToken as any).tokens[0].type == "image" ) { images.push((lastToken as any).tokens[0]); item.tokens.splice(item.tokens.length - 1, 1); } } const imagesODT = images.length > 0 ? ` ${images .map(image => context.generateImage(image.href) ) .join("\n")} ` : ""; if (imagesODT) { console.log(imagesODT, context.listLevel, listPass); } const styleName = context.listLevel > 1 ? `List_20_${context.listLevel}` : i == 0 ? `List_20_${context.listLevel}_20_Start` : i == token.items.length - 1 ? `List_20_${context.listLevel}_20_End` : `List_20_${context.listLevel}`; return ` ${lexemesToODT(context, item.tokens, 1)} ${imagesODT} ${lexemesToODT(context, item.tokens, 2)} `; }) .join("\n"); if (context.listLevel > 1) { if (listPass == 2) { odt += listItems; } } else { odt += ` ${listItems} `; } context.listLevel--; } else if (token.type == "image") { odt += context.generateImage(token.href); } else if (token.type == "link") { odt += `${token.text} (link)`; } else { console.warn( "Unknown lexeme in " + context.componentInfo.name, token ); } } return odt; } function markdownToODT( context: Context, markdown: MarkdownDescription | undefined ) { if (!markdown || markdown.empty) { return ""; } const tokens = marked.lexer(markdown.raw); if ( context.componentInfo.name == "Progress (EEZ-GUI)" || context.componentInfo.name == "SCPI" ) { console.log(context.componentInfo.name, tokens); } return lexemesToODT(context, tokens, 0); } async function generateODTFile( componentInfo: ComponentInfo, filePath: string, imageDimensions: Map ) { const context = new Context(componentInfo, imageDimensions); let i: number | undefined; function projectIconX() { if (i == undefined) { const numIcons = (componentInfo.isDashboardComponent ? 1 : 0) + (componentInfo.isEezGuiComponent ? 1 : 0) + (componentInfo.isLVGLComponent ? 1 : 0); i = 3 - numIcons; } return ["125.01", "141.01", "157"][i++]; } const groupPropertiesArray = getPropertyGroups( componentInfo, componentInfo.allProperties.filter( property => !componentInfo.isEmptyProperty(property.name) ) ); const titleList = context.getListID(); const propertiesList = !componentInfo.isEmptyProperties() ? context.getListID() : undefined; const inputsList = !componentInfo.isEmptyInputs() ? context.getListID() : undefined; const outputsList = !componentInfo.isEmptyOutputs() ? context.getListID() : undefined; const examplesList = !componentInfo.isEmptyExamples() ? context.getListID() : undefined; let propertyPrevListID: string | undefined; let content = ` ${ componentInfo.isDashboardComponent ? ` ` : "" }${ componentInfo.isEezGuiComponent ? ` ` : "" }${ componentInfo.isLVGLComponent ? ` ` : "" }${componentInfo.name} Description ${markdownToODT(context, componentInfo.getDescriptionMarkdown())} ${ propertiesList ? ` Properties ${groupPropertiesArray .map( groupProperties => ` ${ groupProperties.group.title || "Other" } ${groupProperties.properties .map(property => { let propertyName = getPropertyName(property); let propertyDescription = getPropertyDescription(property); let listID = context.getListID(); const result = ` ${propertyName} ${propertyDescription} ${markdownToODT( context, componentInfo.getPropertyDescriptionMarkdown( property.name ) )}`; propertyPrevListID = listID; return result; }) .join()} ` ) .join()}` : "" } ${ inputsList ? ` Inputs ${componentInfo.inputs .filter(input => !componentInfo.isEmptyInput(input.name)) .map(input => { const inputName = input.name; let inputDescription = getInputDescription(input); return ` ${inputName} ${inputDescription} ${markdownToODT( context, componentInfo.getInputDescriptionMarkdown( input.name ) )}`; }) .join()}` : "" } ${ outputsList ? ` Outputs ${componentInfo.outputs .filter(output => !componentInfo.isEmptyOutput(output.name)) .map(output => { const outputName = output.name; let outputDescription = getOutputDescription(output); return ` ${outputName} ${outputDescription} ${markdownToODT( context, componentInfo.getOutputDescriptionMarkdown( output.name ) )}`; }) .join()}` : "" } ${ examplesList ? ` Examples ${markdownToODT( context, componentInfo.getExamplesMarkdown() )}` : "" } `; const archiver = await import("archiver"); await new Promise((resolve, reject) => { var archive = archiver.default("zip", { zlib: { level: 9 } }); var output = fs.createWriteStream(filePath); output.on("close", function () { resolve(); }); archive.on("warning", function (err: any) { console.warn(err); }); archive.on("error", function (err: any) { reject(err); }); const odtTemplatePath = `${sourceRootDir()}/../docs/odt-template`; archive.directory(odtTemplatePath, false); archive.append(content, { name: "content.xml" }); const manifest = ` ${[...context.images] .map( image => `` ) .join("\n")} `; archive.append(manifest, { name: "META-INF/manifest.xml" }); [...context.images].forEach(image => { archive.file( `${sourceRootDir()}/../help/en-US/components/images/${image}`, { name: "Pictures/" + image } ); }); archive.pipe(output); archive.finalize(); }); } export async function generateODTFilesForAllComponents() { const progressToastId = notification.info("Start...", { autoClose: false }); const folderPath = `${sourceRootDir()}/../docs/components/odt/en-US`; const model = getModel(); const n = model.allComponentsNoSearchFilter.length; let i = 0; const imageDimensions = new Map< string, { width: number; height: number } >(); await (async () => { const imagesFolderPath = `${sourceRootDir()}/../help/en-US/components/images`; const images = await fs.promises.readdir(imagesFolderPath); await Promise.all( images.map(image => { return new Promise((resolve, reject) => { var img = new Image(); img.onload = function () { imageDimensions.set(image, { width: img.width, height: img.height }); resolve(); }; img.onerror = function () { reject(); }; img.src = `file://${imagesFolderPath}/${image}`; }); }) ); })(); for (const componentInfo of model.allComponentsNoSearchFilter) { try { await generateODTFile( componentInfo, folderPath + "/" + componentInfo.type + "s/" + componentInfo.name + ".odt", imageDimensions ); i++; notification.update(progressToastId, { render: `${i} / ${n}: ${componentInfo.name}`, type: notification.INFO }); } catch (err) { notification.error(`Failed for ${componentInfo.name}: ${err}`); return; } } notification.update(progressToastId, { render: "Done.", type: notification.SUCCESS, autoClose: 3000 }); }