diff --git a/src/tools/index.ts b/src/tools/index.ts index c4e56ae..3568f9b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,3 +2,4 @@ export { download } from './download'; export { log } from './log'; export * as fs from './fs'; export * as json from './json'; +export * as lua from './lua'; diff --git a/src/tools/json.ts b/src/tools/json.ts index d167d59..f8d8955 100644 --- a/src/tools/json.ts +++ b/src/tools/json.ts @@ -80,3 +80,12 @@ export class Json { this._patch = undefined; } } + +/** + * 解析json文本,支持注释和尾随逗号 + * @param text json文本 + * @returns + */ +export function parse(text: string) { + return jsonc.parse(text, undefined, parseOptions); +} diff --git a/src/tools/lua.ts b/src/tools/lua.ts new file mode 100644 index 0000000..35ba113 --- /dev/null +++ b/src/tools/lua.ts @@ -0,0 +1,189 @@ +const encodeOptions = { + newline: '\n', + indent: ' ', + depth: 0, +} as const; + +class Encoder { + private buffer: string[] = []; + public newline = encodeOptions.newline; + public indent = encodeOptions.indent; + public depth = 0; + + constructor() {} + + public encode(jsObject: any) { + this.buffer = []; + + this.encodeValue(jsObject); + + return this.buffer.join(""); + } + + private encodeValue(value: any) { + switch (value) { + case undefined: + this.encodeUndefined(); + return; + case null: + this.encodeNull(); + return; + case true: + this.encodeTrue(); + return; + case false: + this.encodeFalse(); + return; + } + + switch (typeof value) { + case 'string': + this.encodeString(value); + return; + case 'number': + this.encodeNumber(value); + return; + case 'object': + this.encodeObject(value); + return; + } + + this.encodeString(String(value)); + } + + private encodeUndefined() { + this.buffer.push('nil'); + } + + private encodeNull() { + this.buffer.push('nil'); + } + + private encodeTrue() { + this.buffer.push('true'); + } + + private encodeFalse() { + this.buffer.push('false'); + } + + private encodeString(value: string) { + // 字符串中换行符的数量 + let nlNum = value.match('\n')?.length ?? 0; + // 字符串中控制字符和不可见字符的数量,但不包括制表符、换行符 + let ccNum = value.match(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/)?.length ?? 0; + if (ccNum <= nlNum && nlNum > 0) { + this.encodeLongString(value); + } else { + this.encodeShortString(value); + } + } + + private encodeLongString(value: string) { + let end = ']]'; + while (value.includes(end)) { + end = end.slice(0, -2) + '=]'; + } + let start = '[' + end.slice(1, -2) + '['; + this.buffer.push(start, this.newline, value, end); + } + + private encodeShortString(value: string) { + let singleNum = value.match(/'/)?.length ?? 0; + let doubleNum = value.match(/"/)?.length ?? 0; + let delim = doubleNum > singleNum ? "'" : '"'; + + let reg = new RegExp(`[${delim}\\\x00-\x08\x0B-\x1F\x7F-\x9F]`, 'g'); + value = value.replaceAll(reg, (match) => { + if (match === delim || match === '\\') { + return '\\' + delim; + } else { + return `\\x${match.charCodeAt(0).toString(16).padStart(2, '0')}`; + } + }); + this.buffer.push(delim, value, delim); + } + + private encodeNumber(value: number) { + if (Number.isInteger(value)) { + this.buffer.push(value.toString()); + } else if (value === Infinity) { + this.buffer.push('1/0'); + } else if (value === -Infinity) { + this.buffer.push('-1/0'); + } else if (Number.isNaN(value)) { + this.buffer.push('0/0'); + } else { + let str = value.toString(); + if (!str.includes('.') && !str.includes('e')) { + str += '.0'; + } + this.buffer.push(str); + } + } + + private encodeArray(value: any[]) { + if (value.length === 0) { + this.buffer.push('{}'); + } else if (value.length <= 5) { + this.buffer.push('{ '); + for (let i = 0; i < value.length; i++) { + if (i > 0) { + this.buffer.push(', '); + } + this.encodeValue(value[i]); + } + this.buffer.push(' }'); + } else { + this.buffer.push('{', this.newline); + this.depth++; + for (let i = 0; i < value.length; i++) { + this.buffer.push(this.indent.repeat(this.depth)); + this.encodeValue(value[i]); + this.buffer.push(',', this.newline); + } + this.depth--; + this.buffer.push(this.indent.repeat(this.depth)); + this.buffer.push('}'); + } + } + + private encodeTable(value: { [key: string]: any }) { + this.buffer.push('{', this.newline); + this.depth++; + for (const [k, v] of Object.entries(value)) { + this.buffer.push(this.indent.repeat(this.depth)); + if (k.match(/^[_\p{L}][_\p{L}\p{N}]*$/u)) { + this.buffer.push(k); + } else { + this.buffer.push('['); + this.encodeString(k); + this.buffer.push(']'); + } + this.buffer.push(' = '); + this.encodeValue(v); + this.buffer.push(',', this.newline); + } + this.depth--; + this.buffer.push(this.indent.repeat(this.depth)); + this.buffer.push('}'); + } + + private encodeObject(value: Object) { + if (Array.isArray(value)) { + this.encodeArray(value); + } else { + this.encodeTable(value); + } + } +} + + + +export function encode(jsObject: any, options?: Partial) { + let encoder = new Encoder(); + if (options) { + Object.assign(encoder, options); + } + return encoder.encode(jsObject); +} diff --git a/template/plugin/y3-helper.d.ts b/template/plugin/y3-helper.d.ts index a68957e..dfeb3e0 100644 --- a/template/plugin/y3-helper.d.ts +++ b/template/plugin/y3-helper.d.ts @@ -3030,6 +3030,13 @@ declare class Json { set(key: string, value: any): boolean; private applyPatch; } +declare function parse(text: string): any; +declare const encodeOptions: { + readonly newline: "\n"; + readonly indent: " "; + readonly depth: 0; +}; +declare function encode(jsObject: any, options?: Partial): string; export type EditorVersion = "1.0" | "2.0" | "unknown"; declare class Env { private envChangeEmitter; @@ -3109,8 +3116,8 @@ export declare function openInExplorer(uri: vscode.Uri | string): void; export declare function sleep(ms: number): Promise; export declare function assert(exp: any, msg?: string): void; -declare namespace json { - export { Item, JArray, JObject, Json }; +declare namespace lua { + export { encode }; } declare namespace plugin { export { init$3 as init, onceDidRun, runAllPlugins }; @@ -3127,11 +3134,14 @@ declare namespace language { declare namespace fs { export { copy, dir, isAbsolutePath, isDirectory, isExists, isFile, isRelativePath, readFile, removeFile, scan, stat, writeFile }; } +declare namespace json { + export { Item, JArray, JObject, Json, parse }; +} declare namespace consts { export { CSV, Table$1 as Table, Template }; } declare namespace y3 { - export { consts, excel, fs, json, language, plugin, table }; + export { consts, excel, fs, json, language, lua, plugin, table }; } export { @@ -3140,6 +3150,7 @@ export { fs, json, language, + lua, plugin, table, };