@docuify/engine
Version:
A flexible, pluggable engine for building and transforming documentation content from source files.
370 lines (359 loc) • 10.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// lib/core/engine.ts
var engine_exports = {};
__export(engine_exports, {
DocuifyEngine: () => DocuifyEngine
});
module.exports = __toCommonJS(engine_exports);
// lib/base/basePlugin.ts
var BasePlugin = class {
};
// lib/base/baseSource.ts
var BaseSource = class {
};
// lib/utils/build_tree.ts
var idCounter = 0;
function generateId() {
return `node-${idCounter++}`;
}
function createFolderNode(name, fullPath, parentId) {
return {
id: generateId(),
name,
fullPath,
type: "folder",
parentId,
children: []
};
}
function createFileNode(file, name, fullPath, parentId) {
const nodeId = generateId();
const transformQueue = [];
const getRawContent = file.loadContent;
async function transformContent(rawContent) {
let result = rawContent;
for (const transformFunction of transformQueue) {
result = await transformFunction(result);
}
return result;
}
async function loadContent() {
if (!getRawContent) {
throw new Error(`No content getter for ${file.path}`);
}
const raw = await getRawContent();
return transformContent(raw);
}
return {
id: nodeId,
name,
fullPath,
type: "file",
parentId,
extension: file.extension,
metadata: file.metadata ? { ...file.metadata } : void 0,
// For inspection or debugging purposes — it's like peeking under the hood.
_contentTransformQueue: transformQueue,
// All the file's special powers are grouped here. Transform, load, mutate!
actions: {
useTransform: (fn) => {
transformQueue.push(fn);
},
transformContent,
loadContent
}
};
}
function buildTree(sourceFiles) {
const root = {
id: generateId(),
name: "root",
// Root node — the top of the tree. No parents. Just responsibilities.
fullPath: ".",
type: "folder",
children: []
};
for (const file of sourceFiles) {
const parts = file.path.split("/");
let current = root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isLast = i === parts.length - 1;
if (!current.children) {
current.children = [];
}
let found = current.children.find((child) => child.name === part);
if (!found) {
const fullPath = parts.slice(0, i + 1).join("/");
const newNode = isLast && file.type === "file" ? createFileNode(file, part, fullPath, current.id) : createFolderNode(part, fullPath, current.id);
current.children.push(newNode);
found = newNode;
}
current = found;
}
}
return root;
}
// lib/utils/flatten_tree.ts
function flattenTree(tree) {
const flat = [];
const walk = (node) => {
flat.push(node);
if (node.children) {
for (const child of node.children) {
walk(child);
}
}
delete node.children;
};
walk(tree);
return flat;
}
// lib/utils/walk_tree_with_plugins.ts
async function walkTreeWithPlugins(root, plugins) {
const state = {};
let currentRoot = root;
for (const plugin of plugins) {
if (plugin.applyBefore) {
const result = await plugin.applyBefore(currentRoot, state);
if (result) currentRoot = result;
}
}
async function walk(node, ancestors = [], index) {
const context = {
parent: ancestors[ancestors.length - 1],
ancestors,
index,
visit: (child, ctx) => walk(child, ctx.ancestors, ctx.index),
state
};
for (const plugin of plugins) {
if (plugin.onVisit) {
await plugin.onVisit(node, context);
}
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
await walk(node.children[i], [...ancestors, node], i);
}
}
}
await walk(currentRoot);
for (const plugin of plugins) {
if (plugin.applyAfter) {
const result = await plugin.applyAfter(currentRoot, state);
if (result) currentRoot = result;
}
}
return currentRoot;
}
// lib/core/query_context.ts
var import_isMatch = __toESM(require("lodash/isMatch"));
var QueryContext = class {
flatNodes;
constructor(flatNodes) {
this.flatNodes = flatNodes;
}
/**
* Filters nodes by a deep partial `where` clause.
* Returns a new array of matching nodes (immutable).
*/
findMany(where) {
if (!where) return this.flatNodes;
return this.flatNodes.filter((node) => (0, import_isMatch.default)(node, where));
}
/**
* Finds the first node matching `where`.
* Returns undefined if none found.
*/
findFirst(where) {
if (!where) return this.flatNodes[0];
return this.flatNodes.find((node) => (0, import_isMatch.default)(node, where));
}
/**
* Loads content from a given node if possible.
* Throws if the node has no loadContent action or is not a file.
* Returns loaded content as string.
*/
async loadContent(node) {
if (node.type !== "file") {
throw new Error("loadContent only supported for file nodes");
}
if (!node.actions?.loadContent) {
throw new Error(`Node at ${node.fullPath} has no content loader`);
}
return await node.actions.loadContent();
}
};
// lib/utils/walk_tree.ts
function walkTree(node, visitor) {
visitor(node);
if (node.type === "folder" && node.children?.length) {
for (const child of node.children) {
walkTree(child, visitor);
}
}
}
// lib/utils/preload.ts
async function preloadFiles(files, concurrency = 10, keepContent = false) {
const fileList = files.filter(
(f) => f.type === "file" && typeof f.actions?.loadContent === "function"
);
let index = 0;
const worker = async () => {
while (index < fileList.length) {
const currentIndex = index++;
const file = fileList[currentIndex];
try {
await file.actions?.loadContent?.();
} catch (err) {
console.warn(
`[docuify] Failed to preload content for: ${file.path}`,
err
);
}
}
};
const workers = Array.from({ length: concurrency }, () => worker());
await Promise.all(workers);
}
async function preloadFlatNodesContent(nodes, concurrency = 10, keepContent = false) {
await preloadFiles(nodes, concurrency, keepContent);
}
async function preloadTreeContent(tree, concurrency = 10, keepContent = false) {
const files = [];
walkTree(tree, (node) => {
if (node.type === "file") {
files.push(node);
}
});
await preloadFiles(files, concurrency, keepContent);
}
// lib/core/engine.ts
var DocuifyEngine = class {
config;
tree;
constructor(config) {
if (!config.source) {
throw new Error("DocuifyEngine requires a source. It's the whole point.");
}
this.config = {
...config,
plugins: config.plugins || []
// If no plugins are passed, we still dance alone.
};
}
get buildResult() {
const pluginNames = this.config.plugins.map((p) => p.name);
return {
head: {},
// Reserved for future lore: commit hashes, timestamps, secrets...
tree: this.tree,
foot: {
source: this.config.source.name,
pluginNames
}
};
}
/**
* Builds the Docuify tree from the source.
* Filters are applied here. Lazy loading is respected.
*/
async treeBuilder() {
const rawFiles = await this.config.source.fetch();
const filteredFiles = this.config.filter ? rawFiles.filter(this.config.filter) : rawFiles;
this.tree = buildTree(filteredFiles);
return this.tree;
}
/**
* Applies plugins to the tree. Plugins may mutate nodes, extract metadata, or summon demons.
*/
async applyPlugins() {
this.tree = await walkTreeWithPlugins(this.tree, this.config.plugins);
return this.tree;
}
/**
* Returns the current Docuify tree.
* Caution: this may return undefined if `buildTree` hasn’t run.
*/
getTree() {
return this.tree;
}
/**
* One-click build: fetches source, builds tree, runs plugins.
* Think of it like `npm run build`, but for trees.
*/
async buildTree() {
await this.treeBuilder();
await this.applyPlugins();
if (!this.config.disableContentPreload) {
await preloadTreeContent(this.tree);
}
return this.buildResult;
}
/**
* Flattens the tree and returns only file nodes.
* Ideal for querying, indexing, or just skipping the forest to find your leaf.
*/
async buildFlat() {
await this.treeBuilder();
await this.applyPlugins();
const flatNodes = flattenTree(this.tree).filter(
(node) => node.type === "file"
);
if (!this.config.disableContentPreload) {
await preloadFlatNodesContent(flatNodes);
}
return {
head: {},
nodes: flatNodes,
foot: {
source: this.config.source.name,
pluginNames: this.config.plugins.map((p) => p.name)
}
};
}
use(base) {
if (base && base instanceof BasePlugin) {
this.config.plugins.push(base);
}
if (base && base instanceof BaseSource) {
this.config.source = base;
}
}
async query() {
const { nodes } = await this.buildFlat();
return new QueryContext(nodes);
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DocuifyEngine
});