@tdocs/tdocs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/compiler.js +34 -0
- package/build/generator.js +45 -0
- package/build/index.js +62 -0
- package/build/parser.js +99 -0
- package/build/types.js +3 -0
- package/package.json +38 -0
- package/templates/layout.ejs +73 -0
- package/templates/style.css +51 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.compileMarkdown = compileMarkdown;
|
|
7
|
+
const marked_1 = require("marked");
|
|
8
|
+
const marked_highlight_1 = require("marked-highlight");
|
|
9
|
+
const highlight_js_1 = __importDefault(require("highlight.js"));
|
|
10
|
+
marked_1.marked.use((0, marked_highlight_1.markedHighlight)({
|
|
11
|
+
langPrefix: 'hljs language-',
|
|
12
|
+
highlight(code, lang) {
|
|
13
|
+
const language = highlight_js_1.default.getLanguage(lang) ? lang : 'plaintext';
|
|
14
|
+
return highlight_js_1.default.highlight(code, { language }).value;
|
|
15
|
+
}
|
|
16
|
+
}));
|
|
17
|
+
// Phase 2: Markdown Compilation
|
|
18
|
+
// This module will iterate through the AST and transform raw Markdown strings into valid HTML strings.
|
|
19
|
+
function compileMarkdown(ast) {
|
|
20
|
+
// Recursively compile markdown content to html
|
|
21
|
+
function compileNode(node) {
|
|
22
|
+
if (node.markdownContent) {
|
|
23
|
+
node.htmlContent = marked_1.marked.parse(node.markdownContent);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
node.htmlContent = '';
|
|
27
|
+
}
|
|
28
|
+
if (node.children && node.children.length > 0) {
|
|
29
|
+
node.children.forEach(compileNode);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
ast.tree.forEach(compileNode);
|
|
33
|
+
return ast; // Return mutablility-modified AST
|
|
34
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateStaticSite = generateStaticSite;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const ejs_1 = __importDefault(require("ejs"));
|
|
10
|
+
// Phase 3: Static Site Generation (Rendering)
|
|
11
|
+
// This module will take the compiled AST and inject it into UI templates.
|
|
12
|
+
async function generateStaticSite(ast, destDir = './dist') {
|
|
13
|
+
// 1. Ensure dest exists
|
|
14
|
+
if (!fs_1.default.existsSync(destDir)) {
|
|
15
|
+
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
// 2. Read template and render
|
|
18
|
+
const templatePath = path_1.default.join(__dirname, '../templates/layout.ejs');
|
|
19
|
+
const templateStr = fs_1.default.readFileSync(templatePath, 'utf8');
|
|
20
|
+
const htmlOutput = ejs_1.default.render(templateStr, {
|
|
21
|
+
config: ast.config,
|
|
22
|
+
tree: ast.tree,
|
|
23
|
+
serializeTree: (nodes) => JSON.stringify(nodes) // Pass the AST to the client to render content dynamically
|
|
24
|
+
});
|
|
25
|
+
// 3. Write index.html
|
|
26
|
+
fs_1.default.writeFileSync(path_1.default.join(destDir, 'index.html'), htmlOutput);
|
|
27
|
+
// 4. Copy CSS
|
|
28
|
+
const cssPath = path_1.default.join(__dirname, '../templates/style.css');
|
|
29
|
+
if (fs_1.default.existsSync(cssPath)) {
|
|
30
|
+
fs_1.default.copyFileSync(cssPath, path_1.default.join(destDir, 'style.css'));
|
|
31
|
+
}
|
|
32
|
+
// 5. Copy user assets if specified
|
|
33
|
+
if (ast.config && ast.config.assets) {
|
|
34
|
+
const assetsSrcPath = path_1.default.resolve(ast.config.assets);
|
|
35
|
+
if (fs_1.default.existsSync(assetsSrcPath)) {
|
|
36
|
+
const assetsDestPath = path_1.default.join(destDir, path_1.default.basename(assetsSrcPath));
|
|
37
|
+
fs_1.default.cpSync(assetsSrcPath, assetsDestPath, { recursive: true });
|
|
38
|
+
console.log(`Copied assets from ${assetsSrcPath} to ${assetsDestPath}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.warn(`Asset folder not found: ${assetsSrcPath}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log(`Generated documentation at ${destDir}`);
|
|
45
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
9
|
+
const parser_1 = require("./parser");
|
|
10
|
+
const compiler_1 = require("./compiler");
|
|
11
|
+
const generator_1 = require("./generator");
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name('tdoc')
|
|
15
|
+
.description('A custom Static Site Generator for rapid documentation prototyping.')
|
|
16
|
+
.version('1.0.0');
|
|
17
|
+
async function build(file, outDir = './dist') {
|
|
18
|
+
try {
|
|
19
|
+
const ast = (0, parser_1.parseTDoc)(file);
|
|
20
|
+
const compiledAst = (0, compiler_1.compileMarkdown)(ast);
|
|
21
|
+
await (0, generator_1.generateStaticSite)(compiledAst, outDir);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('Error during build:', error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
program
|
|
28
|
+
.command('build')
|
|
29
|
+
.description('Compile a .tdoc file into a static documentation site')
|
|
30
|
+
.argument('<file>', 'The .tdoc file to compile')
|
|
31
|
+
.option('-o, --out <dir>', 'Output directory', './dist')
|
|
32
|
+
.action((file, options) => {
|
|
33
|
+
console.log(`Building site from ${file}...`);
|
|
34
|
+
build(file, options.out);
|
|
35
|
+
});
|
|
36
|
+
const browser_sync_1 = __importDefault(require("browser-sync"));
|
|
37
|
+
// ... (in watch command)
|
|
38
|
+
program
|
|
39
|
+
.command('watch')
|
|
40
|
+
.description('Watch for changes on a .tdoc file and hot-reload')
|
|
41
|
+
.argument('<file>', 'The .tdoc file to watch')
|
|
42
|
+
.option('-o, --out <dir>', 'Output directory', './dist')
|
|
43
|
+
.action(async (file, options) => {
|
|
44
|
+
console.log(`Watching ${file} for changes...`);
|
|
45
|
+
// Initial build
|
|
46
|
+
await build(file, options.out);
|
|
47
|
+
// Initialize BrowserSync
|
|
48
|
+
const bsInstance = browser_sync_1.default.create();
|
|
49
|
+
bsInstance.init({
|
|
50
|
+
server: {
|
|
51
|
+
baseDir: options.out
|
|
52
|
+
},
|
|
53
|
+
open: true,
|
|
54
|
+
notify: false
|
|
55
|
+
});
|
|
56
|
+
chokidar_1.default.watch(file).on('change', async () => {
|
|
57
|
+
console.log(`\nFile ${file} changed. Rebuilding...`);
|
|
58
|
+
await build(file, options.out);
|
|
59
|
+
bsInstance.reload();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
program.parse();
|
package/build/parser.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseTDoc = parseTDoc;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
// The config is simple key-value pairs, let's parse it manually.
|
|
9
|
+
function parseTDoc(filePath) {
|
|
10
|
+
const fileContent = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
11
|
+
let config = {};
|
|
12
|
+
let rawBody = fileContent;
|
|
13
|
+
if (fileContent.startsWith('@config')) {
|
|
14
|
+
const endConfigIdx = fileContent.indexOf('---');
|
|
15
|
+
if (endConfigIdx !== -1) {
|
|
16
|
+
const configBlock = fileContent.substring('@config'.length, endConfigIdx).trim();
|
|
17
|
+
rawBody = fileContent.substring(endConfigIdx + 3).trim();
|
|
18
|
+
// Parse key: value
|
|
19
|
+
const configLines = configBlock.split('\n');
|
|
20
|
+
for (const line of configLines) {
|
|
21
|
+
const colonIdx = line.indexOf(':');
|
|
22
|
+
if (colonIdx !== -1) {
|
|
23
|
+
const key = line.substring(0, colonIdx).trim();
|
|
24
|
+
let val = line.substring(colonIdx + 1).trim();
|
|
25
|
+
// remove surrounding quotes if any
|
|
26
|
+
if (val.startsWith('"') && val.endsWith('"'))
|
|
27
|
+
val = val.substring(1, val.length - 1);
|
|
28
|
+
if (val.startsWith("'") && val.endsWith("'"))
|
|
29
|
+
val = val.substring(1, val.length - 1);
|
|
30
|
+
config[key] = val;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 2. Parse structural markers and markdown content
|
|
36
|
+
const tree = buildTree(rawBody);
|
|
37
|
+
return {
|
|
38
|
+
config,
|
|
39
|
+
tree
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buildTree(content) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
const rootNodes = [];
|
|
45
|
+
const stack = []; // Stack to keep track of parent hierarchy based on indentation
|
|
46
|
+
let currentNode = null;
|
|
47
|
+
const markerRegex = /^(\s*)\[(.*?)\]\s*$/;
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const match = line.match(markerRegex);
|
|
51
|
+
if (match) {
|
|
52
|
+
// It's a structural marker
|
|
53
|
+
const indentStr = match[1];
|
|
54
|
+
const title = match[2];
|
|
55
|
+
const level = indentStr.length;
|
|
56
|
+
const newNode = {
|
|
57
|
+
id: title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
|
58
|
+
title: title,
|
|
59
|
+
markdownContent: '',
|
|
60
|
+
children: [],
|
|
61
|
+
level: level
|
|
62
|
+
};
|
|
63
|
+
// Determine parent based on indentation level
|
|
64
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
65
|
+
stack.pop(); // Pop nodes that are siblings or deeper
|
|
66
|
+
}
|
|
67
|
+
if (stack.length > 0) {
|
|
68
|
+
// We found a parent
|
|
69
|
+
stack[stack.length - 1].children.push(newNode);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// It's a root node
|
|
73
|
+
rootNodes.push(newNode);
|
|
74
|
+
}
|
|
75
|
+
// Make this the current node to append markdown to and push to stack
|
|
76
|
+
currentNode = newNode;
|
|
77
|
+
stack.push(currentNode);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// It's markdown content belonging to the current node
|
|
81
|
+
if (currentNode) {
|
|
82
|
+
// Avoid adding extraneous leading empty lines if content is just starting
|
|
83
|
+
if (currentNode.markdownContent === '' && line.trim() === '') {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
currentNode.markdownContent += line + '\n';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Clean up trailing whitespace on markdown content
|
|
91
|
+
const cleanMarkdown = (nodes) => {
|
|
92
|
+
for (const node of nodes) {
|
|
93
|
+
node.markdownContent = node.markdownContent.trim();
|
|
94
|
+
cleanMarkdown(node.children);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
cleanMarkdown(rootNodes);
|
|
98
|
+
return rootNodes;
|
|
99
|
+
}
|
package/build/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tdocs/tdocs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Custom Static Site Generator for rapid documentation prototyping",
|
|
5
|
+
"main": "./build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tdoc": "./build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build/**/*",
|
|
11
|
+
"templates/**/*"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:cli": "tsc",
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"browser-sync": "^3.0.4",
|
|
19
|
+
"chokidar": "^5.0.0",
|
|
20
|
+
"commander": "^14.0.3",
|
|
21
|
+
"ejs": "^5.0.1",
|
|
22
|
+
"highlight.js": "^11.11.1",
|
|
23
|
+
"marked": "^17.0.4",
|
|
24
|
+
"marked-highlight": "^2.2.3"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/browser-sync": "^2.29.1",
|
|
31
|
+
"@types/ejs": "^3.1.5",
|
|
32
|
+
"@types/marked": "^5.0.2",
|
|
33
|
+
"@types/node": "^25.5.0",
|
|
34
|
+
"gray-matter": "^4.0.3",
|
|
35
|
+
"ts-node": "^10.9.2",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= config.title || 'Documentation' %></title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
9
|
+
<style>
|
|
10
|
+
.nav-item { margin-left: var(--indent, 0px); padding: 5px 0; cursor: pointer; color: var(--accent-color); }
|
|
11
|
+
.nav-item:hover { text-decoration: underline; }
|
|
12
|
+
.nav-item.active { font-weight: bold; color: var(--text-color); }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<header>
|
|
17
|
+
<h1><%= config.header || 'TDoc Site' %></h1>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<div class="layout">
|
|
21
|
+
<aside class="sidebar" id="sidebar">
|
|
22
|
+
<!-- Sidebar navigation populated by JS -->
|
|
23
|
+
</aside>
|
|
24
|
+
|
|
25
|
+
<main class="content" id="content-area">
|
|
26
|
+
<!-- Main documentation content will go here -->
|
|
27
|
+
</main>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<footer>
|
|
31
|
+
<p><%= config.footer || 'Generated with TDoc' %></p>
|
|
32
|
+
</footer>
|
|
33
|
+
|
|
34
|
+
<script>
|
|
35
|
+
const tree = <%- serializeTree(tree) %>;
|
|
36
|
+
|
|
37
|
+
const sidebar = document.getElementById('sidebar');
|
|
38
|
+
const contentArea = document.getElementById('content-area');
|
|
39
|
+
|
|
40
|
+
// Flatten tree to a map for easy lookup
|
|
41
|
+
const nodeMap = {};
|
|
42
|
+
|
|
43
|
+
function renderSidebar(nodes, parentEl) {
|
|
44
|
+
nodes.forEach(node => {
|
|
45
|
+
nodeMap[node.id] = node;
|
|
46
|
+
|
|
47
|
+
const div = document.createElement('div');
|
|
48
|
+
div.className = 'nav-item';
|
|
49
|
+
div.style.setProperty('--indent', `${node.level * 15}px`);
|
|
50
|
+
div.textContent = node.title;
|
|
51
|
+
div.onclick = () => {
|
|
52
|
+
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
|
53
|
+
div.classList.add('active');
|
|
54
|
+
contentArea.innerHTML = node.htmlContent;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
parentEl.appendChild(div);
|
|
58
|
+
|
|
59
|
+
if (node.children && node.children.length > 0) {
|
|
60
|
+
renderSidebar(node.children, parentEl);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
renderSidebar(tree, sidebar);
|
|
66
|
+
|
|
67
|
+
// Render the first item by default if exists
|
|
68
|
+
if (tree.length > 0) {
|
|
69
|
+
sidebar.firstChild.click();
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-color: #ffffff;
|
|
3
|
+
--text-color: #333333;
|
|
4
|
+
--sidebar-bg: #f5f5f5;
|
|
5
|
+
--header-bg: #1a1a1a;
|
|
6
|
+
--header-text: #ffffff;
|
|
7
|
+
--accent-color: #0066cc;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
background-color: var(--bg-color);
|
|
15
|
+
color: var(--text-color);
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
min-height: 100vh;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
header {
|
|
22
|
+
background-color: var(--header-bg);
|
|
23
|
+
color: var(--header-text);
|
|
24
|
+
padding: 1rem 2rem;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.layout {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex: 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sidebar {
|
|
33
|
+
width: 250px;
|
|
34
|
+
background-color: var(--sidebar-bg);
|
|
35
|
+
padding: 2rem 1rem;
|
|
36
|
+
border-right: 1px solid #e0e0e0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.content {
|
|
40
|
+
flex: 1;
|
|
41
|
+
padding: 2rem 4rem;
|
|
42
|
+
max-width: 900px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
footer {
|
|
46
|
+
padding: 1rem 2rem;
|
|
47
|
+
text-align: center;
|
|
48
|
+
border-top: 1px solid #e0e0e0;
|
|
49
|
+
font-size: 0.9rem;
|
|
50
|
+
color: #666;
|
|
51
|
+
}
|