class MetadataManager { /** * Initializes a new instance of the MetadataManager class. * * @param {Object} options - Configuration options for the metadata manager. * @param {boolean} [options.debug=false] - Enables debug logging if true. * * Sets up the pageMetadata object containing default title, faviconUrl, and an empty meta array. * If debug mode is enabled, logs the initialization options and the initial metadata state. */ constructor(options = {}) { this.options = options; this.pageMetadata = { title: "", faviconUrl: "", meta: [], }; if (this.options.debug) { console.log( "[MetadataManager] Initialized with options:", JSON.stringify(options, null, 2) ); console.log( "[MetadataManager] Initial metadata state:", JSON.stringify(this.pageMetadata, null, 2) ); } } /** * Converts a node to a string for debugging purposes, avoiding circular * references. * @param {Object} node - Node to stringify * @returns {string} String representation of the node */ debugStringify(node) { const getCircularReplacer = () => { const seen = new WeakSet(); return (key, value) => { if (key === "parent") return "[Circular:Parent]"; if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "[Circular]"; } seen.add(value); } return value; }; }; try { return JSON.stringify(node, getCircularReplacer(), 2); } catch (err) { return `[Unable to stringify: ${err.message}]`; } } /** * Processes the metadata of a given node object, updating the internal page metadata state. * * Iterates through the node's properties and children to extract metadata information such as * title, favicon, description, keywords, and author. This information is used to populate * the pageMetadata object. * * For each property or child, it handles known metadata fields directly and adds custom * meta tags for any properties or children with a "meta-" prefix. * * @param {Object} node - The node containing properties and children to process for metadata. */ processPageMetadata(node) { if (this.options.debug) { console.log("\n[MetadataManager] Processing page metadata"); console.log("[MetadataManager] Node details:", this.debugStringify(node)); } if (node.props) { if (this.options.debug) { console.log( `\n[MetadataManager] Processing ${node.props.length} page properties` ); console.log( "[MetadataManager] Properties:", this.debugStringify(node.props) ); } node.props.forEach((prop) => { if (typeof prop === "object" && prop.name && prop.value) { if (this.options.debug) { console.log( `\n[MetadataManager] Processing property:`, this.debugStringify(prop) ); } switch (prop.name) { case "title": this.pageMetadata.title = prop.value; if (this.options.debug) { console.log( `[MetadataManager] Set page title: "${prop.value}"` ); } break; case "favicon": this.pageMetadata.faviconUrl = prop.value; if (this.options.debug) { console.log( `[MetadataManager] Set favicon URL: "${prop.value}"` ); } break; case "description": this.pageMetadata.meta.push({ name: "description", content: prop.value, }); if (this.options.debug) { console.log( `[MetadataManager] Added description meta tag: "${prop.value}"` ); } break; case "keywords": this.pageMetadata.meta.push({ name: "keywords", content: prop.value, }); if (this.options.debug) { console.log( `[MetadataManager] Added keywords meta tag: "${prop.value}"` ); } break; case "author": this.pageMetadata.meta.push({ name: "author", content: prop.value, }); if (this.options.debug) { console.log( `[MetadataManager] Added author meta tag: "${prop.value}"` ); } break; default: if (prop.name.startsWith("meta-")) { const metaName = prop.name.substring(5); this.pageMetadata.meta.push({ name: metaName, content: prop.value, }); if (this.options.debug) { console.log( `[MetadataManager] Added custom meta tag - ${metaName}: "${prop.value}"` ); } } else if (this.options.debug) { console.log( `[MetadataManager] Skipping unknown property: "${prop.name}"` ); } } } }); } if (node.children) { if (this.options.debug) { console.log( `\n[MetadataManager] Processing ${node.children.length} child nodes for metadata` ); } node.children.forEach((child, index) => { if (child.tag) { if (this.options.debug) { console.log( `\n[MetadataManager] Processing child ${index + 1}/${ node.children.length }` ); console.log(`[MetadataManager] Child tag: "${child.tag}"`); console.log( "[MetadataManager] Child details:", this.debugStringify(child) ); } let content = ""; /** * Recursively extracts the text content from a node tree. * * This function traverses the node tree and concatenates the text content of * all text nodes. For non-text nodes, it recursively calls itself on the * children of that node. * * @param {Object} node - The node for which to extract the text content * @return {string} The extracted text content */ const getTextContent = (node) => { if (node.type === "text") return node.value; if (node.children) { return node.children.map(getTextContent).join(""); } return ""; }; content = getTextContent(child); if (this.options.debug) { console.log(`[MetadataManager] Extracted content: "${content}"`); } switch (child.tag) { case "title": this.pageMetadata.title = content; if (this.options.debug) { console.log( `[MetadataManager] Set page title from child: "${content}"` ); } break; case "description": this.pageMetadata.meta.push({ name: "description", content }); if (this.options.debug) { console.log( `[MetadataManager] Added description meta tag from child: "${content}"` ); } break; case "keywords": this.pageMetadata.meta.push({ name: "keywords", content }); if (this.options.debug) { console.log( `[MetadataManager] Added keywords meta tag from child: "${content}"` ); } break; case "author": this.pageMetadata.meta.push({ name: "author", content }); if (this.options.debug) { console.log( `[MetadataManager] Added author meta tag from child: "${content}"` ); } break; default: if (child.tag.startsWith("meta-")) { const metaName = child.tag.substring(5); this.pageMetadata.meta.push({ name: metaName, content }); if (this.options.debug) { console.log( `[MetadataManager] Added custom meta tag from child - ${metaName}: "${content}"` ); } } else if (this.options.debug) { console.log( `[MetadataManager] Skipping unknown child tag: "${child.tag}"` ); } } } }); } if (this.options.debug) { console.log("\n[MetadataManager] Metadata processing complete"); console.log( "[MetadataManager] Final metadata state:", this.debugStringify(this.pageMetadata) ); } } /** * Generates the HTML head content for the page, based on the metadata * previously collected. The generated content includes the page title, * favicon link, meta tags, and stylesheet link. * * @param {string} baseName - The base name of the page (used for the * stylesheet link) * @return {string} The generated HTML head content */ generateHeadContent(baseName) { if (this.options.debug) { console.log("\n[MetadataManager] Generating head content"); console.log(`[MetadataManager] Base name: "${baseName}"`); } let content = ""; const title = this.pageMetadata.title || baseName; content += `