chore: use Function() to simplify parsing attribute
This commit is contained in:
parent
eede579e1b
commit
1dcc69ab54
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/// <reference lib="esnext" />
|
||||
|
||||
import { parse as vueSfcParse } from 'vue/compiler-sfc';
|
||||
import {
|
||||
createLogger,
|
||||
|
@ -95,116 +97,13 @@ function generateJavaScriptCode(resolvedRootMarkers: SearchIndexItem[]): string
|
|||
* オブジェクトを特殊な形式の文字列に変換する
|
||||
* i18n参照を保持しつつ適切な形式に変換
|
||||
*/
|
||||
function customStringify(obj: unknown, depth = 0): string {
|
||||
const INDENT_STR = '\t';
|
||||
|
||||
// 配列の処理
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return '[]';
|
||||
const indent = INDENT_STR.repeat(depth);
|
||||
const childIndent = INDENT_STR.repeat(depth + 1);
|
||||
|
||||
// 配列要素の処理
|
||||
const items = obj.map(item => {
|
||||
// オブジェクト要素
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
return `${childIndent}${customStringify(item, depth + 1)}`;
|
||||
}
|
||||
|
||||
// i18n参照を含む文字列要素
|
||||
if (typeof item === 'string' && item.includes('i18n.ts.')) {
|
||||
return `${childIndent}${item}`; // クォートなしでそのまま出力
|
||||
}
|
||||
|
||||
// その他の要素
|
||||
return `${childIndent}${JSON5.stringify(item)}`;
|
||||
}).join(',\n');
|
||||
|
||||
return `[\n${items},\n${indent}]`;
|
||||
}
|
||||
|
||||
// null または非オブジェクト
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return JSON5.stringify(obj);
|
||||
}
|
||||
|
||||
// オブジェクトの処理
|
||||
const indent = INDENT_STR.repeat(depth);
|
||||
const childIndent = INDENT_STR.repeat(depth + 1);
|
||||
|
||||
const entries = Object.entries(obj)
|
||||
// 不要なプロパティを除去
|
||||
.filter(([key, value]) => {
|
||||
if (value === undefined) return false;
|
||||
if (key === 'children' && Array.isArray(value) && value.length === 0) return false;
|
||||
return true;
|
||||
})
|
||||
// 各プロパティを変換
|
||||
.map(([key, value]) => {
|
||||
// 子要素配列の特殊処理
|
||||
if (key === 'children' && Array.isArray(value) && value.length > 0) {
|
||||
return `${childIndent}${key}: ${customStringify(value, depth + 1)}`;
|
||||
}
|
||||
|
||||
// ラベルやその他プロパティを処理
|
||||
return `${childIndent}${key}: ${formatSpecialProperty(key, value)}`;
|
||||
});
|
||||
|
||||
if (entries.length === 0) return '{}';
|
||||
return `{\n${entries.join(',\n')},\n${indent}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 特殊プロパティの書式設定
|
||||
*/
|
||||
function formatSpecialProperty(key: string, value: unknown): string {
|
||||
// 値がundefinedの場合は空文字列を返す
|
||||
if (value === undefined) {
|
||||
return '""';
|
||||
}
|
||||
|
||||
// childrenが配列の場合は特別に処理
|
||||
if (key === 'children' && Array.isArray(value)) {
|
||||
return customStringify(value);
|
||||
}
|
||||
|
||||
// keywordsが配列の場合、特別に処理
|
||||
if (key === 'keywords' && Array.isArray(value)) {
|
||||
return `[${formatArrayForOutput(value)}]`;
|
||||
}
|
||||
|
||||
// 文字列値の場合の特別処理
|
||||
if (typeof value === 'string') {
|
||||
// i18n.ts 参照を含む場合 - クォートなしでそのまま出力
|
||||
if (isI18nReference(value)) {
|
||||
logger.info(`Preserving i18n reference in output: ${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
// keywords が配列リテラルの形式の場合
|
||||
if (key === 'keywords' && value.startsWith('[') && value.endsWith(']')) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// 上記以外は通常の JSON5 文字列として返す
|
||||
return JSON5.stringify(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配列式の文字列表現を生成
|
||||
*/
|
||||
function formatArrayForOutput(items: unknown[]): string {
|
||||
return items.map(item => {
|
||||
// i18n.ts. 参照の文字列はそのままJavaScript式として出力
|
||||
if (typeof item === 'string' && isI18nReference(item)) {
|
||||
logger.info(`Preserving i18n reference in array: ${item}`);
|
||||
return item; // クォートなしでそのまま
|
||||
}
|
||||
|
||||
// その他の値はJSON5形式で文字列化
|
||||
return JSON5.stringify(item);
|
||||
}).join(', ');
|
||||
function customStringify(obj: unknown): string {
|
||||
return JSON.stringify(obj).replaceAll(/"(.*?)"/g, (all, group) => {
|
||||
// propertyAccessProxy が i18n 参照を "${i18n.xxx}"のような形に変換してるので、これをそのまま`${i18n.xxx}`
|
||||
// のような形にすると、実行時にi18nのプロパティにアクセスするようになる。
|
||||
// objectのkeyでは``が使えないので、${ が使われている場合にのみ``に置き換えるようにする
|
||||
return group.includes('${') ? '`' + group + '`' : all;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -269,7 +168,7 @@ function extractDirectContent(node: TemplateChildNode): string | null {
|
|||
// 直接i18n参照を含む場合
|
||||
if (isI18nReference(content)) {
|
||||
logger.info(`Direct i18n reference found: ${content}`);
|
||||
return content;
|
||||
return '$\{' + content + '}';
|
||||
}
|
||||
|
||||
// その他のコンテンツ
|
||||
|
@ -289,7 +188,7 @@ function extractInterpolationContent(children: TemplateChildNode[]): string | nu
|
|||
logger.info(`Interpolation content: ${content}`);
|
||||
|
||||
if (isI18nReference(content)) {
|
||||
return content;
|
||||
return '$\{' + content + '}';
|
||||
}
|
||||
} else if (child.content && typeof child.content === 'object') {
|
||||
if (child.content.type == NodeTypes.COMPOUND_EXPRESSION) throw new Error("Unexpected COMPOUND_EXPRESSION");
|
||||
|
@ -301,7 +200,7 @@ function extractInterpolationContent(children: TemplateChildNode[]): string | nu
|
|||
|
||||
if (isI18nReference(content)) {
|
||||
logger.info(`Found i18n reference in complex interpolation: ${content}`);
|
||||
return content;
|
||||
return '$\{' + content + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +221,7 @@ function extractExpressionContent(children: TemplateChildNode[]): string | null
|
|||
|
||||
if (isI18nReference(expr)) {
|
||||
logger.info(`Found i18n reference in expression node: ${expr}`);
|
||||
return expr;
|
||||
return '$\{' + expr + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -356,7 +255,7 @@ function extractTextContent(children: TemplateChildNode[]): string | null {
|
|||
|
||||
if (mustacheMatch && mustacheMatch[1] && isI18nReference(mustacheMatch[1])) {
|
||||
logger.info(`Extracted i18n ref from text mustache: ${mustacheMatch[1]}`);
|
||||
return mustacheMatch[1].trim();
|
||||
return '$\{' + mustacheMatch[1] + '}';
|
||||
}
|
||||
|
||||
return text;
|
||||
|
@ -578,19 +477,26 @@ function extractUsageInfoFromTemplateAst(
|
|||
|
||||
// バインドプロパティを取得
|
||||
const bindings = extractNodeBindings(node);
|
||||
if (bindings.path) markerInfo.path = bindings.path;
|
||||
if (bindings.icon) markerInfo.icon = bindings.icon;
|
||||
if (bindings.label) markerInfo.label = bindings.label;
|
||||
|
||||
const assertString = (value: unknown, key: string): string => {
|
||||
if (typeof value !== 'string') throw new Error(`Invalid type for ${key} in marker ${markerId}: expected string, got ${typeof value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const assertStringArray = (value: unknown, key: string): string[] => {
|
||||
if (!Array.isArray(value) || !value.every(x => typeof x === 'string')) throw new Error(`Invalid type for ${key} in marker ${markerId}: expected string array`);
|
||||
return value;
|
||||
}
|
||||
|
||||
if (bindings.path) markerInfo.path = assertString(bindings.path, 'path');
|
||||
if (bindings.icon) markerInfo.icon = assertString(bindings.icon, 'icon');
|
||||
if (bindings.label) markerInfo.label = assertString(bindings.label, 'label');
|
||||
if (bindings.inlining) {
|
||||
markerInfo.inlining = bindings.inlining;
|
||||
markerInfo.inlining = assertStringArray(bindings.inlining, 'inlining');
|
||||
logger.info(`Added inlining ${JSON.stringify(bindings.inlining)} to marker ${markerId}`);
|
||||
}
|
||||
if (bindings.keywords) {
|
||||
if (Array.isArray(bindings.keywords)) {
|
||||
markerInfo.keywords = bindings.keywords;
|
||||
} else {
|
||||
markerInfo.keywords = bindings.keywords || [];
|
||||
}
|
||||
markerInfo.keywords = assertStringArray(bindings.keywords, 'keywords');
|
||||
}
|
||||
|
||||
//pathがない場合はファイルパスを設定
|
||||
|
@ -667,11 +573,7 @@ function extractUsageInfoFromTemplateAst(
|
|||
return allMarkers;
|
||||
}
|
||||
|
||||
type SpecialBindings = {
|
||||
inlining: string[];
|
||||
keywords: string[] | string;
|
||||
};
|
||||
type Bindings = Partial<Omit<Record<keyof SearchIndexItem, string>, keyof SpecialBindings> & SpecialBindings>;
|
||||
type Bindings = Partial<Record<keyof SearchIndexItem, unknown>>;
|
||||
// バインドプロパティの処理を修正する関数
|
||||
function extractNodeBindings(node: TemplateChildNode | RootNode): Bindings {
|
||||
const bindings: Bindings = {};
|
||||
|
@ -687,161 +589,72 @@ function extractNodeBindings(node: TemplateChildNode | RootNode): Bindings {
|
|||
|
||||
logger.info(`Processing bind prop ${propName}: ${propContent}`);
|
||||
|
||||
// inliningプロパティの処理を追加
|
||||
if (propName === 'inlining') {
|
||||
try {
|
||||
const content = propContent.trim();
|
||||
|
||||
// 配列式の場合
|
||||
if (content.startsWith('[') && content.endsWith(']')) {
|
||||
// 配列要素を解析
|
||||
const elements = parseArrayExpression(content);
|
||||
if (elements.length > 0) {
|
||||
bindings.inlining = elements;
|
||||
logger.info(`Parsed inlining array: ${JSON5.stringify(elements)}`);
|
||||
} else {
|
||||
bindings.inlining = [];
|
||||
}
|
||||
}
|
||||
// 文字列の場合は配列に変換
|
||||
else if (content) {
|
||||
bindings.inlining = [content]; // 単一の値を配列に
|
||||
logger.info(`Converting inlining to array: [${content}]`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Failed to parse inlining binding: ${propContent}`, e);
|
||||
}
|
||||
}
|
||||
// keywordsの特殊処理
|
||||
else if (propName === 'keywords') {
|
||||
try {
|
||||
const content = propContent.trim();
|
||||
|
||||
// 配列式の場合
|
||||
if (content.startsWith('[') && content.endsWith(']')) {
|
||||
// i18n参照や特殊な式を保持するため、各要素を個別に解析
|
||||
const elements = parseArrayExpression(content);
|
||||
if (elements.length > 0) {
|
||||
bindings.keywords = elements;
|
||||
logger.info(`Parsed keywords array: ${JSON5.stringify(elements)}`);
|
||||
} else {
|
||||
bindings.keywords = [];
|
||||
logger.info('Empty keywords array');
|
||||
}
|
||||
}
|
||||
// その他の式(非配列)
|
||||
else if (content) {
|
||||
bindings.keywords = content; // 式をそのまま保持
|
||||
logger.info(`Keeping keywords as expression: ${content}`);
|
||||
} else {
|
||||
bindings.keywords = [];
|
||||
logger.info('No keywords provided');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Failed to parse keywords binding: ${propContent}`, e);
|
||||
// エラーが起きても何らかの値を設定
|
||||
bindings.keywords = propContent || [];
|
||||
}
|
||||
}
|
||||
// その他のプロパティ
|
||||
else if (propName === 'label') {
|
||||
// ラベルの場合も式として保持
|
||||
bindings[propName] = propContent;
|
||||
logger.info(`Set label from bind expression: ${propContent}`);
|
||||
}
|
||||
else {
|
||||
bindings[propName] = propContent;
|
||||
}
|
||||
bindings[propName] = evalExpression(propContent);
|
||||
}
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
// 配列式をパースする補助関数(文字列リテラル処理を改善)
|
||||
function parseArrayExpression(expr: string): string[] {
|
||||
try {
|
||||
// 単純なケースはJSON5でパースを試みる
|
||||
return JSON5.parse(expr.replace(/'/g, '"'));
|
||||
} catch (e) {
|
||||
// 複雑なケース(i18n.ts.xxx などの式を含む場合)は手動パース
|
||||
logger.info(`Complex array expression, trying manual parsing: ${expr}`);
|
||||
/**
|
||||
* expr を実行します。
|
||||
* i18n はそのアクセスを保持するために propertyAccessProxy を使用しています。
|
||||
*/
|
||||
function evalExpression(expr: string): unknown {
|
||||
const rarResult = Function('i18n', `return ${expr}`)(i18nProxy);
|
||||
// JSON.stringify を一回通すことで、 AccessProxy を文字列に変換する
|
||||
// Walk してもいいんだけど横着してJSON.stringifyしてる。ビルド時にしか通らないのであんまりパフォーマンス気にする必要ないんで
|
||||
return JSON.parse(JSON.stringify(rarResult));
|
||||
}
|
||||
|
||||
// "["と"]"を取り除く
|
||||
const content = expr.substring(1, expr.length - 1).trim();
|
||||
if (!content) return [];
|
||||
const propertyAccessProxySymbol = Symbol('propertyAccessProxySymbol');
|
||||
|
||||
const result: string[] = [];
|
||||
let currentItem = '';
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let stringChar = '';
|
||||
type AccessProxy = {
|
||||
[propertyAccessProxySymbol]: string[],
|
||||
[k: string]: AccessProxy,
|
||||
}
|
||||
|
||||
// カンマで区切る(ただし文字列内や入れ子の配列内のカンマは無視)
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
const char = content[i];
|
||||
|
||||
if (inString) {
|
||||
if (char === stringChar && content[i - 1] !== '\\') {
|
||||
inString = false;
|
||||
}
|
||||
currentItem += char;
|
||||
} else if (char === '"' || char === "'") {
|
||||
inString = true;
|
||||
stringChar = char;
|
||||
currentItem += char;
|
||||
} else if (char === '[') {
|
||||
depth++;
|
||||
currentItem += char;
|
||||
} else if (char === ']') {
|
||||
depth--;
|
||||
currentItem += char;
|
||||
} else if (char === ',' && depth === 0) {
|
||||
// 項目の区切りを検出
|
||||
const trimmed = currentItem.trim();
|
||||
|
||||
// 純粋な文字列リテラルの場合、実際の値に変換
|
||||
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
||||
try {
|
||||
result.push(JSON5.parse(trimmed));
|
||||
} catch (err) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
} else {
|
||||
// それ以外の式はそのまま(i18n.ts.xxx など)
|
||||
result.push(trimmed);
|
||||
}
|
||||
|
||||
currentItem = '';
|
||||
} else {
|
||||
currentItem += char;
|
||||
}
|
||||
const propertyAccessProxyHandler: ProxyHandler<AccessProxy> = {
|
||||
get(target: AccessProxy, p: string | symbol): any {
|
||||
if (p in target) {
|
||||
return (target as any)[p];
|
||||
}
|
||||
|
||||
// 最後の項目を処理
|
||||
if (currentItem.trim()) {
|
||||
const trimmed = currentItem.trim();
|
||||
|
||||
// 純粋な文字列リテラルの場合、実際の値に変換
|
||||
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
||||
try {
|
||||
result.push(JSON5.parse(trimmed));
|
||||
} catch (err) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
} else {
|
||||
// それ以外の式はそのまま(i18n.ts.xxx など)
|
||||
result.push(trimmed);
|
||||
}
|
||||
if (p == "toJSON" || p == Symbol.toPrimitive) {
|
||||
return propertyAccessProxyToJSON;
|
||||
}
|
||||
|
||||
logger.info(`Parsed complex array expression: ${expr} -> ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
if (typeof p == 'string') {
|
||||
return target[p] = propertyAccessProxy([...target[propertyAccessProxySymbol], p]);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function propertyAccessProxyToJSON(this: AccessProxy, hint: string) {
|
||||
const expression = this[propertyAccessProxySymbol].reduce((prev, current) => {
|
||||
if (current.match(/^[a-z][0-9a-z]*$/i)) {
|
||||
return `${prev}.${current}`;
|
||||
} else {
|
||||
return `${prev}['${current}']`;
|
||||
}
|
||||
});
|
||||
return '$\{' + expression + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* プロパティのアクセスを保持するための Proxy オブジェクトを作成します。
|
||||
*
|
||||
* この関数で生成した proxy は JSON でシリアライズするか、`${}`のように string にすると、 ${property.path} のような形になる。
|
||||
* @param path
|
||||
*/
|
||||
function propertyAccessProxy(path: string[]): AccessProxy {
|
||||
const target: AccessProxy = {
|
||||
[propertyAccessProxySymbol]: path,
|
||||
};
|
||||
return new Proxy(target, propertyAccessProxyHandler);
|
||||
}
|
||||
|
||||
const i18nProxy = propertyAccessProxy(['i18n']);
|
||||
|
||||
export function collectFileMarkers(id: string, code: string): SearchIndexItem[] {
|
||||
try {
|
||||
const { descriptor, errors } = vueSfcParse(code, {
|
||||
|
|
Loading…
Reference in New Issue