From fba67a77a8da03794b613d52b5d21eddc57179a4 Mon Sep 17 00:00:00 2001 From: gesslar <1266935+gesslar@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:01:23 -0500 Subject: [PATCH] refactor: update example plugins and hooks for formatter rename Co-Authored-By: Claude Sonnet 4.6 --- examples/chokidar-as-cli/npm-shrinkwrap.json | 44 + examples/chokidar-as-cli/package.json | 21 + .../hooks/lpc-wikitext-hooks.js | 4 +- examples/chokidar-as-npm/npm-shrinkwrap.json | 44 + examples/chokidar-as-npm/package.json | 6 +- .../config-lpc-to-markdown-combined.json5 | 8 + examples/config/config-lpc-to-markdown.json | 10 + examples/config/config-lpc-to-markdown.json5 | 10 + examples/config/config-lpc-to-wikitext.json | 7 + examples/hooks/lpc-markdown-hooks.js | 110 ++- .../hooks/lpc-wikitext-hooks-with-upload.js | 220 +++-- examples/hooks/lpc-wikitext-hooks.js | 4 +- examples/hooks/lua-markdown-hooks.js | 4 +- examples/hooks/lua-wikitext-hooks.js | 4 +- examples/hooks/package-lock.json | 150 ++++ examples/hooks/package.json | 13 + .../bedoc-lpc-markdown-combined.js | 751 ------------------ .../bedoc-lpc-markdown-combined/package.json | 15 - .../bedoc-lpc-parser/bedoc-lpc-parser.js | 717 ++++++++--------- .../bedoc-lpc-parser/bedoc-lpc-parser.yaml | 49 +- .../bedoc-lpc-parser/npm-shrinkwrap.json | 149 ++++ .../bedoc-lpc-parser/package.json | 4 + .../bedoc-lua-parser/bedoc-lua-parser.js | 746 +++++++---------- .../bedoc-lua-parser/bedoc-lua-parser.yaml | 80 ++ .../bedoc-lua-parser/npm-shrinkwrap.json | 149 ++++ .../bedoc-lua-parser/package.json | 4 + .../bedoc-markdown-formatter.js | 276 +++++++ .../bedoc-markdown-formatter.yaml | 94 +++ .../npm-shrinkwrap.json | 149 ++++ .../bedoc-markdown-formatter/package.json | 19 + .../bedoc-markdown-printer.js | 315 -------- .../bedoc-markdown-printer/package.json | 15 - .../admonition.txt | 0 .../bedoc-wikitext-formatter.js | 211 +++++ .../bedoc-wikitext-formatter.yaml | 98 +++ .../npm-shrinkwrap.json | 149 ++++ .../bedoc-wikitext-formatter/package.json | 19 + .../bedoc-wikitext-printer.js | 270 ------- .../bedoc-wikitext-printer/package.json | 15 - 39 files changed, 2543 insertions(+), 2410 deletions(-) create mode 100644 examples/chokidar-as-cli/npm-shrinkwrap.json create mode 100644 examples/chokidar-as-cli/package.json create mode 100644 examples/chokidar-as-npm/npm-shrinkwrap.json create mode 100644 examples/config/config-lpc-to-markdown-combined.json5 create mode 100644 examples/config/config-lpc-to-markdown.json create mode 100644 examples/config/config-lpc-to-markdown.json5 create mode 100644 examples/config/config-lpc-to-wikitext.json create mode 100644 examples/hooks/package-lock.json create mode 100644 examples/hooks/package.json delete mode 100644 examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js delete mode 100644 examples/node_modules_test/bedoc-lpc-markdown-combined/package.json create mode 100644 examples/node_modules_test/bedoc-lpc-parser/npm-shrinkwrap.json create mode 100644 examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.yaml create mode 100644 examples/node_modules_test/bedoc-lua-parser/npm-shrinkwrap.json create mode 100644 examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.js create mode 100644 examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.yaml create mode 100644 examples/node_modules_test/bedoc-markdown-formatter/npm-shrinkwrap.json create mode 100644 examples/node_modules_test/bedoc-markdown-formatter/package.json delete mode 100644 examples/node_modules_test/bedoc-markdown-printer/bedoc-markdown-printer.js delete mode 100644 examples/node_modules_test/bedoc-markdown-printer/package.json rename examples/node_modules_test/{bedoc-wikitext-printer => bedoc-wikitext-formatter}/admonition.txt (100%) create mode 100644 examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.js create mode 100644 examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.yaml create mode 100644 examples/node_modules_test/bedoc-wikitext-formatter/npm-shrinkwrap.json create mode 100644 examples/node_modules_test/bedoc-wikitext-formatter/package.json delete mode 100644 examples/node_modules_test/bedoc-wikitext-printer/bedoc-wikitext-printer.js delete mode 100644 examples/node_modules_test/bedoc-wikitext-printer/package.json diff --git a/examples/chokidar-as-cli/npm-shrinkwrap.json b/examples/chokidar-as-cli/npm-shrinkwrap.json new file mode 100644 index 0000000..0ec0602 --- /dev/null +++ b/examples/chokidar-as-cli/npm-shrinkwrap.json @@ -0,0 +1,44 @@ +{ + "name": "chokidar-as-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chokidar-as-cli", + "version": "1.0.0", + "license": "Tattooine", + "dependencies": { + "chokidar": "^5.0.0" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + } + } +} diff --git a/examples/chokidar-as-cli/package.json b/examples/chokidar-as-cli/package.json new file mode 100644 index 0000000..fd729bd --- /dev/null +++ b/examples/chokidar-as-cli/package.json @@ -0,0 +1,21 @@ +{ + "name": "chokidar-as-cli", + "version": "1.0.0", + "description": "", + "license": "Tattooine", + "author": "you", + "type": "module", + "scripts": { + "test": "echo \"Hello, world!\" && exit 1" + }, + "dependencies": { + "chokidar": "^5.0.0" + }, + "bedoc": { + "output": "../output/wikitext/", + "language": "lpc", + "format": "wikitext", + "hooks": "./hooks/lpc-wikitext-hooks.js", + "maxConcurrent": 5 + } +} diff --git a/examples/chokidar-as-npm/hooks/lpc-wikitext-hooks.js b/examples/chokidar-as-npm/hooks/lpc-wikitext-hooks.js index 09d95fe..cf51466 100644 --- a/examples/chokidar-as-npm/hooks/lpc-wikitext-hooks.js +++ b/examples/chokidar-as-npm/hooks/lpc-wikitext-hooks.js @@ -1,7 +1,7 @@ export const Hooks = { - parse: {}, + parser: {}, - print: { + formatter: { async enter(section) { const {sectionName, sectionContent} = section diff --git a/examples/chokidar-as-npm/npm-shrinkwrap.json b/examples/chokidar-as-npm/npm-shrinkwrap.json new file mode 100644 index 0000000..52c1784 --- /dev/null +++ b/examples/chokidar-as-npm/npm-shrinkwrap.json @@ -0,0 +1,44 @@ +{ + "name": "chokidar-as-npm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chokidar-as-npm", + "version": "1.0.0", + "license": "Unlicense", + "dependencies": { + "chokidar": "^5.0.0" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + } + } +} diff --git a/examples/chokidar-as-npm/package.json b/examples/chokidar-as-npm/package.json index 0be4e1d..1e6e414 100644 --- a/examples/chokidar-as-npm/package.json +++ b/examples/chokidar-as-npm/package.json @@ -2,15 +2,15 @@ "name": "chokidar-as-npm", "version": "1.0.0", "description": "", - "license": "ISC", - "author": "", + "license": "Unlicense", + "author": "you", "type": "module", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "chokidar": "^4.0.3" + "chokidar": "^5.0.0" }, "bedoc": { "output": "../output/wikitext/", diff --git a/examples/config/config-lpc-to-markdown-combined.json5 b/examples/config/config-lpc-to-markdown-combined.json5 new file mode 100644 index 0000000..b9b4753 --- /dev/null +++ b/examples/config/config-lpc-to-markdown-combined.json5 @@ -0,0 +1,8 @@ +{ + parser: "examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js", + formatter: "examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js", + input: [ + "examples/source/lpc/arrays.c", + "examples/source/lpc/base64.c", + ], +} diff --git a/examples/config/config-lpc-to-markdown.json b/examples/config/config-lpc-to-markdown.json new file mode 100644 index 0000000..397858f --- /dev/null +++ b/examples/config/config-lpc-to-markdown.json @@ -0,0 +1,10 @@ +{ + "language": "lpc", + "format": "markdown", + "output": "output/markdown", + "input": [ + "examples/source/lpc/arrays.c", + "examples/source/lpc/base64.c" + ], + "hooks": "examples/hooks/lpc-wikitext-hooks.js" +} diff --git a/examples/config/config-lpc-to-markdown.json5 b/examples/config/config-lpc-to-markdown.json5 new file mode 100644 index 0000000..f04e2e1 --- /dev/null +++ b/examples/config/config-lpc-to-markdown.json5 @@ -0,0 +1,10 @@ +{ + "language": "lpc", + "format": "markdown", + "output": "examples/output/markdown", + "input": [ + "examples/source/lpc/arrays.c", + "examples/source/lpc/base64.c" + ], + "hooks": "examples/hooks/lpc-wikitext-hooks.js" +} diff --git a/examples/config/config-lpc-to-wikitext.json b/examples/config/config-lpc-to-wikitext.json new file mode 100644 index 0000000..a9721dc --- /dev/null +++ b/examples/config/config-lpc-to-wikitext.json @@ -0,0 +1,7 @@ +{ + "language": "lpc", + "format": "wikitext", + "output": "output/", + "input": ["wip/sample/arrays.c", "wip/sample/base64.c"], + "hooks": "wip/wikitext-hooks.js" +} diff --git a/examples/hooks/lpc-markdown-hooks.js b/examples/hooks/lpc-markdown-hooks.js index 1435f99..adb5a59 100644 --- a/examples/hooks/lpc-markdown-hooks.js +++ b/examples/hooks/lpc-markdown-hooks.js @@ -1,83 +1,65 @@ -export const Hooks = { - parse: {}, +export class Parse { + after$extractTags = ctx => { + // NO EXAMPLES! Figure it out on your own. - print: { - - name: "lpc-markdown-hooks", - jokes: [], - - async setup({log}) { - this.log = log - this.debug = log.newDebug() - - this.log.debug("Init hooks for: %o", 2, this.name) - }, - - async start(document) { - const debug = this.debug - - const {moduleName,moduleContent} = document - - debug("Start hook for %s (%d functions)", 2, - moduleName, moduleContent.length - ) - - const result = await this.getDadJokes(moduleContent.length) - const {status, jokes} = result + delete ctx.tag.example + } +} - if(status === "error") - throw new Error(`Failed to fetch jokes: ${result.error}`) +export class Format { + jokes = [] - debug("Fetched %o jokes", 2, jokes.length) + setup = async ctx => { + const result = await this.getDadJokes(ctx.length) + const {status, jokes} = result - this.jokes = jokes.map(joke => joke.joke) - }, + if(status === "error") + throw new Error(`Failed to fetch jokes: ${result.error}`) - async enter(section) { - const {sectionName, sectionContent} = section + this.jokes = jokes.map(joke => joke.joke) + } - if(sectionName === "description") { - const joke = this.jokes.pop() + before$formatFunction = async ctx => { + const joke = this.jokes.pop() - return joke - ? [...sectionContent, joke] - : sectionContent - } - }, + if(joke) + ctx.description.push("", joke) + } - /** + /** * Fetches a dad joke from the icanhazdadjoke API. + * * @param {number} number - The number of jokes to fetch. * @returns {Promise} The result of the fetch operation. */ - async getDadJokes(number = 1) { - const url = `https://icanhazdadjoke.com/search?limit=${number}` + getDadJokes = async(number = 1) => { + const url = `https://icanhazdadjoke.com/search?limit=${number}` - try { - const headers = new Headers() - headers.append("Accept", "application/json") - headers.append("User-Agent", "BeDoc Sample API Usage " + + try { + const headers = new Headers() + headers.append("Accept", "application/json") + headers.append("User-Agent", "BeDoc Sample API Usage " + "(https://github.com/gesslar/BeDoc)") - const response = await fetch(url, { - method: "GET", - headers: headers, - }) - if(!response.ok) - throw new Error(`HTTP error! status: ${response.status}: `+ + const response = await fetch(url, { + method: "GET", + headers: headers, + }) + if(!response.ok) + throw new Error(`HTTP error! status: ${response.status}: `+ `${response.statusText}`) - const data = await response.json() - return { - status: "success", - message: "Jokes fetched successfully", - jokes: data.results - } - } catch(error) { - return { - status: "error", - error: error, - } + const data = await response.json() + + return { + status: "success", + message: "Jokes fetched successfully", + jokes: data.results + } + } catch(error) { + return { + status: "error", + error: error, } } - }, + } } diff --git a/examples/hooks/lpc-wikitext-hooks-with-upload.js b/examples/hooks/lpc-wikitext-hooks-with-upload.js index 5b8b8fe..ae296fc 100644 --- a/examples/hooks/lpc-wikitext-hooks-with-upload.js +++ b/examples/hooks/lpc-wikitext-hooks-with-upload.js @@ -1,139 +1,127 @@ -import process from "node:process" -import {setTimeout as timeoutPromise} from "node:timers/promises" import "dotenv/config" -import MediaWikiUploader from "./mediawiki-uploader.js" -import Logger from "@gesslar/bedoc/core/Logger.js" - -export const Hooks = { - parse: {}, - - print: { - /** - * Setup the current hooks and set some initial values. - * @async - * @function - * @param {object} options Passed in options object - * @param {Logger} options.log An instance of the Logger class - */ - async setup({log}) { - try { - this.log = log - this.debug = log.newDebug() - - const {BASE_URL,BOT_USERNAME,BOT_PASSWORD} = process.env - this.BASE_URL = BASE_URL - this.BOT_USERNAME = BOT_USERNAME - this.BOT_PASSWORD = BOT_PASSWORD - - this.log.debug("Init hooks for: %o", 2, this) - } catch(error) { - this.log.error(`Error setting up hooks:`, error) - } - }, - - async enter(section) { - const {sectionName, sectionContent} = section - - if(sectionName === "description") { - // Trim leading and trailing empty lines. - const content = sectionContent - while(content.length && !content.at(0)) - content.shift() - - while(content.length && !content.at(-1)) - content.pop() - - return section - } - }, - - /** +import process from "node:process" +import {Glog, Time} from "@gesslar/toolkit" +import Wikid from "@gesslar/wikid" + +export class Format { + #BASE_URL + #BOT_USERNAME + #BOT_PASSWORD + + #glog + + constructor() { + this.#glog = new Glog() + } + + /** + * Setup the current hooks and set some initial values. + * + * @async + */ + setup = async() => { + try { + this.debug = log.newDebug() + + const {BASE_URL,BOT_USERNAME,BOT_PASSWORD} = process.env + this.#BASE_URL = BASE_URL + this.#BOT_USERNAME = BOT_USERNAME + this.#BOT_PASSWORD = BOT_PASSWORD + } catch(error) { + this.#glog.error(`Error setting up hooks:`, error) + } + } + + /** * Processes a module at the end, converting the syntax highlighting from * markdown to wikitext. * * Additionally, uploads the data to a MediaWiki site. + * * @async - * @function * @param {object} module Options object. * @param {string} module.moduleName The name of the module being processed - * @param {string} module.moduleContent The output generated by the printer + * @param {string} module.moduleContent The output generated by the formatt * @param {number?} [module.count] The number of times we've done this. * @returns {Promise} The altered text. */ - async end(module) { - const {moduleName,moduleContent} = module - let count = module.count ?? 0 - - const {BASE_URL, BOT_USERNAME, BOT_PASSWORD} = this - const bot = new MediaWikiUploader() - if(!bot) - throw new Error("MediaWiki bot not instantiated.") - - const info = (...arg) => this.log.info(...arg) - - if(count > 0) - info(`Retrying \`${moduleName}\` #${count}...`) - - const wikitext = moduleContent - .replace( - /```c\n([\s\S]+?)```/g, - '\n$1\n', - ) + "\n{{sefun}}\n" - - try { - const loginResult = await bot.login({ - baseUrl: BASE_URL, - botUsername: BOT_USERNAME, - botPassword: BOT_PASSWORD - }) - - if(loginResult.status === "error") - throw loginResult.error - - const request = { - token: loginResult.token, - title: moduleName, - content: wikitext - } + after$finalize = async(ctx, current) => { + debugger + /* + const {moduleName,moduleContent} = module + const count = module.count ?? 0 + + const {#BASE_URL: BASE_URL, #BOT_USERNAME: BOT_USERNAME, #BOT_PASSWORD: BOT_PASSWORD} = this + const bot = new MediaWikiUploader() + if(!bot) + throw new Error("MediaWiki bot not instantiated.") + + const info = (...arg) => this.log.info(...arg) + + if(count > 0) + info(`Retrying \`${moduleName}\` #${count}...`) + + const wikitext = moduleContent + .replace( + /```c\n([\s\S]+?)```/g, + '\n$1\n', + ) + "\n{{sefun}}\n" + + try { + const loginResult = await bot.login({ + baseUrl: BASE_URL, + botUsername: BOT_USERNAME, + botPassword: BOT_PASSWORD + }) + + if(loginResult.status === "error") + throw loginResult.error + + const request = { + token: loginResult.token, + title: moduleName, + content: wikitext + } - const editResult = await bot.createOrEditPage(request) + const editResult = await bot.createOrEditPage(request) - if(editResult.status === "error") { - const data = JSON.parse(editResult.error.message) - const secs = 10 + (count * 2) + if(editResult.status === "error") { + const data = JSON.parse(editResult.error.message) + const secs = 10 + (count * 2) - if(data?.error?.code) { - if(data.error.code === "ratelimited") { - this.log.warn(`Rate limited for \`${moduleName}\`. Trying again in ${secs} seconds.`) + if(data?.error?.code) { + if(data.error.code === "ratelimited") { + this.log.warn(`Rate limited for \`${moduleName}\`. Trying again in ${secs} seconds.`) - await timeoutPromise(secs*1_000) - module.count = count+1 - return this.end(module) - } else { - throw new Error(`Error uploading \`${moduleName}\`: ${data.error.info}`) - } - } - } + await timeoutPromise(secs*1_000) + module.count = count+1 - // console.log(editResult) - // console.log(editResult.result) - const {title, oldrevid} = editResult.result - const sanitizedUrl = - `${BASE_URL}/index.php?title=${encodeURIComponent(title)}` - if(oldrevid !== undefined) { - if(oldrevid === 0) { - this.log.info(`Page created successfully: '${sanitizedUrl}'`) + return this.end(module) } else { - this.log.info(`Page edited successfully: '${sanitizedUrl}'`) + throw new Error(`Error uploading \`${moduleName}\`: ${data.error.info}`) } + } + } + + // console.log(editResult) + // console.log(editResult.result) + const {title, oldrevid} = editResult.result + const sanitizedUrl = + `${BASE_URL}/index.php?title=${encodeURIComponent(title)}` + if(oldrevid !== undefined) { + if(oldrevid === 0) { + this.log.info(`Page created successfully: '${sanitizedUrl}'`) } else { - this.log.info(`No change was made to page '${sanitizedUrl}'`) + this.log.info(`Page edited successfully: '${sanitizedUrl}'`) } - } catch(error) { - this.log.error(`Error creating/editing page: ${error.message}`) + } else { + this.log.info(`No change was made to page '${sanitizedUrl}'`) } + } catch(error) { + this.log.error(`Error creating/editing page: ${error.message}`) + } - return wikitext - }, - }, + return wikitext + */ + } } diff --git a/examples/hooks/lpc-wikitext-hooks.js b/examples/hooks/lpc-wikitext-hooks.js index 09d95fe..cf51466 100644 --- a/examples/hooks/lpc-wikitext-hooks.js +++ b/examples/hooks/lpc-wikitext-hooks.js @@ -1,7 +1,7 @@ export const Hooks = { - parse: {}, + parser: {}, - print: { + formatter: { async enter(section) { const {sectionName, sectionContent} = section diff --git a/examples/hooks/lua-markdown-hooks.js b/examples/hooks/lua-markdown-hooks.js index dc287b1..d290849 100644 --- a/examples/hooks/lua-markdown-hooks.js +++ b/examples/hooks/lua-markdown-hooks.js @@ -1,7 +1,7 @@ export const Hooks = { - parse: {}, + parser: {}, - print: { + formatter: { async enter({name, section}) { if(name === "return") this.log.debug("section: %j", 1, section) diff --git a/examples/hooks/lua-wikitext-hooks.js b/examples/hooks/lua-wikitext-hooks.js index 134718d..77bfaed 100644 --- a/examples/hooks/lua-wikitext-hooks.js +++ b/examples/hooks/lua-wikitext-hooks.js @@ -1,7 +1,7 @@ export const Hooks = { - parse: {}, + parser: {}, - print: { + formatter: { async end(module) { const {moduleContent} = module diff --git a/examples/hooks/package-lock.json b/examples/hooks/package-lock.json new file mode 100644 index 0000000..8429ced --- /dev/null +++ b/examples/hooks/package-lock.json @@ -0,0 +1,150 @@ +{ + "name": "hooks", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hooks", + "version": "1.0.0", + "license": "NPH", + "dependencies": { + "@gesslar/toolkit": "^3.37.0", + "@gesslar/wikid": "^2.3.0" + } + }, + "node_modules/@gesslar/colours": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gesslar/colours/-/colours-0.8.0.tgz", + "integrity": "sha512-Sy+xwKAqoE+qVZ/0jvoVRsXEaNMJTc2pEUoyoRYewlAw5k4iLuIGR6cBcZ10W1UvgYTJKxh+462+Eg0QBAx44w==", + "license": "Unlicense", + "bin": { + "colours": "src/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/toolkit": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-3.37.0.tgz", + "integrity": "sha512-w+tHyvyMKhLpPZ2CLv6rsdwclSxe+Vm+dlJ853QoZwOVccgw9U6/i/CIsmv4l6UA0Y0MT0y1sRC88xgiGJb3jA==", + "hasInstallScript": true, + "license": "Unlicense", + "dependencies": { + "@gesslar/colours": "^0.8.0", + "ajv": "^8.18.0", + "json5": "^2.2.3", + "supports-color": "^10.2.2", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/@gesslar/wikid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@gesslar/wikid/-/wikid-2.3.0.tgz", + "integrity": "sha512-VyWUaZLLLCzQ5G0YDJI6yT+PzsgeUtzONQpl0flt6cIYmwb/pXr/Dph8FfhG/BgJuMXy0Z+RPNgiP1RZhrSVDw==", + "license": "Unlicense", + "dependencies": { + "@gesslar/toolkit": "^3.6.3" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/examples/hooks/package.json b/examples/hooks/package.json new file mode 100644 index 0000000..65c2a51 --- /dev/null +++ b/examples/hooks/package.json @@ -0,0 +1,13 @@ +{ + "name": "hooks", + "version": "1.0.0", + "description": "", + "keywords": [], + "author": "", + "license": "NPH", + "type": "module", + "dependencies": { + "@gesslar/toolkit": "^3.37.0", + "@gesslar/wikid": "^2.3.0" + } +} diff --git a/examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js b/examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js deleted file mode 100644 index e57c0ce..0000000 --- a/examples/node_modules_test/bedoc-lpc-markdown-combined/bedoc-lpc-markdown-combined.js +++ /dev/null @@ -1,751 +0,0 @@ -export const actions = [ - { - meta: Object.freeze({ - action: "parse", - language: "lpc", - }), - - patterns: { - commentStart: /^\s*\/\*\*(.*)$/, // Match start of a docblock - commentEnd: /^\s*\*\/\s*$/, // Match end of a docblock - commentContinuation: /^\s*\*\s?(?.*)$/, // Match continuation of a docblock - functionPattern: /^\s*(?public|protected|private)?\s*(?nomask|varargs)?\s*(?nomask|varargs)?\s*(?(int|float|void|string|object|mixed|mapping|array|buffer|function)\s*\*?)\s*(?[a-zA-Z_][a-zA-Z0-9_]*)\s*\((?.*)\)\s*\{?.*$/, - blankLine: /^\s*$/, // Match blank lines - argArray: /\w+(\s*\[\s *\]\s *)?/, - tagContent: /^\{(?\w+(?:\|\w+)*(?:\*)?)\}\s+(?(\w+(\.\w?)*=?\w*\s*(?\.{3})?|\[\w+=?.*]))(?:\s+-)?\s+(?.*)$/, - returnContent: /^\{(?[^}]*)\}(?:\s+(?:-\s+)?(?.*))?$/, - }, - - tags: { - all: [ - "brief", "description", "param", "returns?", "example", - "meta", "name", "deprecated", - ], - singletons: ["name", "return", "example", "meta", "deprecated"], - convert: {returns: "return"}, - normalize: tag => this.tags.convert[tag] || tag, - isTagValid(tag) { - const tags = this.tags - return [ - ...tags.all, - ...Object.keys(tags.convert), - ...Object.values(tags.convert), - ].includes(tag) - }, - }, - - resetState(full = false) { - if(full === true) - this.processing = false - - this.processingComment = false - this.currentTag = null - }, - - async setup({parent, log}) { - this.parent = parent - this.log = log - this.resetState() - this.regex = { - ...this.patterns, - tag: new RegExp( - `^\\s*\\*\\s+@(?${[...this.tags.all].join("|")})\\s?(?.*)$`, - ), - } - }, - - /** - * Parse the content of an LPC file and send it to BeDoc - * @param {object} module The file name to parse. - * @param {string} module.file The file object representing the current being processed - * @param {object} module.moduleContent The content of the file to parse. - * @returns {object} The result of the parse operation. - */ - async run(module) { - const {file: {module: moduleName}, moduleContent} = module - - this.resetState() - const result = [] - - const lines = moduleContent.split(/\r?\n/) - let func = null - let position = 0 - const length = lines.length - - for(; position < length; position++) { - const line = lines[position] - const lineTrimmed = line.trim() - - // Skip empty lines unless we're processing a comment - if(!this.processingComment && !lineTrimmed.length) { - continue - // Check for start of doc comment block - } else if(this.isCommentStart(lineTrimmed)) { - // Restart with a new function - func = this.newFunction() - } else if(this.isCommentEnd(lineTrimmed)) { - this.resetState() - continue - } else if(this.isFunctionLine(lineTrimmed)) { - const {status, signature} = - this.extractFunctionSignature(lineTrimmed) - - this.resetState(true) - if(status === "success") { - // Only do this if we actually have any content, tho - if(Object.keys(func ?? {}).length > 0) - result.push({...func, signature}) - else - continue - } else { - return { - status: "error", - file: moduleName, - line, - lineNumber: position + 1, - error: new Error("Problem determining function name.") - } - } - - continue - } else if(this.processingComment) { - const processed = this.processLine({ - line, - func, - file: moduleName, - position - }) - - const {status,error} = processed - if(status === "error") - return { - status, - file: moduleName, - line, - lineNumber: position + 1, - error: error - } - } - } - - return {status: "success", result} - }, - - /** - * Determines if a line is a comment start. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is the start of a comment. - */ - isCommentStart(line) { - // Only consider it a new doc block start if we're not already in a - // comment, or not processing at all. - return !this.processing && - !this.processingComment && - this.regex.commentStart.test(line) - }, - - /** - * Determines if a line is a comment end. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is the end of a comment. - */ - isCommentEnd(line) { - return this.processing && - this.processingComment && - this.regex.commentEnd.test(line) - }, - - /** - * Create a new function object. - * @returns {object} A new function object. - */ - newFunction() { - this.resetState() - this.processing = true - this.processingComment = true - return {} - }, - - /** - * Generate a formatted message. - * @param {string} message - The message to log. - * @param {string} funcName - The function name that generated the message. - * @param {string} file - The file name that generated the message. - * @param {number} position - The line number in the source file. - * @param {string} line - The line of code in the source file. - * @returns {string} The formatted message. - */ - generateMessage(message, funcName, file, position, line) { - return `[${funcName}] ${message}: ${file.moduleName}:${position + 1} - ${line}` - }, - - /** - * Process a line of code. - * @param {object} params - The parameters for processing the line. - * @param {string} params.line - The line to process. - * @param {object} params.func - The function object being processed. - * @param {string} params.file - The file name being processed. - * @param {number} params.position - The line number being processed. - * @returns {object} The result of the line processing. - */ - processLine({line, func, file, position}) { - const lineTrimmed = line.trim() - const msg = this.generateMessage - - if(!func) - return { - status: "error", - error: new Error(msg("No function context", "processLine", file, position, line)), - } - - const tagMatches = this.regex.tag.exec(line) - if(tagMatches) { - const {tag, content} = tagMatches.groups - const isValid = this.tags.isTagValid.call(this, tag) - - if(!isValid) - return { - status: "error", - error: new Error(msg(`Invalid tag \`${tag}\``, "processLine", file, position, line)), - } - - const singleton = this.tags.singletons.includes(tag) - - if(singleton) { - if(func[tag]) - return { - status: "error", - error: new Error(msg(`Singleton tag already exists: ${tag}`, "processLine", file, position, line)), - } - - func[tag] = null - } else { - func[tag] = func[tag] || [] - } - - this.currentTag = tag - this.section = null - - if(tag === "return") { - this.section = {tag, name: null} - const tagContentMatches = this.regex.returnContent.exec(content) - if(tagContentMatches) { - const {type, content} = tagContentMatches.groups - if(!type) - return { - status: "error", - error: new Error(msg(`Missing return type: ${tag}`, "processLine", file, position, line)), - } - - if(!content) { - return { - status: "error", - error: new Error(msg(`Missing return content: ${tag}`, "processLine", file, position, line)) - } - } else { - singleton - ? (func[tag] = {type, content: [content]}) - : func[tag].push({type, content: [content]}) - } - } else { - return { - status: "error", - error: new Error(msg("Failed to parse return tag", "processLine", file, position, line)), - } - } - } else { - const tagContentMatches = this.regex.tagContent.exec(content) - if(tagContentMatches) { - const {type, name, rest, content} = tagContentMatches.groups - if(!type) - return { - status: "error", - error: new Error(msg("Missing tag type", "processLine", file, position, line)), - } - - if(!name) - return { - status: "error", - error: new Error(msg("Missing tag name", "processLine", file, position, line)), - } - - this.section = {tag, name} - const result = { - type, - name, - rest: Boolean(rest), - content: [content] - } - - singleton ? func[tag] = result : func[tag].push(result) - } else { - // This is probably a singleton - if(this.tags.singletons.includes(tag)) { - this.section = {tag, name: null} - func[tag] = [] - - // If we have content, we should add it here. - content && func[tag].push(content) - } else { - return { - status: "error", - error: new Error(msg("Failed to parse tag", "processLine", file, position, line)), - } - } - } - } - - return {status: "success", message: "Processed tag"} - } - - // Process multiline content - if(this.currentTag) { - if(this.section?.name) { - const currentTag = this.currentTag - const {tag, name} = this.section - - const index = name - ? func[tag].findIndex(item => item.name === name) - : null - const tagMatch = this.regex.commentContinuation.exec(lineTrimmed) - - if(tagMatch && tagMatch.groups?.content) { - if(index > -1) - func[currentTag][index].content.push(tagMatch.groups.content) - else - func[currentTag].content.push(tagMatch.groups.content) - } else { - if(index !== null) - func[currentTag][index].content.push("") - else - func[currentTag].content.push("") - } - } else { - const {tag} = this.section - const commentMatch = this.regex.commentContinuation.exec(lineTrimmed) - if(commentMatch && commentMatch.groups?.content) { - if(func[tag].content) - func[tag].content.push(commentMatch.groups.content) - else - func[tag].push(commentMatch.groups.content) - } else { - if(func[tag].content) - func[tag].content.push("") - else - func[tag].push("") - } - } - - return {status: "success", message: "Processed tag continuation"} - } - - // If not a special tag, treat as description - const descMatch = this.regex.commentContinuation.exec(lineTrimmed) - if(descMatch && descMatch.groups?.content) { - func.description = func.description || [] - func.description.push(descMatch.groups.content) - return {status: "success", message: "Processed description"} - } else { - func.description = func.description || [] - func.description.push("") - return {status: "success", message: "Processed description"} - } - }, - - /** - * Determines if a line is a function definition. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is a function definition. - */ - isFunctionLine(line) { - return this.processing && - !this.processingComment && - this.regex.functionPattern.test(line) - }, - - /** - * Determine the function name from a line. - * @param {string} line - The line to determine the function name from. - * @returns {object} The result of the function name determination. - */ - extractFunctionSignature(line) { - const match = this.regex.functionPattern.exec(line) - - if(!match?.groups?.name) - return { - status: "error", - error: new Error(`Failed to extract function from line: ${line}`), - } - - const groups = match.groups - const signature = { - name: groups.name, - access: groups.access ?? "public", - type: groups.type ?? "void", - modifiers: [groups.modifier1, groups.modifier2].filter(Boolean), - parameters: groups.parms?.split(",").map(p => p.trim()) ?? [] - } - - return {status: "success", signature} - }, - }, - { - meta: Object.freeze({ - action: "print", - format: "markdown", - }), - - setup({parent,log}) { - this.parent = parent - this.log = log - this.documentExtension = ".md" - }, - - /** - * This is the action to print structured object to text. - * @param {object} module Data coming in from the printer - * @param {object} module.file The file object representing the file - * being currently being processed - * @param {object[]} module.moduleContent An array of objects containing - * function definitions prepared by the parser. - * @returns {Promise} The result of the print operations. - */ - async run(module) { - const hook = this.hook ?? (async() => null) - const debug = this.log.newDebug() - const {START, SECTION_LOAD, ENTER, EXIT, END} = this.HOOKS ?? {} - - const {file: {module: moduleName}, moduleContent} = - await hook(START, module) ?? module - - debug("Printing module", 3, moduleName) - - const sorted = - moduleContent?.sort(function(a, b) { - return a.name?.localeCompare(b.name) - }) ?? module.moduleContent - - if(sorted === undefined || sorted.length === 0) - return { - status: "warning", - warning: `No functions to print for module: \`${moduleName}\`` - } - - const moduleOutput = [] - - /** - * Generic section printer - * @param {string} sectionName - The section name - * @param {object} sectionContent - The function section to process - * @param {(sectionContent: unknown) => string} formatContent - Callback to format the content - * @returns {Promise} The formatted content - */ - async function printIt(sectionName, sectionContent, formatContent) { - // If we don't even have anything, nevermind? lulz - if(!sectionContent) - return null - - // ENTER - should return the exactly same shaped object as was passed - // to it. - const enter = await hook(ENTER, {moduleName,sectionName,sectionContent}) - - // Whew, that was a lot of work far! We should now get a string result. - const formatted = formatContent(enter?.sectionContent || sectionContent) - - // EXIT - should take the string so far, and return even more string. - // Well, not _MORE_ string, but... shut up. - const exit = await hook( - EXIT, { - moduleName, - sectionName, - sectionContent: formatted - } - ) ?? formatted - - return exit - } - - for(const section of sorted) { - const work = await hook(SECTION_LOAD, {moduleName, section}) - ?? section - let output, sectionName - const sectionOutput = new Map() - - // 1. Print the section name - sectionName = "name" - output = await printIt(sectionName, section.signature.name, w => - `## ${w}` - ) - output && sectionOutput.set(sectionName, output) - - // 2. Print the signature - sectionName = "signature" - output = await printIt(sectionName, work[sectionName], w => { - return `${w.access} `+ - `${w.modifiers.length?w.modifiers.join(" ")+" ":""}`+ - `*${w.type}* **${w.name}**`+ - `(${w.parameters.join(", ")})` - }) - output && sectionOutput.set(sectionName, output) - - // 2. Print the section description - sectionName = "description" - output = await printIt(sectionName, work[sectionName], w => - w.length ? w.map(line => line.trim()).join("\n") : "" - ) - output && sectionOutput.set(sectionName, output) - - // 3. Print the section parameters - sectionName = "param" - output = await printIt(sectionName, work[sectionName], w => { - const params = w.map(p => { - - // capture detailed name info - let optionalParam, paramName, defaultValue - - // Determine if this is an optional parameter - const optionalMatch = p.name.match(/^\[(.*)\]$/) - if(optionalMatch) { - optionalParam = true - paramName = optionalMatch[1] - } else { - paramName = p.name - } - - // Determine if there is a default value - const defaultMatch = paramName.match(/(.*)=(.*)/) - defaultValue = defaultMatch ? defaultMatch[2] : null - paramName = defaultMatch ? defaultMatch[1] : paramName - - let optionalAndOrDefault = optionalParam || defaultValue - ?(() => { - if(optionalParam && defaultValue) - return ` (Optional. Default: ${defaultValue})` - else if(optionalParam) - return " (Optional)" - else if(defaultValue) - return ` (Default: ${defaultValue})` - else - throw new Error("Uhm, we seem to have hit a bump.") - })() - : "" - - const content = p.content - while(content.length && (!content.at(0) || !content.at(-1))) { - if(!content.at(0)) - content.shift() - - if(!content.at(-1)) - content.pop() - } - - return `**${paramName}** *${p.type}${optionalAndOrDefault}*\n\n` + - `: ${content.map(c => c.trim()).join(" ")}` - }) ?? [] - return params.join("\n") - }) - output && sectionOutput.set(sectionName, output) - - // 4. Print the section return - output = await printIt(sectionName, work[sectionName], w => w - ? `### Returns\n\n**${w.type}** `+ - `${w.content?.map(c => c.trim()).join(" ") ?? ""}` - : "" - ) - output && sectionOutput.set(sectionName, output) - - // 5. Print the section example - sectionName = "example" - output = await printIt(sectionName, work[sectionName], w => w.length - ? "### Example\n\n" + w.join("\n") - : "" - ) - output && sectionOutput.set(sectionName, output) - moduleOutput.push(Array.from(sectionOutput.values()).join("\n\n")) - } - - debug(`Printing complete for module \`${moduleName}\``, 3) - - const joinedOutput = moduleOutput.join("\n") - const finalOutput = await hook( - END, { - moduleName, - moduleContent: joinedOutput - } - ) ?? joinedOutput - - return { - status: "success", - message: "File printed successfully", - destFile: `${moduleName}${this.documentExtension}`, - destContent: finalOutput, - } - }, - - /** - * Wraps text to a specified width with optional indentation - * @param {string} str - The text to wrap - * @param {number} [wrapAt] - The column at which to wrap the text - * @param {number} [indentAt] - The number of spaces to indent wrapped lines - * @returns {string} The wrapped text - */ - wrap(str, wrapAt = 80, indentAt = 0) { - const sections = str.split("\n").map(section => { - let parts = section.split(" ") - let inCodeBlock = false - let isStartOfLine = true // Start of each section is start of line - - // Preserve leading space if it existed - if(section[0] === " ") - parts = ["", ...parts] - - let running = 0 - - parts = parts.map(part => { - // Only check for code block if we're at start of line - if(isStartOfLine && /^```(?:\w+)?$/.test(part)) { - inCodeBlock = !inCodeBlock - running += part.length + 1 - isStartOfLine = false - return part - } - - if(part[0] === "\n") { - running = 0 - isStartOfLine = true // Next part will be at start of line - return part - } - - running += part.length + 1 - isStartOfLine = false // No longer at start of line - - if(!inCodeBlock && running >= wrapAt) { - running = part.length + indentAt - isStartOfLine = true // After newline, next part will be at start - return "\n" + " ".repeat(indentAt) + part - } - - return part - }) - - return parts - .join(" ") - .split("\n") - .map(line => line.trimEnd()) - .join("\n") - }) - - return sections.join("\n") - }, - }, -] - -export const contracts = [ - ` -type: object -properties: - provides: - type: object - properties: - functions: - type: array - items: - type: object - properties: - name: - type: string - description: - type: array - items: - type: string - param: - type: array - items: - type: object - properties: - type: - type: string - name: - type: string - content: - type: array - items: - type: string - return: - type: object - properties: - type: - type: string - content: - type: array - items: - type: string - example: - type: array - items: - type: string -`, - ` -type: object -properties: - accepts: - type: object - required: - - functions - properties: - functions: - type: array - items: - type: object - required: - - name - - return - properties: - name: - type: string - description: - type: array - items: - type: string - param: - type: array - items: - type: object - required: - - type - - name - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - name: - type: string - content: - type: array - items: - type: string - return: - type: object - required: - - type - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - content: - type: array - items: - type: string - example: - type: array - items: - type: string - -`, -] diff --git a/examples/node_modules_test/bedoc-lpc-markdown-combined/package.json b/examples/node_modules_test/bedoc-lpc-markdown-combined/package.json deleted file mode 100644 index 5371102..0000000 --- a/examples/node_modules_test/bedoc-lpc-markdown-combined/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "bedoc-lpc-parser", - "version": "1.0.0", - "type": "module", - "main": "bedoc-lpc-parser.js", - "description": "LPC parser for BeDoc", - "exports": { - ".": "./bedoc-lpc-markdown-combined.js" - }, - "bedoc": { - "actions": [ - "bedoc-lpc-markdown-combined.js" - ] - } -} diff --git a/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.js b/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.js index 35c72c6..a0550f7 100644 --- a/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.js +++ b/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.js @@ -1,394 +1,359 @@ -export const actions = [ - { - meta: Object.freeze({ - action: "parse", - language: "lpc", - }), - - patterns: { - commentStart: /^\s*\/\*\*(.*)$/, // Match start of a docblock - commentEnd: /^\s*\*\/\s*$/, // Match end of a docblock - commentContinuation: /^\s*\*\s?(?.*)$/, // Match continuation of a docblock - functionPattern: /^\s*(?public|protected|private)?\s*(?nomask|varargs)?\s*(?nomask|varargs)?\s*(?(int|float|void|string|object|mixed|mapping|array|buffer|function)\s*\*?)\s*(?[a-zA-Z_][a-zA-Z0-9_]*)\s*\((?.*)\)\s*\{?.*$/, - blankLine: /^\s*$/, // Match blank lines - argArray: /\w+(\s*\[\s *\]\s *)?/, - tagContent: /^\{(?\w+(?:\|\w+)*(?:\*)?)\}\s+(?(\w+(\.\w?)*=?\w*\s*(?\.{3})?|\[\w+=?.*]))(?:\s+-)?\s+(?.*)$/, - returnContent: /^\{(?[^}]*)\}(?:\s+(?:-\s+)?(?.*))?$/, - }, - - tags: { - all: [ - "brief", "description", "param", "returns?", "example", - "meta", "name", "deprecated", - ], - singletons: ["name", "return", "example", "meta", "deprecated"], - convert: {returns: "return"}, - normalize: tag => this.tags.convert[tag] || tag, - isTagValid(tag) { - const tags = this.tags - return [ - ...tags.all, - ...Object.keys(tags.convert), - ...Object.values(tags.convert), - ].includes(tag) - }, - }, - - resetState(full = false) { - if(full === true) - this.processing = false - - this.processingComment = false - this.currentTag = null - }, - - async setup({parent, log}) { - this.parent = parent - this.log = log - this.resetState() - this.regex = { - ...this.patterns, - tag: new RegExp( - `^\\s*\\*\\s+@(?${[...this.tags.all].join("|")})\\s?(?.*)$`, - ), - } - }, - - /** - * Parse the content of an LPC file and send it to BeDoc - * @param {object} module The file name to parse. - * @param {string} module.file The file object representing the current being processed - * @param {object} module.moduleContent The content of the file to parse. - * @returns {object} The result of the parse operation. - */ - async run(module) { - const {file: {module: moduleName}, moduleContent} = module - - this.resetState() - const result = [] - - const lines = moduleContent.split(/\r?\n/) - let func = null - let position = 0 - const length = lines.length - - for(; position < length; position++) { - const line = lines[position] - const lineTrimmed = line.trim() - - // Skip empty lines unless we're processing a comment - if(!this.processingComment && !lineTrimmed.length) { - continue - // Check for start of doc comment block - } else if(this.isCommentStart(lineTrimmed)) { - // Restart with a new function - func = this.newFunction() - } else if(this.isCommentEnd(lineTrimmed)) { - this.resetState() - continue - } else if(this.isFunctionLine(lineTrimmed)) { - const {status, signature} = - this.extractFunctionSignature(lineTrimmed) - - this.resetState(true) - if(status === "success") { - // Only do this if we actually have any content, tho - if(Object.keys(func ?? {}).length > 0) - result.push({...func, signature}) - else - continue - } else { - return { - status: "error", - file: moduleName, - line, - lineNumber: position + 1, - error: new Error("Problem determining function name.") - } - } - - continue - } else if(this.processingComment) { - const processed = this.processLine({ - line, - func, - file: moduleName, - position - }) - - const {status,error} = processed - if(status === "error") - return { - status, - file: moduleName, - line, - lineNumber: position + 1, - error: error - } +/** + * @file LPC Parser - A parser for extracting documentation from LPC (LPC + * Programming Language) files. + * + * This parser specifically handles LPC function documentation comments and + * extracts structured information including descriptions, parameters, return + * types, and examples. + * + * The parser uses a contract-based approach defined in lpc-parser.yaml and + * integrates with the BeDoc documentation system through ActionBuilder. + * + * @author gesslar + * @version 1.0.0 + * @since 1.0.0 + */ + +import {ActionBuilder, ACTIVITY} from "@gesslar/actioneer" +import {Collection, Data, Util} from "@gesslar/toolkit" + +const {WHILE} = ACTIVITY + +/** + * LPC Parser Class - Parses LPC files to extract function documentation. + * + * This parser is designed to work with LPC (LPC Programming Language) source + * files, extracting LPCDoc comments and function signatures. It + * identifies functions with their access modifiers, types, parameters, and + * associated documentation. + * + * @class + */ +export default class LpcParser { + /** + * Parser metadata defining its characteristics and contract. + * + * @readonly + * @type {object} + * @property {string} kind - The type of action. + * @property {string} input - The input file type this parser handles. + * @property {string} terms - The contract file name. + */ + static meta = Object.freeze({ + kind: "parser", + input: "lpc", + terms: "ref://./bedoc-lpc-parser.yaml" + }) + + /** + * Configures the parser using ActionBuilder's fluent API. + * + * This method sets up the parsing structure and extraction methods for LPC + * documentation. + * + * It defines: + * - Comment block structure (JSDoc-style comments) + * - Function signature patterns with LPC-specific modifiers + * - Extraction methods for descriptions, tags, and return values + * + * @param {ActionBuilder} builder - The ActionBuilder instance to configure + * @returns {ActionBuilder} The configured builder instance + * @example + * // LPC function pattern matched: + * // public varargs int my_function(string arg1, object *args) { + * @see ActionBuilder + */ + + setup = builder => builder + .do("Extract blocks", this.#extractBlocks) + .do("Process functions", ACTIVITY.SPLIT, + ctx => ctx, // splitter + ctx => ctx, // rejoiner + new ActionBuilder() + .do("Extract signature", this.#johnHandcock) + .do("Extract description", this.#extractDescription) + .do("Extract tags", WHILE, ctx => { + return ctx.lines.length > 0 + }, this.#extractTag) + ) + .done(this.#finally) + + async #extractBlocks(ctx) { + ctx = Data.append(ctx, "\n") + + const result = [] + const lines = ctx.split("\n") + + while(lines.length) { + const block = {} + + // Find the block start index + const startIndex = lines.findIndex(line => this.#regexes.get("block-start").exec(line)) + // Find the block end index + const endIndex = lines.findIndex(line => this.#regexes.get("block-stop").exec(line)) + + // Hmmmmm! I guess we done here? + if(startIndex < 0 || endIndex <= startIndex) + break + + // The block is the stuff in between the start and the end + block.lines = lines.slice(startIndex+1, endIndex) + + // Ok, yeet out the stuff we don't need anymore; the block's size + the + // begin and end. I added +1 cos I don't know how math works, I guess, + // but now it is properly gobbling up the */ + lines.splice(0, endIndex+1) + + // Find the function + const idIndex = lines.findIndex(line => this.#regexes.get("function").test(line)) + // Find the next block + const nextBlockIndex = lines.findIndex(line => this.#regexes.get("block-start").test(line)) + + if(idIndex > -1) { + // they can't be equal, they're different patterns + if(nextBlockIndex !== -1 && idIndex > nextBlockIndex) { + // but if the found function ID is later than the next block ID, + // that means we don't have one for this block. EJECT! EJECT! EJECT! + lines.splice(0, nextBlockIndex) + } else { + // Whew! Safe. + + // Set the function match as a property on the array for later + // somethingspection. + const func = this.#regexes.get("function").exec(lines[idIndex]) + block.function = func + + // Slurp! Slurp! + lines.splice(0, 1) } } - return {status: "success", result} - }, - - /** - * Determines if a line is a comment start. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is the start of a comment. - */ - isCommentStart(line) { - // Only consider it a new doc block start if we're not already in a - // comment, or not processing at all. - return !this.processing && - !this.processingComment && - this.regex.commentStart.test(line) - }, - - /** - * Determines if a line is a comment end. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is the end of a comment. - */ - isCommentEnd(line) { - return this.processing && - this.processingComment && - this.regex.commentEnd.test(line) - }, - - /** - * Create a new function object. - * @returns {object} A new function object. - */ - newFunction() { - this.resetState() - this.processing = true - this.processingComment = true - return {} - }, - - /** - * Generate a formatted message. - * @param {string} message - The message to log. - * @param {string} funcName - The function name that generated the message. - * @param {string} file - The file name that generated the message. - * @param {number} position - The line number in the source file. - * @param {string} line - The line of code in the source file. - * @returns {string} The formatted message. - */ - generateMessage(message, funcName, file, position, line) { - return `[${funcName}] ${message}: ${file.moduleName}:${position + 1} - ${line}` - }, - - /** - * Process a line of code. - * @param {object} params - The parameters for processing the line. - * @param {string} params.line - The line to process. - * @param {object} params.func - The function object being processed. - * @param {string} params.file - The file name being processed. - * @param {number} params.position - The line number being processed. - * @returns {object} The result of the line processing. - */ - processLine({line, func, file, position}) { - const lineTrimmed = line.trim() - const msg = this.generateMessage - - if(!func) - return { - status: "error", - error: new Error(msg("No function context", "processLine", file, position, line)), - } + result.push(block) + } + + return result + } + + // Gimme k/v object that only has k where v isn't null or undefined. + // You see that, mistermadammissus PR robot, some of us know that != + // against null means undefined _OR_ null. People who don't maybe need + // to get their education checked. + #gimme = ob => + Object.fromEntries(Object.entries(ob).filter(([_, v]) => v != null)) + + #johnHandcock = ctx => { + const {function: func} = ctx + const signature = this.#gimme(func?.groups ?? {}) + + return Object.assign(ctx, {signature}) + } + + /** + * Extracts the description section from JSDoc-style comment lines. + * + * Processes comment lines to extract the main description text that appears + * before any \@tag declarations. The description continues until it + * encounters a line starting with an @ symbol (indicating a JSDoc tag). + * + * @param {Array} curr - Array of comment lines to process + * @returns {Promise | null>} Array of description lines, or null if empty + * @private + * @example + * // Input comment lines: + * // * This is the main description + * // * of the function. + * // * @param {string} arg - An argument + * // + * // Returns: ["This is the main description", "of the function."] + */ + #extractDescription = ctx => { + const {lines} = ctx + + const comment = this.#regexes.get("comment-line") + const tagId = this.#regexes.get("tag-id") + + const description = [] + Object.assign(ctx, {description}) + + if(!(comment.test(lines.join("\n")))) + return ctx + + while(lines.length > 0) { + const line = lines.shift() + + if(!comment.test(line)) + continue + + if(tagId.test(line)) { + lines.unshift(line) + break + } - const tagMatches = this.regex.tag.exec(line) - if(tagMatches) { - const {tag, content} = tagMatches.groups - const isValid = this.tags.isTagValid.call(this, tag) + const {content} = comment.exec(line)?.groups ?? {} + description.push(content ?? "") + } + + return ctx + } + + /** + * Extracts JSDoc-style tags from comment lines. + * + * Uses a complex regex pattern to capture multi-line tag content and stops + * at the next tag or end of comment block. + * + * @param {string[]} ctx - Array of comment lines to process + * @returns {Promise | null>} Array of extracted tag strings, or null if none found + * @private + * @async + * @example + * // Input comment lines: + * // * @param {string} name - The user's name + * // * @param {number} age - The user's age + * // + * // Returns: [ + * // "@param {string} name - The user's name", + * // "@param {number} age - The user's age", + * // ] + */ + #extractTag = async ctx => { + const {lines, tag: extractedTags = {}} = ctx + + const comment = this.#regexes.get("comment-line") + const tagId = this.#regexes.get("tag-id") + // narrower to more broader + const patterns = ["return", "example", "tag"].map(e => this.#regexes.get(e)) + + const line = lines.shift() + + // Just consume it. We don't need it. + if(!comment.test(line)) + return ctx + + // If this isn't the start of a tag, consume it, too. + if(!tagId.test(line)) + return ctx + + const pattern = patterns.find(e => e.test(line)) + + // No supported tag pattern worked. So, consume. Man, we doing some + // mad nom! + if(!pattern) + return ctx + + // First, let's grab the matches + const {groups} = pattern.exec(line) ?? {} + if(!groups) // something above lied to us! + return ctx // gobble gobble + + const {tag} = groups + if(!tag) + return ctx + + delete groups.tag + if(groups.content) + groups.content = [groups.content] + else + groups.content = [] + + while(lines.length > 0) { + const continued = lines.shift() + + // Anything _not_ a new tag is content. + if(tagId.test(continued)) { + // oops! put it back before anybody notices! + lines.unshift(continued) + + // whistle and step away! + break + } - if(!isValid) - return { - status: "error", - error: new Error(msg(`Invalid tag \`${tag}\``, "processLine", file, position, line)), - } + const {content} = comment.exec(continued)?.groups ?? {} - const singleton = this.tags.singletons.includes(tag) + groups.content.push(content ?? "") + } - if(singleton) { - if(func[tag]) - return { - status: "error", - error: new Error(msg(`Singleton tag already exists: ${tag}`, "processLine", file, position, line)), - } + const curr = extractedTags[tag] ?? [] + curr.push(groups) - func[tag] = null - } else { - func[tag] = func[tag] || [] - } + Object.assign(extractedTags, {[tag]: curr}) - this.currentTag = tag - this.section = null - - if(tag === "return") { - this.section = {tag, name: null} - const tagContentMatches = this.regex.returnContent.exec(content) - if(tagContentMatches) { - const {type, content} = tagContentMatches.groups - if(!type) - return { - status: "error", - error: new Error(msg(`Missing return type: ${tag}`, "processLine", file, position, line)), - } - - if(!content) { - return { - status: "error", - error: new Error(msg(`Missing return content: ${tag}`, "processLine", file, position, line)) - } - } else { - singleton - ? (func[tag] = {type, content: [content]}) - : func[tag].push({type, content: [content]}) - } - } else { - return { - status: "error", - error: new Error(msg("Failed to parse return tag", "processLine", file, position, line)), - } - } - } else { - const tagContentMatches = this.regex.tagContent.exec(content) - if(tagContentMatches) { - const {type, name, rest, content} = tagContentMatches.groups - if(!type) - return { - status: "error", - error: new Error(msg("Missing tag type", "processLine", file, position, line)), - } - - if(!name) - return { - status: "error", - error: new Error(msg("Missing tag name", "processLine", file, position, line)), - } - - this.section = {tag, name} - const result = { - type, - name, - rest: Boolean(rest), - content: [content] - } - - singleton ? func[tag] = result : func[tag].push(result) - } else { - // This is probably a singleton - if(this.tags.singletons.includes(tag)) { - this.section = {tag, name: null} - func[tag] = [] - - // If we have content, we should add it here. - content && func[tag].push(content) - } else { - return { - status: "error", - error: new Error(msg("Failed to parse tag", "processLine", file, position, line)), - } - } - } - } + return Object.assign(ctx, {tag: extractedTags}) + } - return {status: "success", message: "Processed tag"} + /** + * Final processing method called after all extraction is complete. + * + * @param {Array} ctx - Map of extracted data from the parsing process + * @returns {Promise>} The transformation results. + * @private + */ + async #finally(ctx) { + const functions = await Collection.asyncMap(ctx, async func => { + const result = { + name: func.function.groups.name, + description: func.description, + signature: func.signature, } - // Process multiline content - if(this.currentTag) { - if(this.section?.name) { - const currentTag = this.currentTag - const {tag, name} = this.section - - const index = name - ? func[tag].findIndex(item => item.name === name) - : null - const tagMatch = this.regex.commentContinuation.exec(lineTrimmed) - - if(tagMatch && tagMatch.groups?.content) { - if(index > -1) - func[currentTag][index].content.push(tagMatch.groups.content) - else - func[currentTag].content.push(tagMatch.groups.content) - } else { - if(index !== null) - func[currentTag][index].content.push("") - else - func[currentTag].content.push("") - } - } else { - const {tag} = this.section - const commentMatch = this.regex.commentContinuation.exec(lineTrimmed) - if(commentMatch && commentMatch.groups?.content) { - if(func[tag].content) - func[tag].content.push(commentMatch.groups.content) - else - func[tag].push(commentMatch.groups.content) - } else { - if(func[tag].content) - func[tag].content.push("") - else - func[tag].push("") - } - } + const tags = func.tag ?? {} - return {status: "success", message: "Processed tag continuation"} - } + if(tags.param) + result.param = tags.param + .map(({type, name, content}) => ({type, name, content})) - // If not a special tag, treat as description - const descMatch = this.regex.commentContinuation.exec(lineTrimmed) - if(descMatch && descMatch.groups?.content) { - func.description = func.description || [] - func.description.push(descMatch.groups.content) - return {status: "success", message: "Processed description"} - } else { - func.description = func.description || [] - func.description.push("") - return {status: "success", message: "Processed description"} + if(tags.return || tags.returns) { + const ret = (tags.return ?? tags.returns)[0] + result.return = {type: ret.type, content: ret.content} } - }, - - /** - * Determines if a line is a function definition. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is a function definition. - */ - isFunctionLine(line) { - return this.processing && - !this.processingComment && - this.regex.functionPattern.test(line) - }, - - /** - * Determine the function name from a line. - * @param {string} line - The line to determine the function name from. - * @returns {object} The result of the function name determination. - */ - extractFunctionSignature(line) { - const match = this.regex.functionPattern.exec(line) - - if(!match?.groups?.name) - return { - status: "error", - error: new Error(`Failed to extract function from line: ${line}`), - } - const groups = match.groups - const signature = { - name: groups.name, - access: groups.access ?? "public", - type: groups.type ?? "void", - modifiers: [groups.modifier1, groups.modifier2].filter(Boolean), - parameters: groups.parms?.split(",").map(p => p.trim()) ?? [] - } + if(tags.example || tags.examples) { + const examples = tags.example ?? tags.examples - return {status: "success", signature} - }, - }, -] + result.example = examples.flatMap(({content}) => content) + } -export const contracts = ["ref://./bedoc-lpc-parser.yaml"] + return result + }) + + return {functions} + } + + // HERE BE DRAGONS! YOU DONE BEEN WARNED, FUGGAH! + #regexes = new Map([ + ["block-start", /^\s*\/\*\*.*$/], + ["block-stop", /^\s*\*\/\s*$/], + ["comment-line", /^\s\*((?:\s)(?[\s\S]+))?/], + ["tag-id", /^\s\*\s@[a-zA-Z]/], + ["tag", Util.regexify(` + ^\\s*\\*(\\s + @(?\\w+)\\s* + \\{(?\\w+(?:\\|\\w+)*(?:\\*)?)\\}\\s+ + (?(\\w+(\\.\\w?)*=?\\w*\\s*(?\\.{3})?|\\[\\w+=?.*]))(?:\\s+-)?\\s+|\\s) + (?[\\s\\S]+?) + $ + ` + )], + ["tag-except", [/^\s*\*\s+@returns?/, /^\s*\*\s+@example\s[\s\S]\n$/]], + ["tag-stop", /^\s*\*(?:\/|\s*@)/], + ["return", /^\s*\*\s*@(?returns?)\s+\{(?[^}]*)\}(?:\s+(?:-\s+)?(?.*))?/], + ["example", /^\s\* @(?examples?)((?:\s)(?[\s\S]+))?/], + ["function", Util.regexify(` + ^\\s* + (?public|protected|private)? + \\s* + (?nomask|varargs)? + \\s* + (?nomask|varargs)? + \\s* + (?(int|float|void|string|object|mixed|mapping|array|buffer|function))\\s*\\*? + \\s* + (?[a-zA-Z_][a-zA-Z0-9_]*) + \\s* + \\((?.*)\\) + \\s* + \\{?.* + ` + )] + ]) +} diff --git a/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.yaml b/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.yaml index 7d1ffe9..470bebd 100644 --- a/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.yaml +++ b/examples/node_modules_test/bedoc-lpc-parser/bedoc-lpc-parser.yaml @@ -1,14 +1,54 @@ -# yaml-language-server: $schema=https://bedoc.gesslar.dev/schemas/v1/bedoc.action.json +# yaml-language-server: $schema=https://schema.gesslar.dev/bedoc/v1/bedoc-action.json + +$schema: https://schema.gesslar.dev/bedoc/v1/bedoc-action.json provides: type: object + required: + - functions properties: functions: type: array items: type: object + required: + - name + - signature properties: name: type: string + signature: + type: object + required: + - name + properties: + access: + type: string + enum: [protected, public, private] + modifier1: + type: string + enum: [nomask, varargs] + modifier2: + type: string + enum: [nomask, varargs] + type: + type: string + enum: + [ + array, + buffer, + float, + function, + int, + mapping, + mixed, + object, + string, + void, + ] + name: + type: string + parms: + type: string description: type: array items: @@ -17,6 +57,9 @@ provides: type: array items: type: object + required: + - type + - name properties: type: type: string @@ -28,10 +71,12 @@ provides: type: string return: type: object + required: + - type properties: type: type: string - content: + description: type: array items: type: string diff --git a/examples/node_modules_test/bedoc-lpc-parser/npm-shrinkwrap.json b/examples/node_modules_test/bedoc-lpc-parser/npm-shrinkwrap.json new file mode 100644 index 0000000..424dc8e --- /dev/null +++ b/examples/node_modules_test/bedoc-lpc-parser/npm-shrinkwrap.json @@ -0,0 +1,149 @@ +{ + "name": "bedoc-lpc-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bedoc-lpc-parser", + "version": "1.0.0", + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } + }, + "node_modules/@gesslar/actioneer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@gesslar/actioneer/-/actioneer-2.3.1.tgz", + "integrity": "sha512-KKWE1mrlLurwIVgmMKtO7T4cmCs0Maooog/Aw5hAo6aHCfbaGqTtjCIkIlEoZG+U3FzgbUx+TWhOCvESgHRDhA==", + "license": "Unlicense", + "dependencies": { + "@gesslar/toolkit": "^3.34.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/colours": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gesslar/colours/-/colours-0.8.0.tgz", + "integrity": "sha512-Sy+xwKAqoE+qVZ/0jvoVRsXEaNMJTc2pEUoyoRYewlAw5k4iLuIGR6cBcZ10W1UvgYTJKxh+462+Eg0QBAx44w==", + "license": "Unlicense", + "bin": { + "colours": "src/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/toolkit": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-3.37.0.tgz", + "integrity": "sha512-w+tHyvyMKhLpPZ2CLv6rsdwclSxe+Vm+dlJ853QoZwOVccgw9U6/i/CIsmv4l6UA0Y0MT0y1sRC88xgiGJb3jA==", + "hasInstallScript": true, + "license": "Unlicense", + "dependencies": { + "@gesslar/colours": "^0.8.0", + "ajv": "^8.18.0", + "json5": "^2.2.3", + "supports-color": "^10.2.2", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/examples/node_modules_test/bedoc-lpc-parser/package.json b/examples/node_modules_test/bedoc-lpc-parser/package.json index 847bd9a..7f1ac46 100644 --- a/examples/node_modules_test/bedoc-lpc-parser/package.json +++ b/examples/node_modules_test/bedoc-lpc-parser/package.json @@ -11,5 +11,9 @@ "actions": [ "bedoc-lpc-parser.js" ] + }, + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" } } diff --git a/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.js b/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.js index 5c6b17e..2723e43 100644 --- a/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.js +++ b/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.js @@ -1,487 +1,313 @@ -export const actions = [ - { - meta: Object.freeze({ - action: "parse", - language: "lua", - }), - - patterns: { - commentStart: /^\s*---\s?(.*)$/, // Match start of a docblock - commentContent: /^\s*---\s?(?.*)$/, // Match continuation of a docblock - functionPattern: - /^\s*function\s+(?(?[a-zA-Z_]\w*(?=[.:]))?(?[.:])?(?[a-zA-Z_]\w*))\s*\((?.+)?\)\s*(?:end)?$/, - blankLine: /^\s*$/, // Match blank lines - argArray: /\w+(\s*\[\s *\]\s *)?/, - tagContent: /^(?.*) (?.*) - (?.+)$/, - returnContent: /^\s*(?.*)\s+#\s+(?.*)?$/, - }, - - /** - * Parse a Lua type string and split it into an array of types. - * @param {string} typeStr The type string to parse (e.g., "number|nil", - * "string?"). - * @returns {string[]} An array of parsed types (e.g., ["number", "nil"]). - */ - parseLuaType(typeStr) { - if (!typeStr || typeof typeStr !== "string") return [] - - const types = [] - - // Handle nullable shorthand (e.g., "number?") - const nullableMatch = typeStr.match(/(\w+)\?/) - if (nullableMatch) { - types.push(nullableMatch[1], "nil") - typeStr = typeStr.replace(/\w+\?/, nullableMatch[1]) // Normalize by removing the "?" +/** + * @file Lua Parser - A parser for extracting documentation from Lua files. + * + * This parser specifically handles Lua function documentation comments and + * extracts structured information including descriptions, parameters, return + * types, and examples. + * + * The parser uses a contract-based approach defined in bedoc-lua-parser.yaml + * and integrates with the BeDoc documentation system through ActionBuilder. + * + * @author gesslar + * @version 1.0.0 + * @since 1.0.0 + */ + +import {ActionBuilder, ACTIVITY} from "@gesslar/actioneer" +import {Collection} from "@gesslar/toolkit" + +/** + * Lua Parser Class - Parses Lua files to extract function documentation. + * + * This parser is designed to work with Lua source files, extracting LDoc + * comments and function signatures. It identifies functions with their scope, + * delimiter, method name, parameters, and associated documentation. + * + * @class + */ +export default class LuaParser { + /** + * Parser metadata defining its characteristics and contract. + * + * @readonly + * @type {object} + * @property {string} kind - The type of action. + * @property {string} input - The input file type this parser handles. + * @property {string} terms - The contract file name. + */ + static meta = Object.freeze({ + kind: "parser", + input: "lua", + terms: "ref://./bedoc-lua-parser.yaml" + }) + + /** + * Configures the parser using ActionBuilder's fluent API. + * + * This method sets up the parsing structure and extraction methods for Lua + * documentation. + * + * It defines: + * - Comment block structure (LDoc-style --- comments) + * - Function signature patterns with Lua-specific scope/method syntax + * - Extraction methods for descriptions, tags, and return values + * + * @param {ActionBuilder} builder - The ActionBuilder instance to configure + * @returns {ActionBuilder} The configured builder instance + * @example + * // Lua function patterns matched: + * // function MyModule.my_function(arg1, arg2) + * // function MyModule:my_method(arg1, arg2) + * @see ActionBuilder + */ + setup = builder => builder + .do("Extract blocks", this.#extractBlocks) + .do("Process functions", ACTIVITY.SPLIT, + ctx => ctx, // splitter + ctx => ctx, // rejoiner + new ActionBuilder() + .do("Extract signature", this.#extractSignature) + .do("Extract description", this.#extractDescription) + .do("Extract tags", this.#extractTags) + ) + .done(this.#finally) + + #extractBlocks = ctx => { + const result = [] + const lines = ctx.split(/\r?\n/) + + while(lines.length) { + const block = {} + + // Find the start of a comment block + const startIndex = lines.findIndex(line => + this.#regexes.get("comment-start").test(line.trim()) + ) + if(startIndex < 0) + break + + // Remove everything before the comment block + lines.splice(0, startIndex) + + // Collect all consecutive comment lines + const commentLines = [] + while(lines.length && this.#regexes.get("comment-start").test(lines[0].trim())) { + commentLines.push(lines.shift()) } - // Handle union types (e.g., "number|nil") - const unionTypes = typeStr.split("|").map(type => type.trim()) - for (const type of unionTypes) { - if (!types.includes(type)) { - types.push(type) // Avoid duplicates - } - } + block.lines = commentLines - return types - }, - - tags: { - all: ["name", "param", "return", "example"], - singletons: ["name", "return", "example"], - convert: { returns: "return" }, - noContent: ["name"], - normalize: tag => this.tags.convert[tag] || tag, - isTagValid(tag) { - const tags = this.tags - return [ - ...tags.all, - ...Object.keys(tags.convert), - ...Object.values(tags.convert), - ].includes(tag) - } - }, - - resetState(full = false) { - if (full === true) - this.processing = false - - this.processingComment = false - this.currentTag = null - }, - - async setup({ parent, log }) { - this.parent = parent - this.log = log - this.resetState() - this.regex = { - ...this.patterns, - tag: new RegExp( - `^\\s*---@(?${[...this.tags.all].join("|")})\\s?(?.*)$`, - ), - } - }, - - /** - * Parse the content of an Lua file and send it to BeDoc - * @param {object} module The file name to parse. - * @param {string} module.file The file object representing the current - * being processed - * @param {object} module.moduleContent The content of the file to parse. - * @returns {object} The result of the parse operation. - */ - async run(module) { - const { file: { module: moduleName }, moduleContent } = module - - // Setup utilities - const log = this.log - const debug = log.newDebug() - - debug(`Parsing file %s`, 2, module) - - this.resetState() - const result = [] - - const lines = moduleContent.split(/\r?\n/) - let func = null - let position = 0 - const length = lines.length - - for (; position < length; position++) { - const line = lines[position] - const lineTrimmed = line.trim() - - // Skip empty lines unless we're processing a comment - if (!this.processingComment && !lineTrimmed.length) { - continue - // Check for start of doc comment block - } else if (this.isCommentStart(lineTrimmed)) { - // Restart with a new function - func = this.newFunction() - // Rewind one line to re-process this line - position-- - } else if (this.isFunctionLine(lineTrimmed)) { - const { status, signature } = - this.extractFunctionSignature(lineTrimmed, func) - - this.resetState(true) - - if (status === "success") { - // Only do this if we actually have any content, tho - if (Object.keys(func ?? {}).length > 0) - result.push({ ...func, signature }) - else - continue - } else { - return { - status: "error", - file: moduleName, - line, - lineNumber: position + 1, - error: new Error("Problem determining function name.") - } - } + // Look ahead for a function definition, stopping at the next comment start + const funcIndex = lines.findIndex(line => { + const trimmed = line.trim() - continue - } else if (this.processingComment) { - // If we are not a comment, we should turn off comment tracking - // and then continue so that we can test for function definition - if (!this.isComment(lineTrimmed)) { - this.processingComment = false - position-- - continue - } + return this.#regexes.get("function").test(trimmed) || + this.#regexes.get("comment-start").test(trimmed) + }) - const processed = this.processLine({ - line, - func, - file: moduleName, - position - }) - - const { status, error } = processed - if (status === "error") - return { - status: "error", - file: moduleName, - line, - lineNumber: position + 1, - error: error - } - } + if(funcIndex >= 0 && this.#regexes.get("function").test(lines[funcIndex].trim())) { + block.function = this.#regexes.get("function").exec(lines[funcIndex].trim()) + lines.splice(0, funcIndex + 1) + result.push(block) + } else if(funcIndex >= 0) { + // Hit another comment start before a function — discard this block + lines.splice(0, funcIndex) } - - return { status: "success", result } - }, - - isComment(line) { - return this.regex.commentContent.test(line) - }, - - /** - * Determines if a line is a comment start. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is the start of a comment. - */ - isCommentStart(line) { - // Only consider it a new doc block start if we're not already in a - // comment, or not processing at all. - return !this.processing && - !this.processingComment && - this.regex.commentStart.test(line) - }, - - /** - * Create a new function object. - * @returns {object} A new function object. - */ - newFunction() { - this.resetState() - this.processing = true - this.processingComment = true - return {} - }, - - /** - * Generate a formatted message. - * @param {string} message - The message to log. - * @param {string} funcName - The function name that generated the message. - * @param {string} file - The file name that generated the message. - * @param {number} position - The line number in the source file. - * @param {string} line - The line of code in the source file. - * @returns {string} The formatted message. - */ - generateMessage(message, funcName, file, position, line) { - return `[${funcName}] ${message}: ${file}:${position + 1} - ${line}` - }, - - /** - * Process a line of code. - * @param {object} params - The parameters for processing the line. - * @param {string} params.line - The line to process. - * @param {object} params.func - The function object being processed. - * @param {string} params.file - The file name being processed. - * @param {number} params.position - The line number being processed. - * @returns {object} The result of the line processing. - */ - processLine({ line, func, file, position }) { + // else: no more lines, block has no function — discard + } + + return result + } + + #extractSignature = ctx => { + const {function: func} = ctx + if(!func?.groups?.name) + return ctx + + const groups = func.groups + const signature = { + name: groups.name, + scope: groups.scope ?? null, + delimiter: groups.delimiter ?? null, + method: groups.method, + modifiers: [], + access: "", + parameters: groups.parms?.split(",").map(p => p.trim()) ?? [] + } + + return Object.assign(ctx, {signature}) + } + + /** + * Extracts the description section from LDoc-style comment lines. + * + * Processes comment lines to extract the main description text that appears + * before any @tag declarations. + * + * @param {object} ctx - The block context being processed + * @returns {object} The context with description added + * @private + */ + #extractDescription = ctx => { + const {lines} = ctx + + const comment = this.#regexes.get("comment-content") + const tagId = this.#regexes.get("tag-id") + + const description = [] + Object.assign(ctx, {description}) + + while(lines.length > 0) { + const line = lines[0].trim() + + if(!comment.test(line)) + break + + if(tagId.test(line)) + break + + lines.shift() + const {content} = comment.exec(line)?.groups ?? {} + description.push(content ?? "") + } + + return ctx + } + + /** + * Extracts LDoc-style tags from comment lines. + * + * Handles @param, @return/@returns, @example, and @name tags. + * + * @param {object} ctx - The block context being processed + * @returns {object} The context with tags added + * @private + */ + #extractTags = ctx => { + const {lines} = ctx + + const comment = this.#regexes.get("comment-content") + const tagPattern = this.#regexes.get("tag") + const tagContent = this.#regexes.get("tag-content") + const returnContent = this.#regexes.get("return-content") + const tagId = this.#regexes.get("tag-id") + + const extractedTags = {} + Object.assign(ctx, {tag: extractedTags}) + + while(lines.length > 0) { + const line = lines.shift() const lineTrimmed = line.trim() - const msg = this.generateMessage - if (!func) - return { - status: "error", - error: new Error(msg("No function context", "processLine", file, position, line)), - } + if(!comment.test(lineTrimmed)) + continue - const tagMatches = this.regex.tag.exec(line) + const tagMatch = tagPattern.exec(lineTrimmed) + if(!tagMatch) + continue - if (tagMatches) { - const { tag, content } = tagMatches.groups + const {tag, content} = tagMatch.groups + const normalizedTag = tag === "returns" ? "return" : tag - if (!this.tags.isTagValid.call(this, tag)) - return { - status: "error", - error: new Error(msg(`Invalid tag: ${tag}`, "processLine", file, position, line)), + if(normalizedTag === "return") { + const retMatch = returnContent.exec(content) + if(retMatch) { + const {type, content: retContent} = retMatch.groups + const types = type.split(",").map(t => t.trim()) + extractedTags["return"] = { + type: types, + content: retContent ? [retContent] : [] } + } + } else if(normalizedTag === "name") { + if(content) + extractedTags["name"] = content + } else if(normalizedTag === "example") { + const exampleLines = content ? [content] : [] + + while(lines.length > 0) { + const next = lines[0].trim() + if(!comment.test(next) || tagId.test(next)) + break + + lines.shift() + const {content: lineContent} = comment.exec(next)?.groups ?? {} + exampleLines.push(lineContent ?? "") + } - const singleton = this.tags.singletons.includes(tag) - const noContent = this.tags.noContent.includes(tag) - - if (noContent && content) { - func[tag] = func[tag] || content + extractedTags["example"] = exampleLines + } else if(normalizedTag === "param") { + const paramMatch = tagContent.exec(content) + if(paramMatch) { + const {name, type, content: paramContent} = paramMatch.groups + const paramEntry = {type, name, content: paramContent ? [paramContent] : []} - return { status: "success", message: "Processed tag" } - } + if(!extractedTags["param"]) + extractedTags["param"] = [] - if (singleton) { - if (func[tag]) - return { - status: "error", - error: new Error(msg(`Singleton tag already exists: ${tag}`, "processLine", file, position, line)), - } + extractedTags["param"].push(paramEntry) - func[tag] = null - } else { - func[tag] = func[tag] || [] - } + while(lines.length > 0) { + const next = lines[0].trim() + if(!comment.test(next) || tagId.test(next)) + break - this.currentTag = tag - this.section = null - - if (tag === "return") { - this.section = { tag, name: null } - - const tagContentMatches = this.regex.returnContent.exec(content) - - if (tagContentMatches) { - const { type, content } = tagContentMatches.groups - - if (!type) - return { - status: "error", - error: new Error(msg(`Missing return type: ${tag}`, "processLine", file, position, line)), - } - - if (!content) { - this.core.logger.warn( - msg(`Missing return content: ${tag}`, "processLine", file, position, line), - ) - singleton - ? (func[tag] = { type, content: [] }) - : func[tag].push({ type, content: [] }) - } else { - const types = type.split(",").map(t => t.trim()) - - singleton - ? (func[tag] = { type: types, content: [content] }) - : func[tag].push({ type: types, content: [content] }) - } - } else { - return { - status: "error", - error: new Error(msg("Failed to parse return tag", "processLine", file, position, line)), - } - } - } else if (!noContent) { - const tagContentMatches = this.regex.tagContent.exec(content) - - if (tagContentMatches) { - const { type, name, content } = tagContentMatches.groups - - if (!type) - return { - status: "error", - error: new Error(msg("Missing tag type", "processLine", file, position, line)), - } - - if (!name) - return { - status: "error", - error: new Error(msg("Missing tag name", "processLine", file, position, line)), - } - - this.section = { tag, name } - singleton - ? (func[tag] = { type, name, content: [content] }) - : func[tag].push({ type, name, content: [content] }) - } else { - // This is probably a singleton - if (this.tags.singletons.includes(tag)) { - this.section = { tag, name: null } - func[tag] = [] - } else { - return { - status: "error", - error: new Error(msg("Failed to parse tag", "processLine", file, position, line)), - } - } + lines.shift() + const {content: lineContent} = comment.exec(next)?.groups ?? {} + paramEntry.content.push(lineContent ?? "") } } - - return { status: "success", message: "Processed tag" } + } + } + + return ctx + } + + /** + * Final processing method called after all extraction is complete. + * + * @param {Array} ctx - Array of extracted block data + * @returns {Promise} The transformation results. + * @private + */ + async #finally(ctx) { + const functions = await Collection.asyncMap(ctx, async func => { + const result = { + name: func.function.groups.name, + description: func.description, + signature: { + ...func.signature, + type: func.tag?.return?.type?.join(", ") ?? "null", + }, } - // Process multiline content - if (this.currentTag) { - if (this.section?.name) { - const currentTag = this.currentTag - const { tag, name } = this.section - - const index = name - ? func[tag].findIndex(item => item.name === name) - : null - const tagMatch = this.regex.commentContent.exec(lineTrimmed) - - if (tagMatch && tagMatch.groups?.content) { - if (index > -1) - func[currentTag][index].content.push(tagMatch.groups.content) - else - func[currentTag].content.push(tagMatch.groups.content) - } else { - if (index) - func[currentTag][index].content.push("") - else - func[currentTag].content.push("") - } - } else { - const { tag } = this.section - const commentMatch = this.regex.commentContent.exec(lineTrimmed) - if (commentMatch && commentMatch.groups?.content) { - if (func[tag].content) - func[tag].content.push(commentMatch.groups.content) - else - func[tag].push(commentMatch.groups.content) - } else { - if (func[tag].content) - func[tag].content.push("") - else - func[tag].push("") - } - } + const tags = func.tag ?? {} - return { status: "success", message: "Processed tag continuation" } - } + if(tags.param) + result.param = tags.param + .map(({type, name, content}) => ({type, name, content})) - // If not a special tag, treat as description - const descMatch = this.regex.commentContent.exec(lineTrimmed) - if (descMatch && descMatch.groups?.content) { - func.description = func.description || [] - func.description.push(descMatch.groups.content) + if(tags.return) + result.return = {type: tags.return.type, content: tags.return.content} - return { status: "success", message: "Processed description" } - } else { - func.description = func.description || [] - func.description.push("") + if(tags.example) + result.example = tags.example - return { status: "success", message: "Processed description" } - } - }, - - /** - * Determines if a line is a function definition. - * @param {string} line - The line to check. - * @returns {boolean} Whether the line is a function definition. - */ - isFunctionLine(line) { - return this.processing && - !this.processingComment && - this.regex.functionPattern.test(line) - }, - - /** - * Determine the function name from a line. - * @param {string} line - The line to determine the function name from. - * @param {object} func - The function so far - * @returns {object} The result of the function name determination. - */ - extractFunctionSignature(line, func) { - const match = this.regex.functionPattern.exec(line) - - if (!match?.groups?.name) - return { - status: "error", - error: new Error(`Failed to extract function from line: ${line}`), - } + return result + }) - const groups = match.groups - const signature = { - name: groups.name, - scope: groups.scope, - delimiter: groups.delimiter, - method: groups.method, - modifiers: [], - access: "", - type: func.return?.type.map(r => r.trim()).join(", ") ?? "null", - parameters: groups.parms?.split(",").map(p => p.trim()) ?? [] - } + return {functions} + } - return { status: "success", signature } - }, - }, -] - -export const contracts = [ - ` -provides: - type: object - properties: - functions: - type: array - items: - type: object - properties: - name: - type: string - description: - type: array - items: - type: string - param: - type: array - items: - type: object - properties: - type: - type: string - name: - type: string - content: - type: array - items: - type: string - return: - type: object - properties: - type: - type: string - content: - type: array - items: - type: string - example: - type: array - items: - type: string - -`, -] + // HERE BE DRAGONS! YOU DONE BEEN WARNED, FUGGAH! + #regexes = new Map([ + ["comment-start", /^\s*---\s?.*$/], + ["comment-content", /^\s*---\s?(?.*)$/], + ["blank", /^\s*$/], + ["tag-id", /^\s*---@[a-zA-Z]/], + ["tag", /^\s*---@(?name|param|return|returns|example)\s?(?.*)$/], + ["return-content", /^\s*(?.+?)\s+#\s+(?.*)?$/], + ["tag-content", /^(?.*) (?.*) - (?.+)$/], + ["function", /^\s*function\s+(?(?[a-zA-Z_]\w*(?=[.:]))?(?[.:])?(?[a-zA-Z_]\w*))\s*\((?.+)?\)\s*(?:end)?$/], + ]) +} diff --git a/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.yaml b/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.yaml new file mode 100644 index 0000000..fedba29 --- /dev/null +++ b/examples/node_modules_test/bedoc-lua-parser/bedoc-lua-parser.yaml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://schema.gesslar.dev/bedoc/v1/bedoc-action.json + +$schema: https://schema.gesslar.dev/bedoc/v1/bedoc-action.json +provides: + type: object + required: + - functions + properties: + functions: + type: array + items: + type: object + required: + - name + - signature + properties: + name: + type: string + signature: + type: object + required: + - name + properties: + name: + type: string + scope: + type: string + delimiter: + type: string + method: + type: string + modifiers: + type: array + items: + type: string + access: + type: string + type: + type: string + parameters: + type: array + items: + type: string + description: + type: array + items: + type: string + param: + type: array + items: + type: object + required: + - type + - name + properties: + type: + type: string + name: + type: string + content: + type: array + items: + type: string + return: + type: object + required: + - type + properties: + type: + type: array + items: + type: string + content: + type: array + items: + type: string + example: + type: array + items: + type: string diff --git a/examples/node_modules_test/bedoc-lua-parser/npm-shrinkwrap.json b/examples/node_modules_test/bedoc-lua-parser/npm-shrinkwrap.json new file mode 100644 index 0000000..3221954 --- /dev/null +++ b/examples/node_modules_test/bedoc-lua-parser/npm-shrinkwrap.json @@ -0,0 +1,149 @@ +{ + "name": "bedoc-lua-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bedoc-lua-parser", + "version": "1.0.0", + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } + }, + "node_modules/@gesslar/actioneer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@gesslar/actioneer/-/actioneer-2.3.1.tgz", + "integrity": "sha512-KKWE1mrlLurwIVgmMKtO7T4cmCs0Maooog/Aw5hAo6aHCfbaGqTtjCIkIlEoZG+U3FzgbUx+TWhOCvESgHRDhA==", + "license": "Unlicense", + "dependencies": { + "@gesslar/toolkit": "^3.34.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/colours": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gesslar/colours/-/colours-0.8.0.tgz", + "integrity": "sha512-Sy+xwKAqoE+qVZ/0jvoVRsXEaNMJTc2pEUoyoRYewlAw5k4iLuIGR6cBcZ10W1UvgYTJKxh+462+Eg0QBAx44w==", + "license": "Unlicense", + "bin": { + "colours": "src/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/toolkit": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-3.37.0.tgz", + "integrity": "sha512-w+tHyvyMKhLpPZ2CLv6rsdwclSxe+Vm+dlJ853QoZwOVccgw9U6/i/CIsmv4l6UA0Y0MT0y1sRC88xgiGJb3jA==", + "hasInstallScript": true, + "license": "Unlicense", + "dependencies": { + "@gesslar/colours": "^0.8.0", + "ajv": "^8.18.0", + "json5": "^2.2.3", + "supports-color": "^10.2.2", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/examples/node_modules_test/bedoc-lua-parser/package.json b/examples/node_modules_test/bedoc-lua-parser/package.json index 8b35d48..79c8882 100644 --- a/examples/node_modules_test/bedoc-lua-parser/package.json +++ b/examples/node_modules_test/bedoc-lua-parser/package.json @@ -11,5 +11,9 @@ "actions": [ "bedoc-lua-parser.js" ] + }, + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" } } diff --git a/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.js b/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.js new file mode 100644 index 0000000..b0fbcb7 --- /dev/null +++ b/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.js @@ -0,0 +1,276 @@ +/** + * @file Markdown formatter - A formatter for converting structured + * documentation data into Markdown format. + * + * This formatter takes parsed documentation objects (functions with descriptions, + * parameters, return types, and examples) and formats them as readable + * Markdown output. + * + * The formatter uses a pipeline approach defined via ActionBuilder and + * integrates with the BeDoc documentation system. + * + * @author gesslar + * @version 1.0.0 + * @since 1.0.0 + */ + +import {ActionBuilder, ACTIVITY} from "@gesslar/actioneer" +import {Promised} from "@gesslar/toolkit" + +/** + * Markdown formatter Class - Formats parsed documentation into Markdown. + * + * This formatter is designed to work with the structured output from BeDoc + * parsers, converting function documentation into well-formatted Markdown + * files with headers, parameter lists, return types, and examples. + * + * @class + */ +export default class Markdownformatter { + /** + * Printer metadata defining its characteristics and contract. + * + * @readonly + * @type {object} + * @property {string} kind - The type of action. + * @property {string} format - The format of the file this formatter emits. + * @property {string} terms - The contract terms file name. + */ + static meta = Object.freeze({ + kind: "formatter", + format: "markdown", + extension: "md", + terms: "ref://./bedoc-markdown-formatter.yaml" + }) + + /** + * Configures the formatter using ActionBuilder's fluent API. + * + * This method sets up the formatting pipeline: + * - Prepare and sort functions + * - Format each function into Markdown sections (via SPLIT) + * - Finalize by joining all sections into the output + * + * @param {ActionBuilder} builder - The ActionBuilder instance to configure + * @returns {ActionBuilder} The configured builder instance + */ + setup = builder => builder + .do("Format functions", ACTIVITY.SPLIT, + ctx => ctx, // splitter. just bare, nothing to do here. + this.#rejoinFormatted, // rejoiner + new ActionBuilder() + .do("Format function", this.#formatFunction) + ) + .do("Finalize", this.#finalize) + + /** + * Formats a single function's documentation into Markdown. + * + * Processes each section of a function (name, description, parameters, + * return type, and examples) into formatted Markdown text. + * + * @param {object} ctx - A parsed function object + * @param {string} ctx.name - The function name + * @param {Array} [ctx.description] - Description lines + * @param {Array} [ctx.param] - Parameter definitions + * @param {object} [ctx.return] - Return type info + * @param {Array} [ctx.example] - Example lines + * @returns {string} Formatted Markdown for this function + * @private + */ + #formatFunction = ctx => { + // TODO: hook(SECTION_LOAD, func) + const sections = [] + + // 1. Print the function name + if(ctx.name) + sections.push(`## ${ctx.name}`) + + if(ctx.signature) { + const signature = [ + ctx.signature.access ?? "", + ctx.signature.modifier1 ?? "", + ctx.signature.modifier2 ?? "", + ctx.signature.type ?? "", + ctx.signature.name ?? "", + ctx.signature.parms + ? `(${ctx.signature.parms})` + : "()" + ].filter(Boolean) + + if(signature.length) + sections.push(`\`${signature.join(" ")}\``) + } + + // 2. Print the description + if(ctx.description?.length) { + // TODO: hook(ENTER, {sectionName: "description", ...}) + const formatted = ctx.description.map(line => line.trim()).join("\n").trim() + // TODO: hook(EXIT, {sectionName: "description", ...}) + sections.push(formatted) + } + + // 3. Print the parameters + if(ctx.param?.length) { + // TODO: hook(ENTER, {sectionName: "param", ...}) + const params = ctx.param.map(p => { + let paramName = p.name + let optional = false + let defaultValue = null + + // Determine if this is an optional parameter + const optionalMatch = paramName.match(/^\[(.*)\]$/) + if(optionalMatch) { + optional = true + paramName = optionalMatch[1] + } + + // Determine if there is a default value + const defaultMatch = paramName.match(/(.*)=(.*)/) + if(defaultMatch) { + paramName = defaultMatch[1] + defaultValue = defaultMatch[2] + } + + const optionalAndOrDefault = optional || defaultValue + ? (() => { + if(optional && defaultValue) + return ` (Optional. Default: ${defaultValue})` + else if(optional) + return " (Optional)" + else if(defaultValue) + return ` (Default: ${defaultValue})` + else + throw new Error("Uhm, we seem to have hit a bump.") + })() + : "" + + const content = [...(p.content ?? [])] + while(content.length && (!content.at(0) || !content.at(-1))) { + if(!content.at(0)) + content.shift() + + if(!content.at(-1)) + content.pop() + } + + return `* **${paramName}** *${p.type}${optionalAndOrDefault}*` + + `: ${content.map(c => c.trim()).join(" ")}` + }) + // TODO: hook(EXIT, {sectionName: "param", ...}) + + sections.push(params.join("\n")) + } + + // 4. Print the return type + if(ctx.return) { + // TODO: hook(ENTER, {sectionName: "return", ...}) + const r = ctx.return + const formatted = `### Returns\n\n**${r.type}** ` + + `${r.content?.map(c => c.trim()).join(" ") ?? ""}` + // TODO: hook(EXIT, {sectionName: "return", ...}) + + sections.push(formatted) + } + + // 5. Print the examples + if(ctx.example?.length) { + // TODO: hook(ENTER, {sectionName: "example", ...}) + const formatted = "### Example\n\n" + ctx.example.join("\n") + // TODO: hook(EXIT, {sectionName: "example", ...}) + + sections.push(formatted) + } + + return Object.assign({}, {...ctx, formatted: sections}) + } + + #rejoinFormatted(_, settled) { + if(Promised.hasRejected(settled)) + Promised.throw(settled) + + const values = Promised.values(settled) + const formatted = values.map(e => e.formatted) + + formatted.push("") // blank line between functions + + return formatted + } + + /** + * Final processing method called after all formatting is complete. + * + * Joins the formatted function sections into a single Markdown document. + * + * @param {Array} ctx - Array of formatted Markdown strings + * @returns {string} The complete Markdown output + * @private + */ + #finalize = ctx => { + // TODO: hook(END, ctx) + return ctx.flat().join("\n\n") + } +} + +/** + * @todo Reintegrate the following legacy utilities as needed. + * + * --- Signature formatting --- + * + * Expects a `signature` object on each function with shape: + * { access, modifiers: string[], type, name, parameters: string[] } + * + * output = `${w.access} ` + + * `${w.modifiers.length ? w.modifiers.join(" ") + " " : ""}` + + * `*${w.type}* **${w.name}**` + + * `(${w.parameters.join(", ")})` + * + * --- Word wrap utility --- + * + * wrap(str, wrapAt = 80, indentAt = 0) { + * const sections = str.split("\n").map(section => { + * let parts = section.split(" ") + * let inCodeBlock = false + * let isStartOfLine = true + * + * if(section[0] === " ") + * parts = ["", ...parts] + * + * let running = 0 + * + * parts = parts.map(part => { + * if(isStartOfLine && /^```(?:\w+)?$/.test(part)) { + * inCodeBlock = !inCodeBlock + * running += part.length + 1 + * isStartOfLine = false + * return part + * } + * + * if(part[0] === "\n") { + * running = 0 + * isStartOfLine = true + * return part + * } + * + * running += part.length + 1 + * isStartOfLine = false + * + * if(!inCodeBlock && running >= wrapAt) { + * running = part.length + indentAt + * isStartOfLine = true + * return "\n" + " ".repeat(indentAt) + part + * } + * + * return part + * }) + * + * return parts + * .join(" ") + * .split("\n") + * .map(line => line.trimEnd()) + * .join("\n") + * }) + * + * return sections.join("\n") + * } + */ diff --git a/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.yaml b/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.yaml new file mode 100644 index 0000000..7bd597c --- /dev/null +++ b/examples/node_modules_test/bedoc-markdown-formatter/bedoc-markdown-formatter.yaml @@ -0,0 +1,94 @@ +# yaml-language-server: $schema=https://schema.gesslar.dev/bedoc/v1/bedoc-action.json + +$schema: https://schema.gesslar.dev/bedoc/v1/bedoc-action.json +accepts: + type: object + required: + - functions + properties: + functions: + type: array + items: + type: object + required: + - name + - signature + properties: + name: + type: string + signature: + type: object + required: + - name + properties: + access: + type: string + enum: [protected, public, private] + modifier1: + type: string + enum: [nomask, varargs] + modifier2: + type: string + enum: [nomask, varargs] + type: + type: string + enum: + [ + array, + buffer, + float, + function, + int, + mapping, + mixed, + object, + string, + void, + ] + name: + type: string + parms: + type: string + description: + type: array + items: + type: string + param: + type: array + items: + type: object + required: + - type + - name + properties: + type: + oneOf: + - type: string + - type: array + items: + type: string + name: + type: string + content: + type: array + items: + type: string + return: + type: object + required: + - type + properties: + type: + oneOf: + - type: string + - type: array + items: + type: string + content: + type: array + items: + type: string + example: + type: array + items: + type: string diff --git a/examples/node_modules_test/bedoc-markdown-formatter/npm-shrinkwrap.json b/examples/node_modules_test/bedoc-markdown-formatter/npm-shrinkwrap.json new file mode 100644 index 0000000..313945d --- /dev/null +++ b/examples/node_modules_test/bedoc-markdown-formatter/npm-shrinkwrap.json @@ -0,0 +1,149 @@ +{ + "name": "bedoc-markdown-formatter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bedoc-markdown-formatter", + "version": "1.0.0", + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } + }, + "node_modules/@gesslar/actioneer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@gesslar/actioneer/-/actioneer-2.3.1.tgz", + "integrity": "sha512-KKWE1mrlLurwIVgmMKtO7T4cmCs0Maooog/Aw5hAo6aHCfbaGqTtjCIkIlEoZG+U3FzgbUx+TWhOCvESgHRDhA==", + "license": "Unlicense", + "dependencies": { + "@gesslar/toolkit": "^3.34.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/colours": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gesslar/colours/-/colours-0.8.0.tgz", + "integrity": "sha512-Sy+xwKAqoE+qVZ/0jvoVRsXEaNMJTc2pEUoyoRYewlAw5k4iLuIGR6cBcZ10W1UvgYTJKxh+462+Eg0QBAx44w==", + "license": "Unlicense", + "bin": { + "colours": "src/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/toolkit": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-3.37.0.tgz", + "integrity": "sha512-w+tHyvyMKhLpPZ2CLv6rsdwclSxe+Vm+dlJ853QoZwOVccgw9U6/i/CIsmv4l6UA0Y0MT0y1sRC88xgiGJb3jA==", + "hasInstallScript": true, + "license": "Unlicense", + "dependencies": { + "@gesslar/colours": "^0.8.0", + "ajv": "^8.18.0", + "json5": "^2.2.3", + "supports-color": "^10.2.2", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/examples/node_modules_test/bedoc-markdown-formatter/package.json b/examples/node_modules_test/bedoc-markdown-formatter/package.json new file mode 100644 index 0000000..c9e50c3 --- /dev/null +++ b/examples/node_modules_test/bedoc-markdown-formatter/package.json @@ -0,0 +1,19 @@ +{ + "name": "bedoc-markdown-formatter", + "version": "1.0.0", + "type": "module", + "main": "bedoc-markdown-formatter.js", + "description": "Markdown formatter for BeDoc", + "exports": { + ".": "./bedoc-markdown-formatter.js" + }, + "bedoc": { + "actions": [ + "bedoc-markdown-formatter.js" + ] + }, + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } +} diff --git a/examples/node_modules_test/bedoc-markdown-printer/bedoc-markdown-printer.js b/examples/node_modules_test/bedoc-markdown-printer/bedoc-markdown-printer.js deleted file mode 100644 index 54287c2..0000000 --- a/examples/node_modules_test/bedoc-markdown-printer/bedoc-markdown-printer.js +++ /dev/null @@ -1,315 +0,0 @@ -export const actions = [ - { - meta: Object.freeze({ - action: "print", - format: "markdown", - }), - - setup({ parent, log }) { - this.parent = parent - this.log = log - this.documentExtension = ".md" - }, - - /** - * This is the action to print structured object to text. - * @param {object} module Data coming in from the printer - * @param {object} module.file The file object representing the file - * being currently being processed - * @param {object[]} module.moduleContent An array of objects containing - * function definitions prepared by the parser. - * @returns {Promise} The result of the print operations. - */ - async run(module) { - const hook = this.hook ?? (async () => null) - const debug = this.log.newDebug() - const { START, SECTION_LOAD, ENTER, EXIT, END } = this.HOOKS ?? {} - - const { file: { module: moduleName }, moduleContent } = - await hook(START, module) ?? module - - debug("Printing module", 3, moduleName) - - const sorted = - moduleContent?.sort(function (a, b) { - return a.name?.localeCompare(b.name) - }) ?? module.moduleContent - - if (sorted === undefined || sorted.length === 0) - return { - status: "warning", - warning: `No functions to print for module: \`${moduleName}\`` - } - - const moduleOutput = [] - - /** - * Generic section printer - * @param {string} sectionName - The section name - * @param {object} sectionContent - The function section to process - * @param {(sectionContent: unknown) => string} formatContent - Callback to format the content - * @returns {Promise} The formatted content - */ - async function printIt(sectionName, sectionContent, formatContent) { - // If we don't even have anything, nevermind? lulz - if (!sectionContent) - return null - - // ENTER - should return the exactly same shaped object as was passed - // to it. - const enter = await hook(ENTER, { moduleName, sectionName, sectionContent }) - - // Whew, that was a lot of work far! We should now get a string result. - const formatted = formatContent(enter?.sectionContent || sectionContent) - - // EXIT - should take the string so far, and return even more string. - // Well, not _MORE_ string, but... shut up. - const exit = await hook( - EXIT, { - moduleName, - sectionName, - sectionContent: formatted - } - ) ?? formatted - - return exit - } - - for (const section of sorted) { - const work = await hook(SECTION_LOAD, { moduleName, section }) - ?? section - let output, sectionName - const sectionOutput = new Map() - - // 1. Print the section name - sectionName = "name" - output = await printIt(sectionName, section.signature.name, w => - `## ${w}` - ) - output && sectionOutput.set(sectionName, output) - - // 2. Print the signature - sectionName = "signature" - output = await printIt(sectionName, work[sectionName], w => { - return `${w.access} ` + - `${w.modifiers.length ? w.modifiers.join(" ") + " " : ""}` + - `*${w.type}* **${w.name}**` + - `(${w.parameters.join(", ")})` - }) - output && sectionOutput.set(sectionName, output) - - // 2. Print the section description - sectionName = "description" - output = await printIt(sectionName, work[sectionName], w => - w.length ? w.map(line => line.trim()).join("\n") : "" - ) - output && sectionOutput.set(sectionName, output) - - // 3. Print the section parameters - sectionName = "param" - output = await printIt(sectionName, work[sectionName], w => { - const params = w.map(p => { - - // capture detailed name info - let optionalParam, paramName, defaultValue - - // Determine if this is an optional parameter - const optionalMatch = p.name.match(/^\[(.*)\]$/) - if (optionalMatch) { - optionalParam = true - paramName = optionalMatch[1] - } else { - paramName = p.name - } - - // Determine if there is a default value - const defaultMatch = paramName.match(/(.*)=(.*)/) - defaultValue = defaultMatch ? defaultMatch[2] : null - paramName = defaultMatch ? defaultMatch[1] : paramName - - let optionalAndOrDefault = optionalParam || defaultValue - ? (() => { - if (optionalParam && defaultValue) - return ` (Optional. Default: ${defaultValue})` - else if (optionalParam) - return " (Optional)" - else if (defaultValue) - return ` (Default: ${defaultValue})` - else - throw new Error("Uhm, we seem to have hit a bump.") - })() - : "" - - const content = p.content - while (content.length && (!content.at(0) || !content.at(-1))) { - if (!content.at(0)) - content.shift() - - if (!content.at(-1)) - content.pop() - } - - return `**${paramName}** *${p.type}${optionalAndOrDefault}*\n\n` + - `: ${content.map(c => c.trim()).join(" ")}` - }) ?? [] - return params.join("\n") - }) - output && sectionOutput.set(sectionName, output) - - // 4. Print the section return - output = await printIt(sectionName, work[sectionName], w => w - ? `### Returns\n\n**${w.type}** ` + - `${w.content?.map(c => c.trim()).join(" ") ?? ""}` - : "" - ) - output && sectionOutput.set(sectionName, output) - - // 5. Print the section example - sectionName = "example" - output = await printIt(sectionName, work[sectionName], w => w.length - ? "### Example\n\n" + w.join("\n") - : "" - ) - output && sectionOutput.set(sectionName, output) - moduleOutput.push(Array.from(sectionOutput.values()).join("\n\n")) - } - - debug(`Printing complete for module \`${moduleName}\``, 3) - - const joinedOutput = moduleOutput.join("\n") - const finalOutput = await hook( - END, { - moduleName, - moduleContent: joinedOutput - } - ) ?? joinedOutput - - return { - status: "success", - message: "File printed successfully", - destFile: `${moduleName}${this.documentExtension}`, - destContent: finalOutput, - } - }, - - /** - * Wraps text to a specified width with optional indentation - * @param {string} str - The text to wrap - * @param {number} [wrapAt] - The column at which to wrap the text - * @param {number} [indentAt] - The number of spaces to indent wrapped lines - * @returns {string} The wrapped text - */ - wrap(str, wrapAt = 80, indentAt = 0) { - const sections = str.split("\n").map(section => { - let parts = section.split(" ") - let inCodeBlock = false - let isStartOfLine = true // Start of each section is start of line - - // Preserve leading space if it existed - if (section[0] === " ") - parts = ["", ...parts] - - let running = 0 - - parts = parts.map(part => { - // Only check for code block if we're at start of line - if (isStartOfLine && /^```(?:\w+)?$/.test(part)) { - inCodeBlock = !inCodeBlock - running += part.length + 1 - isStartOfLine = false - return part - } - - if (part[0] === "\n") { - running = 0 - isStartOfLine = true // Next part will be at start of line - return part - } - - running += part.length + 1 - isStartOfLine = false // No longer at start of line - - if (!inCodeBlock && running >= wrapAt) { - running = part.length + indentAt - isStartOfLine = true // After newline, next part will be at start - return "\n" + " ".repeat(indentAt) + part - } - - return part - }) - - return parts - .join(" ") - .split("\n") - .map(line => line.trimEnd()) - .join("\n") - }) - - return sections.join("\n") - }, - }, -] - -export const contracts = [ - ` ---- -accepts: - type: object - required: - - functions - properties: - functions: - type: array - items: - type: object - required: - - name - - return - properties: - name: - type: string - description: - type: array - items: - type: string - param: - type: array - items: - type: object - required: - - type - - name - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - name: - type: string - content: - type: array - items: - type: string - return: - type: object - required: - - type - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - content: - type: array - items: - type: string - example: - type: array - items: - type: string - `, -] diff --git a/examples/node_modules_test/bedoc-markdown-printer/package.json b/examples/node_modules_test/bedoc-markdown-printer/package.json deleted file mode 100644 index c733f37..0000000 --- a/examples/node_modules_test/bedoc-markdown-printer/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "bedoc-markdown-printer", - "version": "1.0.0", - "type": "module", - "main": "bedoc-markdown-printer.js", - "description": "Markdown printer for BeDoc", - "exports": { - ".": "./bedoc-markdown-printer.js" - }, - "bedoc": { - "actions": [ - "bedoc-markdown-printer.js" - ] - } -} diff --git a/examples/node_modules_test/bedoc-wikitext-printer/admonition.txt b/examples/node_modules_test/bedoc-wikitext-formatter/admonition.txt similarity index 100% rename from examples/node_modules_test/bedoc-wikitext-printer/admonition.txt rename to examples/node_modules_test/bedoc-wikitext-formatter/admonition.txt diff --git a/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.js b/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.js new file mode 100644 index 0000000..32d3c54 --- /dev/null +++ b/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.js @@ -0,0 +1,211 @@ +/** + * @file Wikitext Printer - A formatter for converting structured documentation + * data into Wikitext format. + * + * This formatter takes parsed documentation objects (functions with descriptions, + * parameters, return types, and examples) and formats them as readable + * Wikitext output. + * + * The formatter uses a pipeline approach defined via ActionBuilder and + * integrates with the BeDoc documentation system. + * + * @author gesslar + * @version 1.0.0 + * @since 1.0.0 + */ + +import {ActionBuilder, ACTIVITY} from "@gesslar/actioneer" +import {Promised} from "@gesslar/toolkit" + +/** + * Wikitext Printer Class - Formats parsed documentation into Wikitext. + * + * This formatter is designed to work with the structured output from BeDoc + * parsers, converting function documentation into well-formatted Wikitext + * files with headers, parameter lists, return types, and examples. + * + * @class + */ +export default class WikitextPrinter { + /** + * Printer metadata defining its characteristics and contract. + * + * @readonly + * @type {object} + * @property {string} kind - The type of action. + * @property {string} format - The format of the file this formatter emits. + * @property {string} extension - The file extension for output files. + * @property {string} terms - The contract terms file name. + */ + static meta = Object.freeze({ + kind: "formatter", + format: "wikitext", + extension: "txt", + terms: "ref://./bedoc-wikitext-formatter.yaml" + }) + + /** + * Configures the formatter using ActionBuilder's fluent API. + * + * This method sets up the formatting pipeline: + * - Prepare and sort functions + * - Format each function into Wikitext sections (via SPLIT) + * - Finalize by joining all sections into the output + * + * @param {ActionBuilder} builder - The ActionBuilder instance to configure + * @returns {ActionBuilder} The configured builder instance + */ + setup = builder => builder + .do("Format functions", ACTIVITY.SPLIT, + ctx => ctx, // splitter. just bare, nothing to do here. + this.#rejoinFormatted, // rejoiner + new ActionBuilder() + .do("Format function", this.#formatFunction) + ) + .do("Finalize", this.#finalize) + + /** + * Formats a single function's documentation into Wikitext. + * + * Processes each section of a function (name, deprecated, signature, + * description, parameters, return type, and examples) into formatted + * Wikitext. + * + * @param {object} ctx - A parsed function object + * @param {string} ctx.name - The function name + * @param {Array} [ctx.deprecated] - Deprecation notice lines + * @param {object} [ctx.signature] - Signature definition + * @param {Array} [ctx.description] - Description lines + * @param {Array} [ctx.param] - Parameter definitions + * @param {object} [ctx.return] - Return type info + * @param {Array} [ctx.example] - Example lines + * @returns {object} The ctx with a `formatted` array of Wikitext sections + * @private + */ + #formatFunction = ctx => { + const sections = [] + + // 1. Print the function name + if(ctx.name) + sections.push(`== ${ctx.name} ==`) + + // 2. Handle any deprecation notices + if(ctx.deprecated?.length) + sections.push(`{{Admonition|type=stop|title=Deprecated|${ctx.deprecated.map(c => c.trim()).join(" ")}}}`) + + // 3. Print the signature + if(ctx.signature) { + const sig = ctx.signature + const signature = [ + sig.access ?? "", + sig.modifier1 ?? "", + sig.modifier2 ?? "", + sig.type ? `''${sig.type}''` : "", + sig.name ? `'''${sig.name}'''` : "", + sig.parms ? `(${sig.parms})` : "()" + ].filter(Boolean) + + if(signature.length) + sections.push(`${signature.join(" ")}`) + } + + // 4. Print the description + if(ctx.description?.length) { + const formatted = ctx.description.map(line => line.trim()).join("\n").trim() + sections.push(formatted) + } + + // 5. Print the parameters + if(ctx.param?.length) { + const params = ctx.param.map(p => { + let paramName = p.name + let optional = false + let defaultValue = null + + // Determine if this is an optional parameter + const optionalMatch = paramName.match(/^\[(.*)\]$/) + if(optionalMatch) { + optional = true + paramName = optionalMatch[1] + } + + // Determine if there is a default value + const defaultMatch = paramName.match(/(.*)=(.*)/) + if(defaultMatch) { + paramName = defaultMatch[1] + defaultValue = defaultMatch[2] + } + + const optionalAndOrDefault = optional || defaultValue + ? (() => { + if(optional && defaultValue) + return ` (Optional. Default: ${defaultValue})` + else if(optional) + return " (Optional)" + else if(defaultValue) + return ` (Default: ${defaultValue})` + else + throw new Error("Uhm, we seem to have hit a bump.") + })() + : "" + + const content = [...(p.content ?? [])] + while(content.length && (!content.at(0) || !content.at(-1))) { + if(!content.at(0)) + content.shift() + + if(!content.at(-1)) + content.pop() + } + + return `;'''${paramName}''' ''${p.type}${optionalAndOrDefault}''\n` + + `:${content.map(c => c.trim()).join(" ")}` + }) + + sections.push(params.join("\n")) + } + + // 6. Print the return type + if(ctx.return) { + const r = ctx.return + sections.push(`=== Returns ===\n\n''${r.type}'' ` + + `${r.content?.map(c => c.trim()).join(" ") ?? ""}`) + } + + // 7. Print the examples + if(ctx.example?.length) + sections.push("=== Example ===\n\n" + ctx.example.join("\n")) + + return Object.assign({}, {...ctx, formatted: sections}) + } + + #rejoinFormatted(_, settled) { + debugger + + if(Promised.hasRejected(settled)) + Promised.throw(settled) + + const values = Promised.values(settled) + const formatted = values.map(e => e.formatted) + + formatted.push("") // blank line between functions + + return formatted + } + + /** + * Final processing method called after all formatting is complete. + * + * Joins the formatted function sections into a single Wikitext document. + * + * @param {Array} ctx - Array of formatted Wikitext strings + * @returns {string} The complete Wikitext output + * @private + */ + #finalize = ctx => { + debugger + console.log("hi") + + return ctx.flat().join("\n\n") + } +} diff --git a/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.yaml b/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.yaml new file mode 100644 index 0000000..a61704d --- /dev/null +++ b/examples/node_modules_test/bedoc-wikitext-formatter/bedoc-wikitext-formatter.yaml @@ -0,0 +1,98 @@ +# yaml-language-server: $schema=https://schema.gesslar.dev/bedoc/v1/bedoc-action.json + +$schema: https://schema.gesslar.dev/bedoc/v1/bedoc-action.json +accepts: + type: object + required: + - functions + properties: + functions: + type: array + items: + type: object + required: + - name + - signature + properties: + name: + type: string + signature: + type: object + required: + - name + properties: + access: + type: string + enum: [protected, public, private] + modifier1: + type: string + enum: [nomask, varargs] + modifier2: + type: string + enum: [nomask, varargs] + type: + type: string + enum: + [ + array, + buffer, + float, + function, + int, + mapping, + mixed, + object, + string, + void, + ] + name: + type: string + parms: + type: string + deprecated: + type: array + items: + type: string + description: + type: array + items: + type: string + param: + type: array + items: + type: object + required: + - type + - name + properties: + type: + oneOf: + - type: string + - type: array + items: + type: string + name: + type: string + content: + type: array + items: + type: string + return: + type: object + required: + - type + properties: + type: + oneOf: + - type: string + - type: array + items: + type: string + content: + type: array + items: + type: string + example: + type: array + items: + type: string diff --git a/examples/node_modules_test/bedoc-wikitext-formatter/npm-shrinkwrap.json b/examples/node_modules_test/bedoc-wikitext-formatter/npm-shrinkwrap.json new file mode 100644 index 0000000..3ecb408 --- /dev/null +++ b/examples/node_modules_test/bedoc-wikitext-formatter/npm-shrinkwrap.json @@ -0,0 +1,149 @@ +{ + "name": "bedoc-wikitext-formatter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bedoc-wikitext-formatter", + "version": "1.0.0", + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } + }, + "node_modules/@gesslar/actioneer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@gesslar/actioneer/-/actioneer-2.3.1.tgz", + "integrity": "sha512-KKWE1mrlLurwIVgmMKtO7T4cmCs0Maooog/Aw5hAo6aHCfbaGqTtjCIkIlEoZG+U3FzgbUx+TWhOCvESgHRDhA==", + "license": "Unlicense", + "dependencies": { + "@gesslar/toolkit": "^3.34.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/colours": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gesslar/colours/-/colours-0.8.0.tgz", + "integrity": "sha512-Sy+xwKAqoE+qVZ/0jvoVRsXEaNMJTc2pEUoyoRYewlAw5k4iLuIGR6cBcZ10W1UvgYTJKxh+462+Eg0QBAx44w==", + "license": "Unlicense", + "bin": { + "colours": "src/cli.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@gesslar/toolkit": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/@gesslar/toolkit/-/toolkit-3.37.0.tgz", + "integrity": "sha512-w+tHyvyMKhLpPZ2CLv6rsdwclSxe+Vm+dlJ853QoZwOVccgw9U6/i/CIsmv4l6UA0Y0MT0y1sRC88xgiGJb3jA==", + "hasInstallScript": true, + "license": "Unlicense", + "dependencies": { + "@gesslar/colours": "^0.8.0", + "ajv": "^8.18.0", + "json5": "^2.2.3", + "supports-color": "^10.2.2", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=24.13.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/examples/node_modules_test/bedoc-wikitext-formatter/package.json b/examples/node_modules_test/bedoc-wikitext-formatter/package.json new file mode 100644 index 0000000..a8d6c5d --- /dev/null +++ b/examples/node_modules_test/bedoc-wikitext-formatter/package.json @@ -0,0 +1,19 @@ +{ + "name": "bedoc-wikitext-formatter", + "version": "1.0.0", + "type": "module", + "main": "bedoc-wikitext-formatter.js", + "description": "Wikitext formatter for BeDoc", + "exports": { + ".": "./bedoc-wikitext-formatter.js" + }, + "bedoc": { + "actions": [ + "bedoc-wikitext-formatter.js" + ] + }, + "dependencies": { + "@gesslar/actioneer": "^2.3.1", + "@gesslar/toolkit": "^3.37.0" + } +} diff --git a/examples/node_modules_test/bedoc-wikitext-printer/bedoc-wikitext-printer.js b/examples/node_modules_test/bedoc-wikitext-printer/bedoc-wikitext-printer.js deleted file mode 100644 index 6f28f19..0000000 --- a/examples/node_modules_test/bedoc-wikitext-printer/bedoc-wikitext-printer.js +++ /dev/null @@ -1,270 +0,0 @@ -export const actions = [ - { - meta: Object.freeze({ - action: "print", - format: "wikitext", - }), - - setup({ parent, log }) { - this.parent = parent - this.log = log - this.documentExtension = ".txt" - }, - - /** - * This is the action to print structured object to text. - * @param {object} module Data coming in from the printer - * @param {string} module.moduleName The module name (base name) of the file - * @param {object[]} module.moduleContent An array of objects containing function definitions. - * @returns {Promise} The result of the print operations. - */ - async run(module) { - const hook = this.hook ?? (async () => null) - const debug = this.log.newDebug() - const { START, SECTION_LOAD, ENTER, EXIT, END } = this.HOOKS ?? {} - - const { file: { module: moduleName }, moduleContent } = - await hook(START, module) ?? module - - debug("Printing module", 3, moduleName) - - const sorted = - moduleContent?.sort(function (a, b) { - return a.name?.localeCompare(b.name) - }) ?? module.moduleContent - - if (sorted === undefined || sorted.length === 0) - return { - status: "warning", - warning: `No functions to print for module: \`${moduleName}\`` - } - - const moduleOutput = [] - - /** - * Generic section printer - * @param {string} sectionName - The section name - * @param {object} sectionContent - The function section to process - * @param {(sectionContent: unknown) => string} formatContent - Callback to format the content - * @returns {Promise} The formatted content - */ - async function printIt(sectionName, sectionContent, formatContent) { - // If we don't even have anything, nevermind? lulz - if (!sectionContent) - return null - - // ENTER - should return the exactly same shaped object as was passed - // to it. - const enter = await hook(ENTER, { moduleName, sectionName, sectionContent }) - - // Whew, that was a lot of work far! We should now get a string result. - const formatted = formatContent(enter?.sectionContent || sectionContent) - - // EXIT - should take the string so far, and return even more string. - // Well, not _MORE_ string, but... shut up. - const exit = await hook( - EXIT, { - moduleName, - sectionName, - sectionContent: formatted - } - ) ?? formatted - - return exit - } - - for (const section of sorted) { - const work = await hook(SECTION_LOAD, { moduleName, section }) - ?? section - - let output, sectionName - const sectionOutput = new Map() - - // 1. Print the section name - this one is special since it - // is actually part of the signature section - sectionName = "name" - output = await printIt(sectionName, section.signature.name, w => { - return `== ${w} ==` - }) - output && sectionOutput.set(sectionName, output) - - // 2. Handle any deprecation notices - sectionName = "deprecated" - output = await printIt(sectionName, work[sectionName], w => { - return `{{Admonition|type=stop|title=Deprecated|${w.map(c => c.trim()).join(" ")}}}` - }) - output && sectionOutput.set(sectionName, output) - - // 2. Print the signature - sectionName = "signature" - output = await printIt(sectionName, work[sectionName], w => { - return `${w.access} ` + - `${w.modifiers.length ? w.modifiers.join(" ") + " " : ""}` + - `''${w.type}'' '''${w.name}'''` + - `(${w.parameters.join(", ")})` + - `` - }) - output && sectionOutput.set(sectionName, output) - - // 3. Print the section description - sectionName = "description" - output = await printIt(sectionName, work[sectionName], w => { - return w.length ? w.map(line => line.trim()).join("\n") : "" - }) - output && sectionOutput.set(sectionName, output) - - // 3. Print the section parameters - sectionName = "param" - output = await printIt(sectionName, work[sectionName], w => { - const params = w.map(p => { - - // capture detailed name info - let optionalParam, paramName, defaultValue - - // Determine if this is an optional parameter - const optionalMatch = p.name.match(/^\[(.*)\]$/) - if (optionalMatch) { - optionalParam = true - paramName = optionalMatch[1] - } else { - paramName = p.name - } - - // Determine if there is a default value - const defaultMatch = paramName.match(/(.*)=(.*)/) - defaultValue = defaultMatch ? defaultMatch[2] : null - paramName = defaultMatch ? defaultMatch[1] : paramName - - let optionalAndOrDefault = optionalParam || defaultValue - ? (() => { - if (optionalParam && defaultValue) - return ` (Optional. Default: ${defaultValue})` - else if (optionalParam) - return " (Optional)" - else if (defaultValue) - return ` (Default: ${defaultValue})` - else - throw new Error("Uhm, we seem to have hit a bump.") - })() - : "" - - const content = p.content - while (content.length && (!content.at(0) || !content.at(-1))) { - if (!content.at(0)) - content.shift() - - if (!content.at(-1)) - content.pop() - } - - return `;'''${paramName}''' ''${p.type}${optionalAndOrDefault}''\n` + - `:${content.map(c => c.trim()).join(" ")}` - }) ?? [] - return params.join("\n") - }) - output && sectionOutput.set(sectionName, output) - - // 4. Print the section return - sectionName = "return" - output = await printIt(sectionName, work[sectionName], w => { - const ret = w - - return ret - ? `=== Returns ===\n\n''${ret.type}'' ` + - `${ret.content?.map(c => c.trim()).join(" ") ?? ""}` - : "" - }) - output && sectionOutput.set(sectionName, output) - - // 5. Print the section example - sectionName = "example" - output = await printIt(sectionName, work[sectionName], w => w.length - ? "=== Example ===\n\n" + w.join("\n") - : "" - ) - output && sectionOutput.set(sectionName, output) - moduleOutput.push(Array.from(sectionOutput.values()).join("\n\n")) - } - - debug(`Printing complete for module \`${moduleName}\``, 3) - - const joinedOutput = moduleOutput.join("\n") - const finalOutput = await hook( - END, { - moduleName, - moduleContent: joinedOutput - } - ) ?? joinedOutput - - return { - status: "success", - message: "File printed successfully", - destFile: `${moduleName}${this.documentExtension}`, - destContent: finalOutput, - } - }, - }, -] - -export const contracts = [ - ` -accepts: - type: object - required: - - functions - properties: - functions: - type: array - items: - type: object - required: - - name - - return - properties: - name: - type: string - description: - type: array - items: - type: string - param: - type: array - items: - type: object - required: - - type - - name - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - name: - type: string - content: - type: array - items: - type: string - return: - type: object - required: - - type - properties: - type: - oneOf: - - type: string - - type: array - items: - type: string - content: - type: array - items: - type: string - example: - type: array - items: - type: string -`, -] diff --git a/examples/node_modules_test/bedoc-wikitext-printer/package.json b/examples/node_modules_test/bedoc-wikitext-printer/package.json deleted file mode 100644 index 328a2fa..0000000 --- a/examples/node_modules_test/bedoc-wikitext-printer/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "bedoc-wikitext-printer", - "version": "1.0.0", - "type": "module", - "main": "bedoc-wikitext-printer.js", - "description": "Wikitext printer for BeDoc", - "exports": { - ".": "./bedoc-wikitext-printer.js" - }, - "bedoc": { - "actions": [ - "bedoc-wikitext-printer.js" - ] - } -}