elm-pages
Version:
Hybrid Elm framework with full-stack and static routes.
106 lines (97 loc) • 3.06 kB
JavaScript
/** @typedef { { type: 'root'; keyValuePair: [string, string] } } RootTagModifier */
/**
* @param {( SeoTag | RootTagModifier )[]} tags
*/
export function gather(tags) {
const withoutRootModifiers = tags.flatMap((value) => {
if (value.type === "root") {
return [];
} else {
return [value];
}
});
const rootModifiers = tags.flatMap((value) => {
if (value.type === "root") {
return [value];
} else {
return [];
}
});
return {
rootElement: headTag(rootModifiers),
headTags: toString(withoutRootModifiers),
};
}
/**
* @param {RootTagModifier[]} rootModifiers
*/
function headTag(rootModifiers) {
const rootModifiersMap = Object.fromEntries(
rootModifiers.map((modifier) => modifier.keyValuePair)
);
if (!("lang" in rootModifiersMap)) {
rootModifiersMap["lang"] = "en";
}
return `<html ${Object.entries(rootModifiersMap)
.map(pairToAttribute)
.join(" ")}>`;
}
function toString(/** @type { SeoTag[] } */ tags) {
return tags
.flatMap((headTag) => {
if (headTag.type === "head") {
return [appendTag(headTag)];
} else if (headTag.type === "json-ld") {
return [appendJsonLdTag(headTag)];
} else if (headTag.type === "stripped") {
console.warn(
`WARNING: Head.nonLoadingTag value ignored because it used a loading tag: ${headTag.message}`
);
return [];
} else {
throw new Error(`Unknown tag type ${JSON.stringify(headTag)}`);
}
})
.join("");
}
/** @typedef {HeadTag | JsonLdTag | StrippedTag} SeoTag */
/** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
function appendTag(/** @type {HeadTag} */ tagDetails) {
const tagsString = tagDetails.attributes.map(([name, value]) => {
return pairToAttribute([name, value]);
});
return ` <${tagDetails.name} ${tagsString.join(" ")} />`;
}
/** @typedef {{ contents: Object; type: 'json-ld' }} JsonLdTag */
/** @typedef {{ message: string; type: 'stripped' }} StrippedTag */
function appendJsonLdTag(/** @type {JsonLdTag} */ tagDetails) {
return `<script type="application/ld+json">
${JSON.stringify(tagDetails.contents)}
</script>`;
}
/**
*
* @param {[string, string]} param0
* @returns string
*/
function pairToAttribute([name, value]) {
return `${name}="${quoteattr(value)}"`;
}
function quoteattr(s, preserveCR) {
preserveCR = preserveCR ? " " : "\n";
return (
("" + s) /* Forces the conversion to string. */
.replace(/&/g, "&") /* This MUST be the 1st replacement. */
.replace(/'/g, "'") /* The 4 other predefined entities, required. */
.replace(/"/g, """)
.replace(/</g, "<")
.replace(/>/g, ">")
/*
You may add other replacements here for HTML only
(but it's not necessary).
Or for XML, only if the named entities are defined in its DTD.
*/
.replace(/\r\n/g, preserveCR) /* Must be before the next replacement. */
.replace(/[\r\n]/g, preserveCR)
);
}