From fab7ab3c3a22bb711e7f5bee0b05d61f4ae26aca Mon Sep 17 00:00:00 2001 From: pqcqaq <905739777@qq.com> Date: Sat, 26 Oct 2024 10:25:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Ewxml=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.ts | 17 +- src/wxml-parser/config.ts | 6 + src/wxml-parser/index.ts | 8 + src/wxml-parser/logParserError.ts | 73 +++ src/wxml-parser/parser.ts | 240 ++++++++++ src/wxml-parser/structs.ts | 240 ++++++++++ syntaxes/xml.language-configuration.json | 34 ++ syntaxes/xml.tmLanguage.json | 560 +++++++++++++++++++++++ 8 files changed, 1174 insertions(+), 4 deletions(-) create mode 100644 src/wxml-parser/config.ts create mode 100644 src/wxml-parser/index.ts create mode 100644 src/wxml-parser/logParserError.ts create mode 100644 src/wxml-parser/parser.ts create mode 100644 src/wxml-parser/structs.ts create mode 100644 syntaxes/xml.language-configuration.json create mode 100644 syntaxes/xml.tmLanguage.json diff --git a/src/extension.ts b/src/extension.ts index 98d14b6..0b44698 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -165,7 +165,11 @@ const checkPagesAndNamespacePlugin = checkPagesAndNamespace(); const createOakTreePanelPlugin = createOakTreePanel(); export async function activate(context: vscode.ExtensionContext) { - const loadPlugin = () => { + const loadPlugin = (config: OakConfiog) => { + // activateWxmlLanguageSupport( + // context, + // Object.assign({}, config.wxml, defaultWxmlConfig) + // ); try { activateOakLocale(context); activateOakComponentPropsLinkProvider(context); @@ -194,7 +198,11 @@ export async function activate(context: vscode.ExtensionContext) { 'Congratulations, your extension "oak-assistant" is now active!' ); - const uris = await vscode.workspace.findFiles('oak.config.json', exclude, 1); + const uris = await vscode.workspace.findFiles( + 'oak.config.json', + exclude, + 1 + ); const fs = vscode.workspace.fs; if (uris.length === 0) { // 获取当前工作区 @@ -223,7 +231,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showInformationMessage( `已将项目主目录设置为: ${projectPath}` ); - loadPlugin(); + loadPlugin({ projectDir: './' }); }); } return; @@ -241,10 +249,11 @@ export async function activate(context: vscode.ExtensionContext) { // 设置projectHome setProjectHome(projectHome); // 通知已经启用 - loadPlugin(); + loadPlugin(config); } export function deactivate() { + // deactivateWxmlLanguageSupport(); commonCommands.dispose(); checkPagesAndNamespacePlugin.dispose(); createOakTreePanelPlugin.dispose(); diff --git a/src/wxml-parser/config.ts b/src/wxml-parser/config.ts new file mode 100644 index 0000000..47eaec6 --- /dev/null +++ b/src/wxml-parser/config.ts @@ -0,0 +1,6 @@ +/****************************************************************** + MIT License http://www.opensource.org/licenses/mit-license.php + Author Mora (https://github.com/qiu8310) +*******************************************************************/ + +export const SourceTags = ['wxs']; diff --git a/src/wxml-parser/index.ts b/src/wxml-parser/index.ts new file mode 100644 index 0000000..ff98950 --- /dev/null +++ b/src/wxml-parser/index.ts @@ -0,0 +1,8 @@ +/****************************************************************** +MIT License http://www.opensource.org/licenses/mit-license.php +Author Mora (https://github.com/qiu8310) +*******************************************************************/ + +export * from './logParserError'; +export * from './parser'; +export * from './structs'; diff --git a/src/wxml-parser/logParserError.ts b/src/wxml-parser/logParserError.ts new file mode 100644 index 0000000..232e5bb --- /dev/null +++ b/src/wxml-parser/logParserError.ts @@ -0,0 +1,73 @@ +/****************************************************************** +MIT License http://www.opensource.org/licenses/mit-license.php +Author Mora (https://github.com/qiu8310) +*******************************************************************/ + +import { ParserError } from './parser'; + +const ELLIPSE = ' ... '; + +/** + * 输出一个便于浏览的 wxml-parser 抛出的异常 + * + * @export + * @param {string} source 解析前的源代码 + * @param {ParserError} e 解析时抛出的异常 + * @param {number} [extraLines=3] 指定同时要输出的出错行的前后几行 + * @param {number} [truncateSize=80] 每行最多输出的字符数(不包括 " Line %d+: ") + */ +export function logParserError( + source: string, + e: ParserError, + extraLines = 3, + truncateSize = 80 +) { + const eol = '\n'; + const prevs = source.substring(0, e.index).split(eol); + const rests = source.substr(e.index).split(eol); + + const p1 = prevs.pop() as string; + let p2 = rests.shift() as string; + const char = p2[0]; + p2 = p2.slice(1); + + const errLineNumber = prevs.length; + const lines = [...prevs, p1 + char + p2, ...rests]; + + lines.forEach((l, i) => { + if (i === errLineNumber) { + const redChar = char; + if (l.length > truncateSize) { + l = + truncate( + p1, + (truncateSize * p1.length) / l.length, + 'left' + ) + + redChar + + truncate( + p2, + (truncateSize * p2.length) / l.length, + 'right' + ); + } else { + l = p1 + redChar + p2; + } + + console.log(`Line ${i}: ${l}`); + console.warn(` ${e.message}`); + } else if (Math.abs(i - errLineNumber) <= extraLines) { + console.log(`Line ${i}: ${truncate(l, truncateSize)}`); + } + }); +} + +function truncate(str: string, size: number, type?: 'left' | 'right') { + if (str.length <= size) {return str;} + + const el = ELLIPSE.length; + str = type === 'left' ? str.slice(el - size) : str.substr(0, size - el); + return type === 'left' + ? ELLIPSE + str.slice(el - size) + : str.substr(0, size - el) + ELLIPSE; +} diff --git a/src/wxml-parser/parser.ts b/src/wxml-parser/parser.ts new file mode 100644 index 0000000..35ca386 --- /dev/null +++ b/src/wxml-parser/parser.ts @@ -0,0 +1,240 @@ +/****************************************************************** +MIT License http://www.opensource.org/licenses/mit-license.php +Author Mora (https://github.com/qiu8310) +*******************************************************************/ + +import { + Node, + TagNodeAttr, + Document, + TextNode, + CommentNode, + TagNode, +} from './structs'; +import { SourceTags } from './config'; + +export class ParserError extends Error { + /** + * 解析失败时的错误 + * @param {number} index 错误位置 + * @param {string} message 错误信息 + * @memberof ParserError + */ + constructor(public index: number, message: string) { + super(message); + } +} + +// tslint:disable:no-conditional-assignment +export function parse(xml: string) { + let lastLocation = 0; + let location = 0; + + return document(); + + function document() { + const doc = new Document(xml); + whitespace(); + let n: Node; + while (!eos() && (n = node())) { + doc.nodes.push(n); + } + return doc; + } + + function node(): Node { + let n: Node; + if (is('/); + if (!m) { + throw new ParserError(location, `comment node has no end tag`); + } else { + return new CommentNode(m[1].trim(), lastLocation, location); + } + } + + function tag(name: string): TagNode { + const n = new TagNode(name, lastLocation); + + whitespace(); + + // attributes + while (!(eos() || is('>') || is('/>'))) { + n.attrs.push(attr()); + whitespace(); + } + + // self closing tag + if (match(/^\/>/)) { + n.selfClose = true; + n.end = location; + return n; + } else if (!match(/^>/)) { + // 文档结束了 + throw new ParserError(location, `expect ">", but got nothing`); + } + n.contentStart = location; + + if (SourceTags.indexOf(n.name) >= 0) { + const source = match(new RegExp(`([\\s\\S]*?)(<\\/${n.name}>)`)); + if (source) { + n.contentEnd = location - source[2].length; + n.end = location; + n.children = [ + new TextNode(source[1], n.contentStart, n.contentEnd), + ]; + return n; + } else { + throw new ParserError( + location, + `expect "", but got nothing` + ); + } + } + + whitespace(); + const closeTag = /^<\/([\w-:.]+)>/; + let child; + while (!eos() && !is(closeTag) && (child = node())) { + n.children.push(child); + } + + // closing + const m = match(closeTag); + if (m) { + if (m[1] === n.name) { + n.contentEnd = lastLocation; + n.end = location; + return n; + } else { + throw new ParserError( + lastLocation, + `expect end tag "", bug got ""` + ); + } + } + + throw new ParserError( + location, + `expect end tag "", bug got nothing` + ); + } + + /** + * Attribute. + */ + function attr() { + const m = match(/^([\w-:.]+)\s*(=\s*("[^"]*"|'[^']*'|\w+))?/); + if (!m) {throw new ParserError(location, `node attribute syntax error`);} + let [, name, hasValue, value] = m; + + let quote = ''; + + if (value) { + quote = value[0]; + if (quote !== '"' && quote !== "'") {quote = '';} + else {value = value.substr(1, value.length - 2);} + } + + return new TagNodeAttr( + name, + hasValue ? value : true, + quote, + location, + lastLocation + ); + } + + /** + * match whitespace + */ + function whitespace() { + match(/^\s*/); + } + + /** + * Match `re` and advance the string. + */ + function match(content: string): string; + function match(reg: RegExp): RegExpMatchArray; + function match(regOrContent: RegExp | string) { + if (typeof regOrContent === 'string') { + if (xml.indexOf(regOrContent) !== 0) {return;} + lastLocation = location; + location += regOrContent.length; + xml = xml.slice(regOrContent.length); + return regOrContent; + } else { + const m = xml.match(regOrContent); + if (!m) {return;} + lastLocation = location; + location += m[0].length; + xml = xml.slice(m[0].length); + return m; + } + } + + /** + * End-of-source. + */ + function eos() { + return 0 === xml.length; + } + + /** + * Check for `prefix`. + */ + function is(prefix: string | RegExp) { + if (typeof prefix === 'string') { + return 0 === xml.indexOf(prefix); + } else { + const m = xml.match(prefix); + return m ? m.index === 0 : false; + } + } +} diff --git a/src/wxml-parser/structs.ts b/src/wxml-parser/structs.ts new file mode 100644 index 0000000..77c5aec --- /dev/null +++ b/src/wxml-parser/structs.ts @@ -0,0 +1,240 @@ +/****************************************************************** +MIT License http://www.opensource.org/licenses/mit-license.php +Author Mora (https://github.com/qiu8310) +*******************************************************************/ + +import { SourceTags } from './config'; + +export namespace Document { + export interface ToXMLOptions { + /** 自定义输出的每行的前缀,默认 "" */ + prefix?: string; + /** tab 使用 space 而不是 \t,默认 true */ + preferSpaces?: boolean; + /** 单个 tab 缩进的数量,默认 2 */ + tabSize?: number; + /** 指定换行符,默认 "\n" */ + eol?: string; + + /** 单行带文本的标签的长度如果超过此限制,则换成多行的写法;如果指定为 0,则表示无限大 */ + maxLineCharacters?: number; + + /** 是否删除注释;注意:开启此选项处理后,原结构中的 CommentNode 都会被移除 */ + removeComment?: boolean; + + /** + * 这里指定的 tag 中的内容不会格式化,会和原内容一致 + * + * 比如在微信小程序中 text 标签中开始的换行和结束的换行都会占用布局,所以这一部分不能被格式化了 + */ + reserveTags?: string[]; + } + + export type RequiredToXMLOptions = Required & { + source: string; + }; +} + +const DefaultToXMLOptions: Document.RequiredToXMLOptions = { + source: '', + prefix: '', + preferSpaces: true, + tabSize: 2, + eol: '\n', + maxLineCharacters: 100, + removeComment: false, + reserveTags: [], +}; + +/** + * wxml 可以是由多个节点组成一个文档 + */ +export class Document { + constructor(public source: string) {} + + nodes: Node[] = []; + + toXML(opts: Document.ToXMLOptions = {}) { + const _: Document.RequiredToXMLOptions = { + ...DefaultToXMLOptions, + ...opts, + source: this.source, + }; + const step = (_.preferSpaces ? ' ' : '\t').repeat(_.tabSize); + + const nodes = opts.removeComment + ? this.nodes.filter(removeComentNode) + : this.nodes; + return nodes.map((n) => toXML(n, _.prefix, step, _)).join(_.eol); + } +} + +function removeComentNode(n: Node) { + if (n.is(TYPE.COMMENT)) { + return false; + } + if (n.is(TYPE.TAG)) { + n.children = n.children.filter(removeComentNode); + } + return true; +} + +export enum TYPE { + TAG, + TEXT, + COMMENT, +} + +export abstract class Location { + /** 节点起始位置 */ + // @ts-ignore + start: number; + /** 节点结束位置 */ + // @ts-ignore + end: number; + + constructor(start?: number, end?: number) { + if (start) { + this.start = start; + } + if (end) { + this.end = end; + } + } +} + +/** + * 节点的基类 + */ +export abstract class Node extends Location { + static TYPE = TYPE; + + is(type: TYPE.TAG): this is TagNode; + is(type: TYPE.TEXT): this is TextNode; + is(type: TYPE.COMMENT): this is CommentNode; + is(type: TYPE) { + return type === TYPE.TAG + ? this instanceof TagNode + : type === TYPE.TEXT + ? this instanceof TextNode + : type === TYPE.COMMENT + ? this instanceof CommentNode + : false; + } +} + +/** + * 注释节点 + */ +export class CommentNode extends Node { + constructor(public comment: string, start?: number, end?: number) { + super(start, end); + } +} +/** + * 文本节点 + */ +export class TextNode extends Node { + constructor(public content: string, start?: number, end?: number) { + super(start, end); + } +} +/** + * 标签节点 + */ +export class TagNode extends Node { + attrs: TagNodeAttr[] = []; + children: Node[] = []; + + /** 是否是自动闭合的标签 */ + selfClose?: boolean; + + /** 标签内容开始的位置(selfClose = false 时此字段才有值) */ + contentStart?: number; + /** 标签内容结束的位置(selfClose = false 时此字段才有值) */ + contentEnd?: number; + + constructor(public name: string, start?: number, end?: number) { + super(start, end); + } + getAttr(key: string) { + return this.attrs.find((a) => a.name === key); + } +} + +/** + * 标签节点的属性 + */ +export class TagNodeAttr extends Location { + constructor( + public name: string, + public value: string | true, + public quote: string, + start?: number, + end?: number + ) { + super(start, end); + } + + toXML() { + const { value, name, quote } = this; + return value !== true ? `${name}=${quote}${value}${quote}` : `${name}`; + } +} + +function toXML( + n: Node, + prefix: string, + step: string, + opts: Document.RequiredToXMLOptions +): string { + if (n.is(TYPE.COMMENT)) { + return prefix + ``; + } else if (n.is(TYPE.TEXT)) { + return prefix + n.content; + } else if (n.is(TYPE.TAG)) { + let prefixedStart = `${prefix}<${n.name}${n.attrs + .map((a) => ' ' + a.toXML()) + .join('')}`; + if (n.selfClose) { + return prefixedStart + ' />'; + } + prefixedStart += '>'; + const endTag = ``; + + if ( + opts.reserveTags.indexOf(n.name) >= 0 && + n.contentEnd && + n.contentStart + ) { + return ( + prefixedStart + + opts.source.substring(n.contentStart, n.contentEnd) + + endTag + ); + } + + const child = n.children[0]; + if (!child) { + return prefixedStart + endTag; + } + + if (n.children.length === 1 && child.is(TYPE.TEXT)) { + const str = prefixedStart + child.content + endTag; + if ( + SourceTags.indexOf(n.name) >= 0 || + opts.maxLineCharacters === 0 || + str.length <= opts.maxLineCharacters + ) { + return str; + } + } + return [ + prefixedStart, + ...n.children.map((_) => toXML(_, prefix + step, step, opts)), + prefix + endTag, + ].join(opts.eol); + } else { + return ''; + } +} diff --git a/syntaxes/xml.language-configuration.json b/syntaxes/xml.language-configuration.json new file mode 100644 index 0000000..09165e3 --- /dev/null +++ b/syntaxes/xml.language-configuration.json @@ -0,0 +1,34 @@ +{ + "comments": { + "blockComment": [ "" ] + }, + "brackets": [ + [""], + ["<", ">"], + ["{", "}"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}"}, + { "open": "[", "close": "]"}, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'" , "notIn": ["string"] }, + { "open": "\"", "close": "\"" , "notIn": ["string"] } + ], + "autoCloseBefore": "\"'-_:<>.,=}# \t\n", + "surroundingPairs": [ + { "open": "'", "close": "'" }, + { "open": "\"", "close": "\"" }, + { "open": "{", "close": "}"}, + { "open": "{{", "close": "}}"}, + { "open": "[", "close": "]"}, + { "open": "(", "close": ")" }, + { "open": "<", "close": ">" } + ], + "folding": { + "markers": { + "start": "^\\s*", + "end": "^^\\s*" + } + } +} diff --git a/syntaxes/xml.tmLanguage.json b/syntaxes/xml.tmLanguage.json new file mode 100644 index 0000000..9448164 --- /dev/null +++ b/syntaxes/xml.tmLanguage.json @@ -0,0 +1,560 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "WXML", + "scopeName": "text.html.wxml", + "repository": { + "tag-id-attribute": { + "end": "(?!\\G)(?<='|\"|[^\\s<>/])", + "name": "meta.attribute-with-value.id.html", + "begin": "\\b(id)\\b\\s*(=)", + "captures": { + "1": { + "name": "entity.other.attribute-name.id.html" + }, + "2": { + "name": "punctuation.separator.key-value.html" + } + }, + "patterns": [ + { + "end": "\"", + "patterns": [ + { + "include": "#wxml-interpolations" + }, + { + "include": "#entities" + } + ], + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.html" + } + }, + "contentName": "meta.toc-list.id.html", + "name": "string.quoted.double.html", + "begin": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.html" + } + } + }, + { + "end": "'", + "patterns": [ + { + "include": "#wxml-interpolations" + }, + { + "include": "#entities" + } + ], + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.html" + } + }, + "contentName": "meta.toc-list.id.html", + "name": "string.quoted.single.html", + "begin": "'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.html" + } + } + }, + { + "name": "string.unquoted.html", + "match": "(?<==)(?:[^\\s<>/'\"]|/(?!>))+", + "captures": { + "0": { + "name": "meta.toc-list.id.html" + } + } + } + ] + }, + "tag-generic-attribute": { + "name": "entity.other.attribute-name.html", + "match": "(?<=[^=])\\b([a-zA-Z0-9:\\-\\_]+)" + }, + "unquoted-attribute": { + "name": "string.unquoted.html", + "match": "(?<==)(?:[^\\s<>/'\"]|/(?!>))+" + }, + "tag-stuff": { + "patterns": [ + { + "include": "#wxml-directives" + }, + { + "include": "#tag-id-attribute" + }, + { + "include": "#tag-generic-attribute" + }, + { + "include": "#string-double-quoted" + }, + { + "include": "#string-single-quoted" + }, + { + "include": "#unquoted-attribute" + } + ] + }, + "entities": { + "patterns": [ + { + "name": "constant.character.entity.html", + "match": "(&)([a-zA-Z0-9]+|#[0-9]+|#x[0-9a-fA-F]+)(;)", + "captures": { + "1": { + "name": "punctuation.definition.entity.html" + }, + "3": { + "name": "punctuation.definition.entity.html" + } + } + }, + { + "name": "invalid.illegal.bad-ampersand.html", + "match": "&" + } + ] + }, + "string-double-quoted": { + "end": "\"", + "patterns": [ + { + "include": "#wxml-interpolations" + }, + { + "include": "#entities" + } + ], + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.html" + } + }, + "name": "string.quoted.double.html", + "begin": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.html" + } + } + }, + "wxml-directives": { + "end": "(?<='|\")|(?=[\\s<>`])", + "name": "meta.directive.wxml", + "begin": "((?:\\b(v-|bind:?|catch:?|capture-bind:?|mut-bind:|capture-catch:)|(:|@))([a-zA-Z\\-\\_]+)(?:\\:([a-zA-Z\\-\\_]+))?(?:\\.([a-zA-Z\\-\\_]+))*)\\s*(=)", + "captures": { + "1": { + "name": "entity.other.attribute-name.html" + }, + "7": { + "name": "punctuation.separator.key-value.html" + } + }, + "patterns": [ + { + "name": "source.directive.wxml", + "contentName": "support.function.wxml", + "begin": "([\"'])", + "beginCaptures": { + "1": { + "name": "string.quoted.start.html" + } + }, + "end": "\\1", + "endCaptures": { + "0": { + "name": "string.quoted.end.html" + } + } + } + ] + }, + "wxml-interpolations": { + "patterns": [ + { + "contentName": "markup.blob markup.heading", + "end": "\\}\\}", + "patterns": [ + { + "include": "source.js#expression" + } + ], + "beginCaptures": { + "0": { + "name": "support.constant.handlebars.wxml" + } + }, + "name": "expression.embedded.wxml", + "begin": "\\{\\{", + "endCaptures": { + "0": { + "name": "support.constant.handlebars.wxml" + } + } + } + ] + }, + "string-single-quoted": { + "end": "'", + "patterns": [ + { + "include": "#wxml-interpolations" + }, + { + "include": "#entities" + } + ], + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.html" + } + }, + "name": "string.quoted.single.html", + "begin": "'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.html" + } + } + } + }, + "uuid": "ca2e4260-5d62-45bf-8cf1-d8b5cc19c8f9", + "patterns": [ + { + "include": "#wxml-interpolations" + }, + { + "end": "(>)(<)(/)(\\2)(>)", + "patterns": [ + { + "include": "#tag-stuff" + } + ], + "beginCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html" + }, + "2": { + "name": "entity.name.tag.html" + } + }, + "name": "meta.tag.any.html", + "begin": "(<)([a-zA-Z0-9:-]++)(?=[^>]*>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.end.html" + }, + "2": { + "name": "punctuation.definition.tag.begin.html meta.scope.between-tag-pair.html" + }, + "3": { + "name": "punctuation.definition.tag.begin.html" + }, + "4": { + "name": "entity.name.tag.html" + }, + "5": { + "name": "punctuation.definition.tag.end.html" + } + } + }, + { + "end": "(\\?>)", + "name": "meta.tag.preprocessor.xml.html", + "begin": "(<\\?)(xml)", + "captures": { + "1": { + "name": "punctuation.definition.tag.html" + }, + "2": { + "name": "entity.name.tag.xml.html" + } + }, + "patterns": [ + { + "include": "#tag-generic-attribute" + }, + { + "include": "#string-double-quoted" + }, + { + "include": "#string-single-quoted" + } + ] + }, + { + "end": "--\\s*>", + "name": "comment.block.html", + "begin": "