Skip to content

promote CSS @import to file attachment #474

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {computeHash} from "./hash.js";
import {parseInfo} from "./info.js";
import type {FileReference, ImportReference, PendingTranspile, Transpile} from "./javascript.js";
import {transpileJavaScript} from "./javascript.js";
import {resolveStylesheet} from "./render.js";
import {transpileTag} from "./tag.js";
import {resolvePath} from "./url.js";

Expand Down Expand Up @@ -332,7 +333,6 @@ const SUPPORTED_PROPERTIES: readonly {query: string; src: "href" | "src" | "srcs
{query: "audio source[src]", src: "src"},
{query: "img[src]", src: "src"},
{query: "img[srcset]", src: "srcset"},
{query: "link[href]", src: "href"},
{query: "picture source[srcset]", src: "srcset"},
{query: "video[src]", src: "src"},
{query: "video source[src]", src: "src"}
Expand All @@ -341,9 +341,9 @@ const SUPPORTED_PROPERTIES: readonly {query: string; src: "href" | "src" | "srcs
export function normalizePieceHtml(html: string, sourcePath: string, context: ParseContext): string {
const {document} = parseHTML(html);

// Extracting references to files (such as from linked stylesheets).
const filePaths = new Set<FileReference["path"]>();
const resolvePath = (source: string): FileReference | undefined => {

function resolveFile(source: string): FileReference | undefined {
const path = getLocalPath(sourcePath, source);
if (!path) return;
const file = fileReference(path, sourcePath);
Expand All @@ -352,7 +352,14 @@ export function normalizePieceHtml(html: string, sourcePath: string, context: Pa
context.files.push(file);
}
return file;
};
}

function resolveStylePath(href: string): string {
const file = resolveFile(href);
return file ? file.path : resolveStylesheet(sourcePath, href);
}

// Extract static references to files, such as images.
for (const {query, src} of SUPPORTED_PROPERTIES) {
for (const element of document.querySelectorAll(query)) {
if (src === "srcset") {
Expand All @@ -362,19 +369,43 @@ export function normalizePieceHtml(html: string, sourcePath: string, context: Pa
.map((p) => {
const parts = p.trim().split(/\s+/);
const source = parts[0];
const file = resolvePath(source);
const file = resolveFile(source);
return file ? `${file.path} ${parts.slice(1).join(" ")}`.trim() : parts.join(" ");
})
.filter((p) => !!p);
if (paths && paths.length > 0) element.setAttribute(src, paths.join(", "));
} else {
const source = element.getAttribute(src)!;
const file = resolvePath(source);
const file = resolveFile(source);
if (file) element.setAttribute(src, file.path);
}
}
}

// Resolve any linked stylesheets, e.g. observablehq:theme-light.css. TODO Parse transitive imports.
for (const link of document.querySelectorAll<HTMLLinkElement>("link[rel=stylesheet][href]")) {
link.href = resolveStylePath(link.href);
}

// Resolve any style @import rules. TODO Parse transitive imports.
for (const style of document.querySelectorAll<HTMLStyleElement>("style")) {
if (!style.sheet) continue;
let changed = false;
for (let i = 0; i < style.sheet.cssRules.length; ++i) {
const rule = style.sheet.cssRules[i];
if (rule.type !== 3) continue;
const importRule = rule as CSSImportRule;
const href = importRule.href;
const newHref = resolveStylePath(href);
if (newHref === href) continue;
const mediaText = importRule.media.mediaText;
style.sheet.deleteRule(i);
style.sheet.insertRule(`@import url("${newHref}")${mediaText ? ` ${mediaText}` : ""};`, i);
changed = true;
}
if (changed) style.textContent = String(style.sheet); // propagate cssom to linkedom
}

// Syntax highlighting for <code> elements. The code could contain an inline
// expression within, or other HTML, but we only highlight text nodes that are
// direct children of code elements.
Expand Down
2 changes: 1 addition & 1 deletion src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async function renderLinks(
}`;
}

function resolveStylesheet(path: string, href: string): string {
export function resolveStylesheet(path: string, href: string): string {
return href.startsWith("observablehq:")
? relativeUrl(path, `/_observablehq/${href.slice("observablehq:".length)}`)
: href;
Expand Down
1 change: 1 addition & 0 deletions test/input/build/style/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
main { border: solid 3px green; }
8 changes: 8 additions & 0 deletions test/input/build/style/style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Testing styles

<style>

@import url("observablehq:theme-light.css");
@import url("custom.css");

</style>
1 change: 1 addition & 0 deletions test/output/build/style/_file/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
main { border: solid 3px green; }
44 changes: 44 additions & 0 deletions test/output/build/style/style.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Testing styles</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-auto.css">
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-auto.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&amp;display=swap" crossorigin>
<link rel="modulepreload" href="./_observablehq/client.js">
<link rel="modulepreload" href="./_observablehq/runtime.js">
<link rel="modulepreload" href="./_observablehq/stdlib.js">
<script type="module">

import "./_observablehq/client.js";

</script>
<input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar">
<label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label>
<nav id="observablehq-sidebar">
<ol>
<li class="observablehq-link"><a href="./">Home</a></li>
</ol>
<ol>
<li class="observablehq-link observablehq-link-active"><a href="./style">Testing styles</a></li>
</ol>
</nav>
<script>{/* redacted init script */}</script>
<aside id="observablehq-toc" data-selector="#observablehq-main h1:not(:first-of-type), #observablehq-main h2:not(h1 + h2)">
<nav>
</nav>
</aside>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="testing-styles" tabindex="-1"><a class="observablehq-header-anchor" href="#testing-styles">Testing styles</a></h1>
<style>@import url(./_observablehq/theme-light.css);
@import url(./_file/custom.css);
</style>
</main>
<footer id="observablehq-footer">
<nav><a rel="prev" href="./"><span>Home</span></a></nav>
<div>Built with <a href="https://observablehq.com/" target=_blank>Observable</a></div>
</footer>
</div>