Skip to content

Commit

Permalink
feat: parse lua functions from html
Browse files Browse the repository at this point in the history
  • Loading branch information
Claudiohbsantos committed Jun 27, 2020
1 parent f44ba49 commit cbd2b83
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 79 deletions.
63 changes: 63 additions & 0 deletions src/functionSignatureParser.ts
Original file line number Diff line number Diff line change
@@ -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 = /(?:(?<returns>.+)\s)?(?:(?<namespace>[\w{}.]+)\.)?(?<name>\w+)\((?<params>[^)]+)?\)/
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 = /(?<mandatory>[^[]+)(?:\[(?<optional>.+)])?/
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[]
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
100 changes: 21 additions & 79 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -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(<string>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)
}
}
}

Expand All @@ -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(<string>n.textContent))
if (nameI < 0) return

const nameMatches = /reaper\.([^(]+)/.exec(<string>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()
}
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit cbd2b83

Please sign in to comment.