Fix HTML export missing a bunch of Compound variables (#12774)
This commit is contained in:
parent
38e1da5626
commit
b4ef5d3cc3
8 changed files with 315 additions and 173 deletions
|
@ -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>`;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue