333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
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 += ` <title>${title}</title>\n`;
|
|
if (this.options.debug) {
|
|
console.log(`[MetadataManager] Added title tag: "${title}"`);
|
|
}
|
|
|
|
if (this.pageMetadata.faviconUrl) {
|
|
content += ` <link rel="icon" href="${this.pageMetadata.faviconUrl}">\n`;
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[MetadataManager] Added favicon link: "${this.pageMetadata.faviconUrl}"`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[MetadataManager] Processing ${this.pageMetadata.meta.length} meta tags`
|
|
);
|
|
}
|
|
this.pageMetadata.meta.forEach((meta, index) => {
|
|
content += ` <meta name="${meta.name}" content="${meta.content}">\n`;
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[MetadataManager] Added meta tag ${index + 1}: ${meta.name} = "${
|
|
meta.content
|
|
}"`
|
|
);
|
|
}
|
|
});
|
|
|
|
content += ` <link rel="stylesheet" href="${baseName}.css">\n`;
|
|
if (this.options.debug) {
|
|
console.log(`[MetadataManager] Added stylesheet link: "${baseName}.css"`);
|
|
console.log("\n[MetadataManager] Head content generation complete");
|
|
console.log("[MetadataManager] Generated content:", content);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
}
|
|
|
|
module.exports = MetadataManager;
|