Skip to content

Commit 582ec21

Browse files
committed
ステージングしてなかったので慌ててcommitやり直し
1 parent 20644c0 commit 582ec21

File tree

9 files changed

+468
-0
lines changed

9 files changed

+468
-0
lines changed

.github/workflows/publish.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Publish to Bun
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*' # Trigger on tags like v1.0.0, v1.2.3, etc.
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v3
15+
16+
- name: Setup Bun
17+
uses: oven-sh/setup-bun@v2
18+
19+
- name: Publish to Bun Registry
20+
run: bun publish
21+
env:
22+
BUN_API_TOKEN: ${{ secrets.BUN_API_TOKEN }}

bun.lock

Lines changed: 194 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bun
2+
3+
import { program } from 'commander';
4+
import { XivCompiler, CompilerError } from './src/compiler';
5+
import { write } from 'bun';
6+
import path from 'path';
7+
8+
program
9+
.name('xiv')
10+
.description('A simple template engine for creating component-based HTML.')
11+
.version('1.0.0');
12+
13+
program
14+
.argument('<input_file>', 'The main XIV file to compile.')
15+
.option('-o, --output_file <path>', 'Path to the output HTML file', './index.html')
16+
.action(async (inputFile, options) => {
17+
console.log(`\n--- Starting XIV compilation ---`);
18+
console.log(` Input file: ${inputFile}`);
19+
console.log(` Output file: ${options.output_file}`);
20+
21+
const compiler = new XivCompiler();
22+
23+
try {
24+
const compiledHtml = await compiler.compile(inputFile);
25+
26+
const outputPath = path.resolve(options.output_file);
27+
await write(outputPath, compiledHtml);
28+
29+
console.log(`\n✅ Compilation successful. Output saved to '${options.output_file}'`);
30+
console.log("\n--- Compilation Result Preview (first 500 characters) ---");
31+
console.log(compiledHtml.substring(0, 500) + (compiledHtml.length > 500 ? "..." : ""));
32+
33+
} catch (error) {
34+
if (error instanceof CompilerError) {
35+
console.error(`\n❌ Compilation failed: ${error.message}`);
36+
} else if (error instanceof Error) {
37+
console.error(`\n❌ An unexpected error occurred: ${error.message}`);
38+
} else {
39+
console.error(`\n❌ An unexpected and unknown error occurred.`);
40+
}
41+
process.exit(1);
42+
}
43+
});
44+
45+
program.parse(process.argv);

docs/templates/my-card.xiv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div>
2+
<h2>{{title}}</h2>
3+
<p><x-slot /></p>
4+
</div>

src/compiler.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { file } from 'bun';
2+
import path from 'path';
3+
import { html as beautifyHtml } from 'js-beautify';
4+
5+
export class CompilerError extends Error {
6+
constructor(message: string) {
7+
super(message);
8+
this.name = 'CompilerError';
9+
}
10+
}
11+
12+
export class XivCompiler {
13+
public async compile(mainFilePath: string): Promise<string> {
14+
const normalizedMainFilePath = path.normalize(mainFilePath);
15+
16+
const mainFile = file(normalizedMainFilePath);
17+
if (!(await mainFile.exists())) {
18+
throw new CompilerError(`Error: Main XIV file not found: ${normalizedMainFilePath}`);
19+
}
20+
21+
let mainContentRaw: string;
22+
try {
23+
mainContentRaw = await mainFile.text();
24+
} catch (e) {
25+
throw new CompilerError(`Error reading main XIV file: ${e}`);
26+
}
27+
28+
let runtimeScript: string;
29+
try {
30+
const runtimePath = path.join(import.meta.dir, 'runtime', 'xiv.js');
31+
runtimeScript = await file(runtimePath).text();
32+
} catch (e) {
33+
runtimeScript = '// XIV Runtime not found. Interactive features will not work.';
34+
}
35+
36+
let headContent = '';
37+
let bodyContent = '';
38+
39+
const mainMatch = mainContentRaw.match(/<xiv type="main">(.*?)<\/xiv>/s);
40+
let contentToParse = mainContentRaw;
41+
if (mainMatch) {
42+
contentToParse = mainMatch[1];
43+
}
44+
45+
const headMatch = contentToParse.match(/<head>(.*?)<\/head>/s);
46+
if (headMatch) {
47+
headContent = headMatch[1];
48+
}
49+
50+
const bodyMatch = contentToParse.match(/<body>(.*?)<\/body>/s);
51+
if (bodyMatch) {
52+
bodyContent = bodyMatch[1];
53+
} else if (!headMatch) {
54+
bodyContent = contentToParse;
55+
} else if (headMatch && !bodyMatch) {
56+
const headEndIndex = contentToParse.indexOf('</head>') + '</head>'.length;
57+
bodyContent = contentToParse.substring(headEndIndex).trim();
58+
}
59+
60+
const hasTitle = /<title>/i.test(headContent);
61+
62+
const finalHead = `<head>
63+
<meta charset="UTF-8">
64+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
65+
${hasTitle ? '' : '<title>XIV App</title>'}
66+
${headContent}
67+
</head>`;
68+
69+
const finalOutput = `<!DOCTYPE html>
70+
<html lang="en">
71+
${finalHead}
72+
<body>
73+
${bodyContent}
74+
<script>
75+
${runtimeScript}
76+
</script>
77+
</body>
78+
</html>`;
79+
80+
return beautifyHtml(finalOutput, {
81+
indent_size: 2,
82+
space_in_empty_paren: true,
83+
});
84+
}
85+
}

tests/compiler.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { test, expect, describe, beforeAll, afterAll } from 'bun:test';
2+
import { XivCompiler, CompilerError } from '../src/compiler';
3+
import { write, unlink } from 'bun';
4+
import * as cheerio from 'cheerio';
5+
import path from 'path';
6+
7+
const TEST_DIR = './test_temp';
8+
9+
describe('XivCompiler', () => {
10+
let compiler: XivCompiler;
11+
12+
beforeAll(async () => {
13+
compiler = new XivCompiler();
14+
// Create a temporary directory for test files
15+
const dirPath = path.join(import.meta.dir, TEST_DIR);
16+
await new Response(await new Blob([""], { type: "text/plain" }).arrayBuffer()).arrayBuffer(); // Quick way to ensure directory exists
17+
try{
18+
await new Response(await new Blob([""], { type: "text/plain" }).arrayBuffer()).arrayBuffer();
19+
} catch (e) {}
20+
21+
});
22+
23+
afterAll(async () => {
24+
// Clean up temporary files
25+
try {
26+
const tempFile1 = path.join(import.meta.dir, TEST_DIR, 'main1.xiv');
27+
const tempFile2 = path.join(import.meta.dir, TEST_DIR, 'main2.xiv');
28+
await unlink(tempFile1);
29+
await unlink(tempFile2);
30+
} catch (e) {
31+
// Ignore errors if files don't exist
32+
}
33+
});
34+
35+
test('basic compilation and runtime injection', async () => {
36+
const mainXivContent = `
37+
<xiv type="main">
38+
<body>
39+
<h1>Hello XIV</h1>
40+
<p>This is the new era.</p>
41+
</body>
42+
</xiv>`;
43+
const mainXivFile = path.join(import.meta.dir, TEST_DIR, 'main1.xiv');
44+
await write(mainXivFile, mainXivContent);
45+
46+
const result = await compiler.compile(mainXivFile);
47+
const $ = cheerio.load(result);
48+
49+
expect($('body').length).toBe(1);
50+
expect($('body h1').text().trim()).toBe('Hello XIV');
51+
expect($('body p').text().trim()).toBe('This is the new era.');
52+
53+
const scriptTag = $('body script').html();
54+
expect(scriptTag).not.toBeNull();
55+
expect(scriptTag).toInclude('const XIV = {'); // A known string from the runtime
56+
});
57+
58+
test('head content injection', async () => {
59+
const mainXivContent = `
60+
<xiv type="main">
61+
<head>
62+
<title>My Custom Title</title>
63+
<meta name="description" content="Test description">
64+
</head>
65+
<body>
66+
<p>Some content</p>
67+
</body>
68+
</xiv>`;
69+
const mainXivFile = path.join(import.meta.dir, TEST_DIR, 'main2.xiv');
70+
await write(mainXivFile, mainXivContent);
71+
72+
const result = await compiler.compile(mainXivFile);
73+
const $ = cheerio.load(result);
74+
75+
expect($('head').length).toBe(1);
76+
expect($('head title').text().trim()).toBe('My Custom Title');
77+
expect($('head meta[name="description"]').attr('content')).toBe('Test description');
78+
expect($('head title').text()).not.toInclude('XIV App');
79+
});
80+
81+
test('file not found error', async () => {
82+
const nonExistentFile = path.join(import.meta.dir, TEST_DIR, 'nonexistent.xiv');
83+
// Using expect().toThrow() for async functions requires a slightly different syntax
84+
await expect(compiler.compile(nonExistentFile)).rejects.toThrow(CompilerError);
85+
await expect(compiler.compile(nonExistentFile)).rejects.toThrow('Error: Main XIV file not found');
86+
});
87+
});

tests/test_temp/main1.xiv

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
<xiv type="main">
3+
<body>
4+
<h1>Hello XIV</h1>
5+
<p>This is the new era.</p>
6+
</body>
7+
</xiv>

tests/test_temp/main2.xiv

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
<xiv type="main">
3+
<head>
4+
<title>My Custom Title</title>
5+
<meta name="description" content="Test description">
6+
</head>
7+
<body>
8+
<p>Some content</p>
9+
</body>
10+
</xiv>

tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["ESNext"],
4+
"module": "ESNext",
5+
"target": "ESNext",
6+
"moduleResolution": "bundler",
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"allowJs": true,
12+
"types": ["bun"]
13+
}
14+
}

0 commit comments

Comments
 (0)