Compare commits
No commits in common. "27bc2fc74741285703c3ad732662b429d0a752ab" and "6fcd879287a38973a4c3fced07fdfb3c02833050" have entirely different histories.
27bc2fc747
...
6fcd879287
|
|
@ -127,7 +127,7 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
||||||
height={`${store.state.dimensions?.cardHeight || 88}mm`}
|
height={`${store.state.dimensions?.cardHeight || 88}mm`}
|
||||||
>
|
>
|
||||||
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
|
<div xmlns="http://www.w3.org/1999/xhtml" class="w-full h-full bg-white">
|
||||||
<div
|
<div
|
||||||
class="absolute"
|
class="absolute"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ interface TableRow {
|
||||||
|
|
||||||
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
|
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
const [rows, setRows] = createSignal<CSV<TableRow>>([] as unknown as CSV<TableRow>);
|
const [rows, setRows] = createSignal<CSV<TableRow>>([]);
|
||||||
const [activeTab, setActiveTab] = createSignal(0);
|
const [activeTab, setActiveTab] = createSignal(0);
|
||||||
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
|
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
|
||||||
const [bodyHtml, setBodyHtml] = createSignal('');
|
const [bodyHtml, setBodyHtml] = createSignal('');
|
||||||
|
|
@ -45,8 +45,7 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const data = csvData();
|
const data = csvData();
|
||||||
if (data) {
|
if (data) {
|
||||||
// 将加载的数据赋值给 rows,CSV 类型已经包含 sourcePath 等属性
|
setRows(data as any[]);
|
||||||
setRows(data as unknown as CSV<TableRow>);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,6 @@ import { For, Show, createEffect } from 'solid-js';
|
||||||
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
||||||
import { createYarnStore } from './stores/yarnStore';
|
import { createYarnStore } from './stores/yarnStore';
|
||||||
|
|
||||||
export interface YarnSpinnerProps {
|
|
||||||
start: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
|
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {loadElementSrc, resolvePath} from "../utils/path";
|
||||||
import {createStore} from "solid-js/store";
|
import {createStore} from "solid-js/store";
|
||||||
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
||||||
import {getTtrpgFunctions} from "./ttrpgRunner";
|
import {getTtrpgFunctions} from "./ttrpgRunner";
|
||||||
import { getIndexedData } from "../../data-loader/file-index";
|
|
||||||
|
|
||||||
type YarnSpinnerStore = {
|
type YarnSpinnerStore = {
|
||||||
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
||||||
|
|
@ -44,8 +43,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
||||||
startAt: props.start,
|
startAt: props.start,
|
||||||
});
|
});
|
||||||
const {commands, functions} = getTtrpgFunctions(runner);
|
const {commands, functions} = getTtrpgFunctions(runner);
|
||||||
runner.registerCommands(commands);
|
|
||||||
runner.registerFunctions(functions);
|
runner.registerFunctions(functions);
|
||||||
|
runner.registerCommands(commands);
|
||||||
return runner;
|
return runner;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize YarnRunner:', error);
|
console.error('Failed to initialize YarnRunner:', error);
|
||||||
|
|
@ -78,7 +77,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
||||||
const processRunnerOutput = () => {
|
const processRunnerOutput = () => {
|
||||||
const runner = store.runnerInstance;
|
const runner = store.runnerInstance;
|
||||||
if(!runner)return;
|
if(!runner)return;
|
||||||
|
|
||||||
const result = runner.currentResult;
|
const result = runner.currentResult;
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
|
|
@ -101,7 +100,7 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
||||||
});
|
});
|
||||||
processRunnerOutput();
|
processRunnerOutput();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store,
|
store,
|
||||||
advance,
|
advance,
|
||||||
|
|
@ -109,8 +108,22 @@ 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 文件并拼接
|
// 加载多个 yarn 文件并拼接
|
||||||
async function loadYarnFiles(paths: string[]): Promise<string> {
|
async function loadYarnFiles(paths: string[]): Promise<string> {
|
||||||
const contents = await Promise.all(paths.map(path => getIndexedData(path)));
|
const contents = await Promise.all(paths.map(path => loadYarnFile(path)));
|
||||||
return contents.join('\n');
|
return contents.join('\n');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { parse } from 'csv-parse/browser/esm/sync';
|
import { parse } from 'csv-parse/browser/esm/sync';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { getIndexedData } from '../../data-loader/file-index';
|
|
||||||
|
/**
|
||||||
|
* 全局缓存已加载的 CSV 内容
|
||||||
|
*/
|
||||||
|
const csvCache = new Map<string, Record<string, string>[]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 front matter
|
* 解析 front matter
|
||||||
|
|
@ -15,7 +19,7 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
||||||
|
|
||||||
// 分割内容
|
// 分割内容
|
||||||
const parts = content.split(/(?:^|\n)---\s*\n/);
|
const parts = content.split(/(?:^|\n)---\s*\n/);
|
||||||
|
|
||||||
// 至少需要三个部分:空字符串、front matter、剩余内容
|
// 至少需要三个部分:空字符串、front matter、剩余内容
|
||||||
if (parts.length < 3) {
|
if (parts.length < 3) {
|
||||||
return { remainingContent: content };
|
return { remainingContent: content };
|
||||||
|
|
@ -25,10 +29,10 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
||||||
// 解析 YAML front matter
|
// 解析 YAML front matter
|
||||||
const frontmatterStr = parts[1].trim();
|
const frontmatterStr = parts[1].trim();
|
||||||
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
|
const frontmatter = yaml.load(frontmatterStr) as JSONObject;
|
||||||
|
|
||||||
// 剩余内容是第三部分及之后的所有内容
|
// 剩余内容是第三部分及之后的所有内容
|
||||||
const remainingContent = parts.slice(2).join('---\n').trimStart();
|
const remainingContent = parts.slice(2).join('---\n').trimStart();
|
||||||
|
|
||||||
return { frontmatter, remainingContent };
|
return { frontmatter, remainingContent };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to parse front matter:', error);
|
console.warn('Failed to parse front matter:', error);
|
||||||
|
|
@ -41,12 +45,16 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
||||||
* @template T 返回数据的类型,默认为 Record<string, string>
|
* @template T 返回数据的类型,默认为 Record<string, string>
|
||||||
*/
|
*/
|
||||||
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
||||||
// 尝试从索引获取
|
if (csvCache.has(path)) {
|
||||||
const content = await getIndexedData(path);
|
return csvCache.get(path)! as CSV<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(path);
|
||||||
|
const content = await response.text();
|
||||||
|
|
||||||
// 解析 front matter
|
// 解析 front matter
|
||||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||||
|
|
||||||
const records = parse(remainingContent, {
|
const records = parse(remainingContent, {
|
||||||
columns: true,
|
columns: true,
|
||||||
comment: '#',
|
comment: '#',
|
||||||
|
|
@ -64,6 +72,8 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
csvResult.sourcePath = path;
|
csvResult.sourcePath = path;
|
||||||
|
|
||||||
|
csvCache.set(path, result);
|
||||||
return csvResult;
|
return csvResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,4 +100,4 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
|
||||||
}
|
}
|
||||||
|
|
||||||
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
|
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
|
||||||
}
|
}
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
/**
|
|
||||||
* 文件索引管理器
|
|
||||||
* 支持任意文件类型的索引加载和缓存
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +1,83 @@
|
||||||
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
||||||
import {getIndexedData, getPathsByExtension} from "./file-index";
|
|
||||||
|
|
||||||
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
|
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[]> }> {
|
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
|
||||||
const mdPaths = await getPathsByExtension("md");
|
await ensureIndexLoaded();
|
||||||
const fileTree = buildFileTree(mdPaths);
|
|
||||||
|
const paths = getIndexedPaths();
|
||||||
|
const fileTree = buildFileTree(paths);
|
||||||
const pathHeadings: Record<string, TocNode[]> = {};
|
const pathHeadings: Record<string, TocNode[]> = {};
|
||||||
|
|
||||||
for (const path of mdPaths) {
|
for (const path of paths) {
|
||||||
const content = await getIndexedData(path);
|
const content = getIndexedData(path);
|
||||||
if (content) {
|
if (content) {
|
||||||
pathHeadings[path] = extractHeadings(content);
|
pathHeadings[path] = extractHeadings(content);
|
||||||
}
|
}
|
||||||
|
|
@ -21,3 +85,31 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading
|
||||||
|
|
||||||
return { fileTree, pathHeadings };
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface WebpackContext {
|
|
||||||
(path: string): { default?: string } | string;
|
|
||||||
keys(): string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
webpackContext(
|
|
||||||
directory: string,
|
|
||||||
options: {
|
|
||||||
recursive?: boolean;
|
|
||||||
regExp?: RegExp;
|
|
||||||
}
|
|
||||||
): WebpackContext;
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
/* icon */
|
/* icon */
|
||||||
icon, pull{
|
icon, pull{
|
||||||
@apply inline-block;
|
@apply inline-block;
|
||||||
width: 2em;
|
width: 1em;
|
||||||
height: 2em;
|
height: 1.27em;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
--icon-src: '';
|
--icon-src: '';
|
||||||
background-image: var(--icon-src);
|
background-image: var(--icon-src);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue