Fix HTML export missing a bunch of Compound variables (#12774)

This commit is contained in:
Michael Telatynski 2024-07-15 11:33:41 +01:00 committed by GitHub
parent 38e1da5626
commit b4ef5d3cc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 315 additions and 173 deletions

View file

@ -163,81 +163,77 @@ export default class HTMLExporter extends Exporter {
<script src="js/script.js"></script>
<title>${_t("export_chat|html_title")}</title>
</head>
<body style="height: 100vh;">
<div
id="matrixchat"
style="height: 100%; overflow: auto"
class="notranslate"
>
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
<div class="mx_MatrixChat">
<main class="mx_RoomView">
<div class="mx_LegacyRoomHeader light-panel">
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
<div class="mx_LegacyRoomHeader_avatar">
<div class="mx_DecoratedRoomAvatar">
${roomAvatar}
</div>
</div>
<div class="mx_LegacyRoomHeader_name">
<div
dir="auto"
class="mx_LegacyRoomHeader_nametext"
title="${safeRoomName}"
>
${safeRoomName}
</div>
</div>
<div class="mx_LegacyRoomHeader_topic" dir="auto"> ${safeTopic} </div>
</div>
</div>
${previousMessagesLink}
<div class="mx_MainSplit">
<div class="mx_RoomView_body">
<div
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
>
<div
class="
mx_AutoHideScrollbar
mx_ScrollPanel
mx_RoomView_messagePanel
"
>
<div class="mx_RoomView_messageListWrapper">
<ol
class="mx_RoomView_MessageList"
aria-live="polite"
role="list"
<body style="height: 100vh;" class="cpd-theme-light">
<div id="matrixchat" style="height: 100%; overflow: auto">
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
<div class="mx_MatrixChat">
<main class="mx_RoomView">
<div class="mx_LegacyRoomHeader light-panel">
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
<div class="mx_LegacyRoomHeader_avatar">
<div class="mx_DecoratedRoomAvatar">
${roomAvatar}
</div>
</div>
<div class="mx_LegacyRoomHeader_name">
<div
dir="auto"
class="mx_LegacyRoomHeader_nametext"
title="${safeRoomName}"
>
${
currentPage == 0
? `<div class="mx_NewRoomIntro">
${roomAvatar}
<h2> ${safeRoomName} </h2>
<p> ${safeCreatedText} <br/><br/> ${safeExportedText} </p>
<br/>
<p> ${safeTopicText} </p>
</div>`
: ""
}
${content}
</ol>
${safeRoomName}
</div>
</div>
<div class="mx_LegacyRoomHeader_topic" dir="auto"> ${safeTopic} </div>
</div>
</div>
${previousMessagesLink}
<div class="mx_MainSplit">
<div class="mx_RoomView_body">
<div
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
>
<div
class="
mx_AutoHideScrollbar
mx_ScrollPanel
mx_RoomView_messagePanel
"
>
<div class="mx_RoomView_messageListWrapper">
<ol
class="mx_RoomView_MessageList"
aria-live="polite"
role="list"
>
${
currentPage == 0
? `<div class="mx_NewRoomIntro">
${roomAvatar}
<h2> ${safeRoomName} </h2>
<p> ${safeCreatedText} <br/><br/> ${safeExportedText} </p>
<br/>
<p> ${safeTopicText} </p>
</div>`
: ""
}
${content}
</ol>
</div>
</div>
</div>
<div class="mx_RoomView_statusArea">
<div class="mx_RoomView_statusAreaBox">
<div class="mx_RoomView_statusAreaBox_line"></div>
</div>
</div>
</div>
</div>
<div class="mx_RoomView_statusArea">
<div class="mx_RoomView_statusAreaBox">
<div class="mx_RoomView_statusAreaBox_line"></div>
</div>
</div>
${nextMessagesLink}
</main>
</div>
</div>
${nextMessagesLink}
</main>
</div>
</div>
</div>
<div id="snackbar"/>
</body>
</html>`;

View file

@ -14,74 +14,80 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Rule, StyleSheet } from "css-tree";
import customCSS from "!!raw-loader!./exportCustomCSS.css";
const cssSelectorTextClassesRegex = /\.[\w-]+/g;
function mutateCssText(css: string): string {
// replace used fonts so that we don't have to bundle Inter & Inconsalata
const sansFont = `-apple-system, BlinkMacSystemFont, avenir next,
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`;
return css
.replace(
/font-family: ?(Inter|'Inter'|"Inter")/g,
`font-family: -apple-system, BlinkMacSystemFont, avenir next,
avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`,
)
.replace(/font-family: ?(Inter|'Inter'|"Inter")/g, `font-family: ${sansFont}`)
.replace(/--cpd-font-family-sans: ?(Inter|'Inter'|"Inter")/g, `--cpd-font-family-sans: ${sansFont}`)
.replace(
/font-family: ?Inconsolata/g,
"font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
);
}
function isLightTheme(sheet: CSSStyleSheet): boolean {
return (<HTMLStyleElement>sheet.ownerNode)?.dataset.mxTheme?.toLowerCase() === "light";
}
function includeRule(rule: Rule, usedClasses: Set<string>): boolean {
if (rule.prelude.type === "Raw") {
// cull empty rules
if (rule.block.children.isEmpty) return false;
async function getRulesFromCssFile(path: string): Promise<CSSStyleSheet> {
const doc = document.implementation.createHTMLDocument("");
const styleElement = document.createElement("style");
const res = await fetch(path);
styleElement.textContent = await res.text();
// the style will only be parsed once it is added to a document
doc.body.appendChild(styleElement);
return styleElement.sheet!;
return rule.prelude.value.split(",").some((subselector) => {
const classes = subselector.trim().match(cssSelectorTextClassesRegex);
if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) {
return false;
}
return true;
});
}
return true;
}
// naively culls unused css rules based on which classes are present in the html,
// doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway.
// We cannot use document.styleSheets as it does not handle variables in shorthand properties sanely,
// see https://github.com/element-hq/element-web/issues/26761
const getExportCSS = async (usedClasses: Set<string>): Promise<string> => {
// only include bundle.css and the data-mx-theme=light styling
const stylesheets = Array.from(document.styleSheets).filter((s) => {
return s.href?.endsWith("bundle.css") || isLightTheme(s);
const csstree = await import("css-tree");
// only include bundle.css and light theme styling
const hrefs = ["bundle.css", "theme-light.css"].map((name) => {
return document.querySelector<HTMLLinkElement>(`link[rel="stylesheet"][href$="${name}"]`)?.href;
});
// If the light theme isn't loaded we will have to fetch & parse it manually
if (!stylesheets.some(isLightTheme)) {
const href = document.querySelector<HTMLLinkElement>('link[rel="stylesheet"][href$="theme-light.css"]')?.href;
if (href) stylesheets.push(await getRulesFromCssFile(href));
}
let css = "";
for (const stylesheet of stylesheets) {
for (const rule of stylesheet.cssRules) {
if (rule instanceof CSSFontFaceRule) continue; // we don't want to bundle any fonts
const selectorText = (rule as CSSStyleRule).selectorText;
for (const href of hrefs) {
if (!href) continue;
const res = await fetch(href);
const text = await res.text();
// only skip the rule if all branches (,) of the selector are redundant
if (
selectorText?.split(",").every((selector) => {
const classes = selector.match(cssSelectorTextClassesRegex);
if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) {
return true; // signal as a redundant selector
}
})
) {
continue; // skip this rule as it is redundant
const ast = csstree.parse(text, {
context: "stylesheet",
parseAtrulePrelude: false,
parseRulePrelude: false,
parseValue: false,
parseCustomProperty: false,
}) as StyleSheet;
for (const rule of ast.children) {
if (rule.type === "Atrule") {
if (rule.name === "font-face") {
continue;
}
}
css += mutateCssText(rule.cssText) + "\n";
if (rule.type === "Rule" && !includeRule(rule, usedClasses)) {
continue;
}
css += mutateCssText(csstree.generate(rule));
}
}

View file

@ -18,6 +18,11 @@ limitations under the License.
This file is raw-imported (imported as plain text) for the export bundle, which is the reason for the .css format and the colours being hard-coded hard-coded.
*/
html,
body {
font-size: var(--cpd-font-size-root) !important;
}
#snackbar {
display: flex;
visibility: hidden;