diff --git a/src/functionSignatureParser.ts b/src/functionSignatureParser.ts new file mode 100644 index 0000000..a7615dc --- /dev/null +++ b/src/functionSignatureParser.ts @@ -0,0 +1,63 @@ +import { Method, Variable } from './types' + +/** + * @description parses code signatures in the format returns[type name,] = namespace.foo([type name,]) + * @example integer retval, string val = reaper.GetProjExtState(ReaProject proj, string extname, string key) + */ +export function parseFunctionEntry(code: string): Method | undefined { + const exceptions = ['new_array'] + + const rgx = /(?:(?.+)\s)?(?:(?[\w{}.]+)\.)?(?\w+)\((?[^)]+)?\)/ + const matches = rgx.exec(code) + if (matches && matches.groups) { + if (exceptions.includes(matches.groups.name)) return + + const method: Method = { name: matches.groups.name } + + if (matches.groups.params) { + method.params = separateMandatoryFromOptional(matches.groups.params, 'params') + } + if (matches.groups.returns) method.returns = parseVariables(matches.groups.returns, 'returns') + if (matches.groups.namespace) method.namespace = matches.groups.namespace.trim() + + return method + } +} + +function separateMandatoryFromOptional(signature: string, type: 'params' | 'returns'): Variable[] { + const rgx = /(?[^[]+)(?:\[(?.+)])?/ + const matches = rgx.exec(signature) + + let variables: Variable[] = [] + + if (matches && matches.groups) { + if (matches.groups.mandatory) { + variables = variables.concat(parseVariables(matches.groups.mandatory, type)) + } + if (matches.groups.optional) { + const optionals = parseVariables(matches.groups.optional, type) + variables = variables.concat(optionals.map((v) => Object.assign(v, { optional: true }))) + } + } + return variables +} + +function parseVariables(signature: string, type: 'params' | 'returns'): Variable[] { + const chunks = signature.split(',') + const rgx = /(\w+)(?:\s+(\w+))?/ + const variables = chunks + .map((c) => { + const matches = rgx.exec(c) + if (matches && matches.length > 1) { + if (matches[2]) { + return { type: matches[1], name: matches[2] } + } else { + if (type === 'params') return { name: matches[1] } + if (type === 'returns') return { type: matches[1] } + } + } + }) + .filter((v) => !!v) + + return variables as Variable[] +} diff --git a/src/index.ts b/src/index.ts index 9d5b02d..acccef1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,7 @@ const apiPath = path.resolve(__dirname, '..', 'api', 'reascripthelp.html') const apiHTML = fs.readFileSync(apiPath, 'utf8') const apiJSON = parser(apiHTML) + +const outputDir = path.resolve(__dirname, '..', 'output') +if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir) +fs.writeFileSync(path.resolve(outputDir, 'reaperAPI.json'), JSON.stringify(apiJSON)) diff --git a/src/parser.ts b/src/parser.ts index eec83c0..1ea98ca 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,33 +1,30 @@ import jsdom = require('jsdom') -interface Variable { - type?: string - name?: string -} - -export interface Method { - name: string - params?: Variable[] - returns?: Variable[] - description?: string -} +import { Method } from './types' +import { parseFunctionEntry } from './functionSignatureParser' export function parser(apiHTML: string): Method[] { const { document } = new jsdom.JSDOM(apiHTML).window - const luaFunctions = document.getElementsByClassName('l_func') + const mainMethods = parseLuaFunctions(document.getElementsByClassName('l_func')) + const builtIn = parseLuaFunctions(document.getElementsByClassName('l_funcs')) + return mainMethods.concat(builtIn) +} +function parseLuaFunctions(luaFunctions: HTMLCollection): Method[] { const methods: Method[] = [] for (let i = 0; i < luaFunctions.length; i++) { - const fEntries = luaFunctions[i].getElementsByTagName('code') - if (fEntries.length != 1) continue - const entry = fEntries[0] - - const method = parseFunctionEntry(entry) - if (method) { - const description = lookForDescription(luaFunctions[i]) - if (description) method.description = description - methods.push(method) + const codes = luaFunctions[i].getElementsByTagName('code') + for (let j = 0; j < codes.length; j++) { + if (!codes[j] || !codes[j].textContent) continue + const m = parseFunctionEntry(codes[j].textContent) + if (m) { + // determines whether it's parsing built in functions or main api in order to look for description in right branch + const siblingOfDescription = codes.length > 1 ? codes[j] : luaFunctions[i] + const description = lookForDescription(siblingOfDescription) + if (description) m.description = description + methods.push(m) + } } } @@ -37,71 +34,16 @@ export function parser(apiHTML: string): Method[] { function lookForDescription(node: Element): string { let currNode: ChildNode | null = node.nextSibling + let description = '' while (currNode) { if (currNode.nodeName !== '#text') { if (currNode.nodeName == 'A') break } else { if (currNode.textContent && /\w+/.test(currNode.textContent)) { - return currNode.textContent.trim() + description = description + currNode.textContent.trim() + '\n' } } currNode = currNode.nextSibling } - return '' -} - -function parseFunctionEntry(node: HTMLElement): Method | undefined { - const nodes: Node[] = [] - - for (let i = 0; i < node.childNodes.length; i++) { - nodes.push(node.childNodes[i]) - } - - if ( - nodes.some( - (n) => !(n.nodeName === '#text' || n.nodeName === 'I') || typeof n.textContent !== 'string' - ) - ) { - return - } - - const nameI = nodes.findIndex((n) => /reaper\./.test(n.textContent)) - if (nameI < 0) return - - const nameMatches = /reaper\.([^(]+)/.exec(nodes[nameI].textContent) - if (!nameMatches || !nameMatches[1]) return - - const method: Method = { name: nameMatches[1] } - - const returns = parseVariables(nodes.slice(0, nameI + 1)) - const params = parseVariables(nodes.slice(nameI, nodes.length)) - - if (returns.length > 0) method.returns = returns - if (params.length > 0) method.params = params - - return method -} - -function parseVariables(nodes: Node[]): Variable[] { - const vars: Variable[] = [] - - nodes.forEach((n) => { - const text = n.textContent?.trim() - if (!text) return - - const textMatches = /\w+/.exec(text) - - if (textMatches && textMatches[0] && n.nodeName === 'I') vars.push({ type: textMatches[0] }) - if ( - textMatches && - textMatches[0] && - n.nodeName === '#text' && - vars[vars.length - 1] && - !/reaper\./.test(textMatches[0]) - ) { - vars[vars.length - 1].name = textMatches[0] - } - }) - - return vars + return description.trim() } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ceea4fb --- /dev/null +++ b/src/types.ts @@ -0,0 +1,13 @@ +export interface Variable { + type?: string + name?: string + optional?: boolean +} + +export interface Method { + name: string + params?: Variable[] + returns?: Variable[] + description?: string + namespace?: string +}