Compare commits

..

3 Commits

Author SHA1 Message Date
hypercross 27bc2fc747 refactor: cached and reloaded file-index 2026-03-13 15:50:50 +08:00
hypercross 2e04934881 fix: typing 2026-03-13 15:36:35 +08:00
hypercross c668dce348 refactor: larger icons 2026-03-13 15:21:04 +08:00
9 changed files with 133 additions and 139 deletions

View File

@ -127,7 +127,7 @@ export function PrintPreview(props: PrintPreviewProps) {
width={`${store.state.dimensions?.cardWidth || 56}mm`}
height={`${store.state.dimensions?.cardHeight || 88}mm`}
>
<div xmlns="http://www.w3.org/1999/xhtml" class="w-full h-full bg-white">
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
<div
class="absolute"
style={{

View File

@ -17,7 +17,7 @@ interface TableRow {
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
noShadowDOM();
const [rows, setRows] = createSignal<CSV<TableRow>>([]);
const [rows, setRows] = createSignal<CSV<TableRow>>([] as unknown as CSV<TableRow>);
const [activeTab, setActiveTab] = createSignal(0);
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
const [bodyHtml, setBodyHtml] = createSignal('');
@ -45,7 +45,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
createEffect(() => {
const data = csvData();
if (data) {
setRows(data as any[]);
// 将加载的数据赋值给 rowsCSV 类型已经包含 sourcePath 等属性
setRows(data as unknown as CSV<TableRow>);
}
});

View File

@ -3,6 +3,10 @@ import { For, Show, createEffect } from 'solid-js';
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
import { createYarnStore } from './stores/yarnStore';
export interface YarnSpinnerProps {
start: string;
}
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
noShadowDOM();

View File

@ -5,6 +5,7 @@ import {loadElementSrc, resolvePath} from "../utils/path";
import {createStore} from "solid-js/store";
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
import {getTtrpgFunctions} from "./ttrpgRunner";
import { getIndexedData } from "../../data-loader/file-index";
type YarnSpinnerStore = {
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
@ -43,8 +44,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
startAt: props.start,
});
const {commands, functions} = getTtrpgFunctions(runner);
runner.registerFunctions(functions);
runner.registerCommands(commands);
runner.registerFunctions(functions);
return runner;
} catch (error) {
console.error('Failed to initialize YarnRunner:', error);
@ -77,7 +78,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
const processRunnerOutput = () => {
const runner = store.runnerInstance;
if(!runner)return;
const result = runner.currentResult;
if (!result) return;
@ -100,7 +101,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
});
processRunnerOutput();
};
return {
store,
advance,
@ -108,22 +109,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
}
}
// 缓存已加载的 yarn 文件内容
const yarnCache = new Map<string, string>();
// 加载 yarn 文件内容
async function loadYarnFile(path: string): Promise<string> {
if (yarnCache.has(path)) {
return yarnCache.get(path)!;
}
const response = await fetch(path);
const content = await response.text();
yarnCache.set(path, content);
return content;
}
// 加载多个 yarn 文件并拼接
async function loadYarnFiles(paths: string[]): Promise<string> {
const contents = await Promise.all(paths.map(path => loadYarnFile(path)));
const contents = await Promise.all(paths.map(path => getIndexedData(path)));
return contents.join('\n');
}

View File

@ -1,10 +1,6 @@
import { parse } from 'csv-parse/browser/esm/sync';
import yaml from 'js-yaml';
/**
* CSV
*/
const csvCache = new Map<string, Record<string, string>[]>();
import { getIndexedData } from '../../data-loader/file-index';
/**
* front matter
@ -19,7 +15,7 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
// 分割内容
const parts = content.split(/(?:^|\n)---\s*\n/);
// 至少需要三个部分空字符串、front matter、剩余内容
if (parts.length < 3) {
return { remainingContent: content };
@ -29,10 +25,10 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
// 解析 YAML front matter
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
// 剩余内容是第三部分及之后的所有内容
const remainingContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, remainingContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
@ -45,16 +41,12 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
* @template T Record<string, string>
*/
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
if (csvCache.has(path)) {
return csvCache.get(path)! as CSV<T>;
}
// 尝试从索引获取
const content = await getIndexedData(path);
const response = await fetch(path);
const content = await response.text();
// 解析 front matter
const { frontmatter, remainingContent } = parseFrontMatter(content);
const records = parse(remainingContent, {
columns: true,
comment: '#',
@ -72,8 +64,6 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
}
}
csvResult.sourcePath = path;
csvCache.set(path, result);
return csvResult;
}
@ -100,4 +90,4 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
}
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
}
}

View File

@ -0,0 +1,88 @@
/**
*
*
*/
type FileIndex = Record<string, string>;
let fileIndex: FileIndex | null = null;
let indexLoadPromise: Promise<void> | null = null;
/**
*
* CLI JSON Dev 使 webpackContext .md
*/
function ensureIndexLoaded(): Promise<void> {
if (indexLoadPromise) return indexLoadPromise;
indexLoadPromise = (async () => {
// 尝试 CLI 环境:从 /__FILE_INDEX.json 加载
try {
const response = await fetch("/__FILE_INDEX.json");
if (response.ok) {
const index = await response.json();
fileIndex = { ...fileIndex, ...index };
return;
}
} catch (e) {
// CLI 索引不可用时尝试 dev 环境
}
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载 .md, .csv, .yarn 文件
try {
const context = import.meta.webpackContext("../../content", {
recursive: true,
regExp: /\.md|\.yarn|\.csv$/i,
});
const keys = context.keys();
const index: FileIndex = {};
for (const key of keys) {
// context 返回的是模块,需要访问其 default 导出raw-loader 处理后的内容)
const module = context(key) as { default?: string } | string;
const content = typeof module === "string" ? module : module.default ?? "";
const normalizedPath = "/content" + key.slice(1);
index[normalizedPath] = content;
}
fileIndex = { ...fileIndex, ...index };
} catch (e) {
// webpackContext 不可用时忽略
}
})();
return indexLoadPromise;
}
/**
*
*/
export async function getIndexedData(path: string): Promise<string> {
await ensureIndexLoaded();
if (fileIndex && fileIndex[path]) {
return fileIndex[path];
}
const res = await fetch(path);
const content = await res.text();
fileIndex = fileIndex || {};
fileIndex[path] = content;
return content;
}
/**
*
*/
export async function getPathsByExtension(ext: string): Promise<string[]> {
await ensureIndexLoaded();
if (!fileIndex) return [];
const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
return Object.keys(fileIndex).filter(path =>
path.toLowerCase().endsWith(normalizedExt.toLowerCase())
);
}
/**
*
*/
export function clearIndex(): void {
fileIndex = null;
indexLoadPromise = null;
}

View File

@ -1,83 +1,19 @@
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
import {getIndexedData, getPathsByExtension} from "./file-index";
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
let dataIndex: Record<string, string> | null = null;
let indexLoadPromise: Promise<void> | null = null;
/**
*
*/
function getIndexedData(path: string): string | null {
if (dataIndex && dataIndex[path]) {
return dataIndex[path];
}
return null;
}
/**
*
*/
export function getIndexedPaths(): string[] {
if (!dataIndex) return [];
return Object.keys(dataIndex);
}
/**
*
*/
export function ensureIndexLoaded(): Promise<void> {
if (indexLoadPromise) return indexLoadPromise;
indexLoadPromise = (async () => {
// 尝试 CLI 环境:从 /__CONTENT_INDEX.json 加载
try {
const response = await fetch("/__CONTENT_INDEX.json");
if (response.ok) {
const index = await response.json();
dataIndex = { ...dataIndex, ...index };
return;
}
} catch (e) {
// CLI 索引不可用时尝试 dev 环境
}
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载
try {
const context = import.meta.webpackContext("../../content", {
recursive: true,
regExp: /\.md$/,
});
const keys = context.keys();
const index: Record<string, string> = {};
for (const key of keys) {
// context 返回的是模块,需要访问其 default 导出raw-loader 处理后的内容)
const module = context(key) as { default?: string } | string;
const content = typeof module === "string" ? module : module.default ?? "";
const normalizedPath = "/content" + key.slice(1);
index[normalizedPath] = content;
}
dataIndex = { ...dataIndex, ...index };
} catch (e) {
// webpackContext 不可用时忽略
}
})();
return indexLoadPromise;
}
/**
* +
* .md
*/
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
await ensureIndexLoaded();
const paths = getIndexedPaths();
const fileTree = buildFileTree(paths);
const mdPaths = await getPathsByExtension("md");
const fileTree = buildFileTree(mdPaths);
const pathHeadings: Record<string, TocNode[]> = {};
for (const path of paths) {
const content = getIndexedData(path);
for (const path of mdPaths) {
const content = await getIndexedData(path);
if (content) {
pathHeadings[path] = extractHeadings(content);
}
@ -85,31 +21,3 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading
return { fileTree, pathHeadings };
}
/**
*
* @param path
* @returns
*/
export async function fetchData(path: string): Promise<string> {
await ensureIndexLoaded();
// 首先尝试从索引获取
const indexedData = getIndexedData(path);
if (indexedData) {
return indexedData;
}
// 索引不存在时,使用 fetch 加载
if (dataIndex !== null) throw new Error(`no data in index: ${path}`);
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Failed to fetch: ${path}`);
}
return await response.text();
} catch (error) {
console.error("fetchData error:", error);
throw error;
}
}

16
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
/// <reference types="vite/client" />
interface WebpackContext {
(path: string): { default?: string } | string;
keys(): string[];
}
interface ImportMeta {
webpackContext(
directory: string,
options: {
recursive?: boolean;
regExp?: RegExp;
}
): WebpackContext;
}

View File

@ -4,8 +4,8 @@
/* icon */
icon, pull{
@apply inline-block;
width: 1em;
height: 1.27em;
width: 2em;
height: 2em;
vertical-align: text-bottom;
--icon-src: '';
background-image: var(--icon-src);