import * as vscode from 'vscode' import { VIEW_TPLS_MATCHER } from '../consts' import { fm } from './FileManage' import type { SubviewNameCompletion } from '../completion/SubviewNameCompletion' import { HtmlParseErr, Dict } from '../type' import { tc } from './TsComplier' interface PropsDict { [key: string]: { value: string; range: vscode.Range } } export class HtmlComplier { private svNameComplet: SubviewNameCompletion private methodsDict: { [key: string]: boolean } = {} private propsArr: string[] | null = null private propsCache: { [key: string]: Dict | null } = {} private eventCache: { [key: string]: Dict | null } = {} private methodsCache: { [key: string]: | { name: string desp: string }[] | null } = {} private timerCache = 0 private lock = false constructor(svNameComplet: SubviewNameCompletion) { this.svNameComplet = svNameComplet } /** * 解析一遍 HTML 文本 ,找到组件节点 并校验他的 props * @param document vscode.TextDocument */ public async doParse(document: vscode.TextDocument) { if (this.lock) return true const fileName = document.fileName // 校验文件路径,是否为viewTpls下的html文件 if (!VIEW_TPLS_MATCHER.test(fileName)) return false const now = Date.now() if (now - this.timerCache <= 1000) { return false } this.timerCache = now const lines = document.lineCount let i = 0 // 迭代每一行 并解析 while (i < lines) { const line = document.lineAt(i++) // 当前行为空白跳过 if (line.isEmptyOrWhitespace) { continue } const text = line.text await this.parseLine(text, i) } return true } /** * 解析当前行,快速找出子视图节点,若子视图节点不存在补全列表中抛出异常 * @param text 当前行文本 * @param i 行号 */ private async parseLine(text: string, i: number) { const len = text.length let j = 0 let nodeBegin = false let svNodeBegin = false let svNodeName = '' while (j < len) { const char = text[j++] if (char === ' ' || char === '>') { // svNode录入结束,开始校验名称是否合法 if (nodeBegin && svNodeBegin) { // 排除内置的 FOR 等节点 if (fm.validateCompName(svNodeName)) { // 验证 compName 是否存在于补全列表中 const res = this.svNameComplet.completionArr.some((item) => item.label === svNodeName) if (res) { // compName 合法则深度校验 await this.parseSubviewNode(text, i - 1, j - 1, svNodeName) } else { // 不在补全列表中 抛出错误 const line = i - 1 const column = j - svNodeName.length - 1 const start = new vscode.Position(line, column) const end = new vscode.Position(line, j - 1) const err: HtmlParseErr = { range: new vscode.Range(start, end), message: this.getErrMessage( line, column, '解析错误', `组件 ${svNodeName} 未定义,请确保其 html 文件和 ts 文件都存在.` ), } throw err } } break } continue } // 标记节点开始录入 if (char === '<') { nodeBegin = true continue } else { if (nodeBegin) { // 标记svNode开始录入 if (/[A-Z]/.test(char)) { svNodeBegin = true } if (svNodeBegin) { svNodeName += char } else { break } } else { break } } } } /** * 深度解析 subviewNode,会获取节点的props,然后迭代props校验是否合法 */ private async parseSubviewNode(text: string, i: number, j: number, compName: string) { const dict = this.parseNodeProps(text, i, j, compName) const keys = Object.keys(dict) this.clearEffect() await this.initPropsCache(compName) for (const key of keys) { if (await this.validateEventHandler(key, compName, dict)) continue if (await this.validateMethodBinder(key, compName, dict)) continue if (this.validateLayout(key)) continue await this.validateProps(key, compName, dict) } if (this.propsArr && this.propsArr.length > 0) { for (const item of this.propsArr) { if (item[item.length - 1] !== '?') { const start = new vscode.Position(i, j - compName.length - 1) const end = new vscode.Position(i, text.length) const range = new vscode.Range(start, end) const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '组件属性非法', `找不到组件 "${compName}" 的必填属性 "${item}"` ), } throw err } } } } /** * 统一错误格式 */ private getErrMessage(line: number, column: number, title: string, message: string) { const texts = [] texts.push(`${title} (${line + 1},${column + 1}) :`) texts.push(message) return texts.join('\n') } /** * 解析节点的属性 * @param text 节点所在行文本 * @param line 节点所在行数 * @param column 开始列数(跳过组件名解析) * @param compName 组件名称 */ private parseNodeProps(text: string, line: number, column: number, compName: string) { const i = line // 从节点名称后开始迭代 例如 获得 ''name> let keyBegin = false let valueBegin = false let quoteBegin = false let key = '' let value = '' let start = 0 const dict: PropsDict = {} const saveProps = (j: number) => { const _start = new vscode.Position(i, start + 1) const end = new vscode.Position(i, j + 1) const range = new vscode.Range(_start, end) if (dict[key]) { // 抛出错误 const err: HtmlParseErr = { range, message: this.getErrMessage(i, j, '解析错误', `组件属性非法: 组件 "${compName}" 的属性 "${key}" 重复`), } throw err } else { dict[key] = { value, range } } } for (let j = column; j < text.length; j++) { const char = text[j] // 标记 key 开始录入 if (char === ' ') { if (!keyBegin && !valueBegin) { keyBegin = true start = j continue } // 特殊处理, 类似 的场景 if (keyBegin) { saveProps(j) key = '' start = j continue } } // 特殊处理, 类似 的场景 if (char === '>' && keyBegin) { saveProps(j) break } // 标记 value 开始录入,key 结束录入 if (char === '=') { keyBegin = false valueBegin = true continue } // 标记引号 if (char === '"') { if (quoteBegin) { quoteBegin = false } else { quoteBegin = true } } // 依据标记 记录 key , value 若匹配到 '>'并且无任何标记 退出循环 if (keyBegin) { key += char } else if (valueBegin) { value += char } else if (!keyBegin && !valueBegin && char === '>') { break } // 标记 value 结束 录入 if (valueBegin && (char === '"' || char === '}') && (text[j + 1] === ' ' || text[j + 1] === '>')) { if (quoteBegin) { continue } valueBegin = false keyBegin = false saveProps(j) key = '' value = '' } } return dict } /** * 校验 method 语法 */ private async validateMethodBinder(key: string, compName: string, dict: PropsDict) { const keyMatcher = /^@method:([a-z][a-zA-Z0-9]*)$/ const res = key.match(keyMatcher) if (res) { // 校验 value const methodName = res[1] const valueMatcher = /^"([a-z][a-zA-Z0-9]+)"$/ const { value, range } = dict[key] const match = value.match(valueMatcher) if (!match) { // 语法错误 const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '非法的子视图方法绑定声明', '请依照语法 \'@method:show="showMsgDialog"\'' ), } throw err } if (this.methodsDict[value]) { // 重复绑定 let viewClass = '' const activeEditor = vscode.window.activeTextEditor if (activeEditor) { viewClass = fm.getCompNameByPath(activeEditor.document.fileName) } const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '@method 绑定方法重复', `视图 ${viewClass} 上已经注册过 ${value} 字段 。请确保父视图注册方法时名称不重复 !` ), } throw err } if (!this.methodsCache[compName]) { this.methodsCache[compName] = await tc.getMethods(compName) } const methods = this.methodsCache[compName] let flag = false if (methods) { flag = methods.some((item) => item.name === methodName) } if (!flag) { // 绑定的字段不存在 const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '@method 绑定方法失败', `视图 ${compName} 上没有暴露 ${methodName} 方法 。如果需要暴露 ${methodName} 方法,请在 ${compName}.ts 中使用 @vMethod 装饰器对其进行标记.` ), } throw err } this.methodsDict[value] = true return true } return false } /** * 校验 EventHandler */ private async validateEventHandler(key: string, compName: string, dict: PropsDict) { const keyMatcher = /^(@bind)?:([a-z][a-zA-Z0-9]*)$/ const res = key.match(keyMatcher) if (res) { // 校验 value const eventName = res[2] const valueMatcher = /^"([a-z][a-zA-Z0-9]+)(?:\(({.+?})\))?"$/ const { value, range } = dict[key] const match = value.match(valueMatcher) if (!match) { // 语法错误 const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, `非法的子视图事件处理器声明 '${key}=${value}'`, `请依照语法 '${key}="handleEvent"' 或者 '${key}="handleEvent({arg1:value, arg2:value})"'` ), } throw err } // 校验 eventName let flag = false if (!this.eventCache[compName]) { this.eventCache[compName] = await tc.getEventsTypeDespByCompName(compName) } const eventsDesp = this.eventCache[compName] if (eventsDesp) { const keys = Object.keys(eventsDesp) if (keys.length > 0) { flag = keys.some((name) => name === eventName) } } if (!flag) { const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '子视图事件处理器绑定失败', `视图 ${compName} 上没有暴露 ${eventName} 事件 。如果需要暴露 ${eventName} 事件,请在 ${compName}.ts 中使用 eventsType 进行声明,并用 this.emitEvent 将其暴露` ), } throw err } return true } return false } /** * 校验 EventHandler */ private validateLayout(key: string) { const keyMatcher = /^@layout$/ const res = key.match(keyMatcher) if (res) { return true } return false } /** * 校验 props */ private async validateProps(key: string, compName: string, dict: PropsDict) { const propsDesp = this.propsCache[compName] let flag = false if (propsDesp && this.propsArr) { if (propsDesp[key]) { flag = true this.propsArr = this.propsArr.filter((e) => e !== key) } else if (propsDesp[key + '?']) { flag = true this.propsArr = this.propsArr.filter((e) => e !== key + '?') } } const { range, value } = dict[key] if (!flag) { const err: HtmlParseErr = { range, message: this.getErrMessage( range.start.line, range.start.character, '组件属性非法', `属性 "${key}" 在组件 "${compName}" 的 propsType 中没有定义` ), } throw err } const editor = vscode.window.activeTextEditor if (editor) { const matchRes = value.match(/"\{\{(.+?)\}\}"/) if (matchRes) { await editor.edit((buffer) => { buffer.replace(range, `${key}={{${matchRes[1]}}}`) }) } } } private async initPropsCache(compName: string) { if (!this.propsCache[compName]) { this.propsCache[compName] = await tc.getPropsDespByCompName(compName) } const propsDesp = this.propsCache[compName] if (propsDesp) { if (!this.propsArr) { this.propsArr = Object.keys(propsDesp) } } } /** * 清除缓存 */ public clearCache() { this.propsCache = {} this.eventCache = {} this.methodsCache = {} } /** * 清除副作用 */ public clearEffect() { this.methodsDict = {} this.propsArr = null } /** * 控制 doParse 的进行 主要是为了防止格式化出现错误,而 doParse 正常时,阻止doParse进行 */ public toggleSwitch(flag: boolean) { this.lock = flag } }