Skip to content
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
68 changes: 58 additions & 10 deletions crates/fe-web/assets/fe-code-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,34 @@ function _invalidateCodeBlockSheet() {
_codeBlockSheet = null;
}

// Shared list of code blocks waiting for the highlight stylesheet to load.
var _pendingStyleAdoptions = [];
var _stylesheetWatchStarted = false;

function _waitForHighlightStylesheet(codeBlock) {
_pendingStyleAdoptions.push(codeBlock);
if (_stylesheetWatchStarted) return;
_stylesheetWatchStarted = true;

var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute("href") || "";
if (href.indexOf("highlight") !== -1) {
links[i].addEventListener("load", function onLoad() {
this.removeEventListener("load", onLoad);
_invalidateCodeBlockSheet();
var pending = _pendingStyleAdoptions;
_pendingStyleAdoptions = [];
for (var j = 0; j < pending.length; j++) {
pending[j]._adoptStyles();
pending[j]._render();
}
});
return;
}
}
}

/**
* Extract a named region from source text.
* Regions are delimited by `// #region name` and `// #endregion name` comments.
Expand Down Expand Up @@ -161,16 +189,7 @@ class FeCodeBlock extends HTMLElement {
// Create shadow root once
if (!this.shadowRoot) {
this.attachShadow({ mode: "open" });
var sheet = _getCodeBlockSheet();
if (sheet) {
this.shadowRoot.adoptedStyleSheets = [sheet];
} else {
// Fallback: clone page styles into shadow root
var pageStyles = document.querySelectorAll("style");
for (var i = 0; i < pageStyles.length; i++) {
this.shadowRoot.appendChild(pageStyles[i].cloneNode(true));
}
}
this._adoptStyles();
}

this._render();
Expand Down Expand Up @@ -201,6 +220,35 @@ class FeCodeBlock extends HTMLElement {
});
}

/** Adopt highlight styles into shadow root, waiting for stylesheet if needed. */
_adoptStyles() {
var sheet = _getCodeBlockSheet();
if (sheet) {
this.shadowRoot.adoptedStyleSheets = [sheet];
return;
}
// Stylesheet not ready — find the <link> that loads it and wait for load
var self = this;
var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute("href") || "";
if (href.indexOf("highlight") !== -1) {
links[i].addEventListener("load", function onLoad() {
this.removeEventListener("load", onLoad);
_invalidateCodeBlockSheet();
self._adoptStyles();
self._render();
});
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve fallback styling when constructable stylesheets fail

Returning immediately after attaching a load listener skips the <style>-clone fallback path whenever a highlight <link> exists. In browsers where new CSSStyleSheet() is unsupported (or still throws), _getCodeBlockSheet() remains null, so _adoptStyles() keeps taking this branch and never reaches the fallback; if the stylesheet was already loaded before this listener is attached, the callback never fires and code blocks stay permanently unstyled. This is a regression from the previous logic in connectedCallback, which always cloned page styles when the shared sheet could not be created.

Useful? React with 👍 / 👎.

}
}
// No highlight <link> found — clone page <style> tags as fallback
var pageStyles = document.querySelectorAll("style");
for (var j = 0; j < pageStyles.length; j++) {
this.shadowRoot.appendChild(pageStyles[j].cloneNode(true));
}
}

/** Look up an item by path in per-component or global index. */
_findItem(path) {
var index = this._index || window.FE_DOC_INDEX;
Expand Down
208 changes: 136 additions & 72 deletions crates/fe-web/assets/fe-highlight.css
Original file line number Diff line number Diff line change
@@ -1,82 +1,140 @@
/* Fe syntax highlighting (tree-sitter) — One Dark inspired
Standalone version with hardcoded colors for embedding in Starlight/Astro.
Override these CSS custom properties to theme the syntax colors:
--fe-hl-keyword, --fe-hl-type, --fe-hl-type-interface, --fe-hl-type-variant,
--fe-hl-function, --fe-hl-string, --fe-hl-string-escape, --fe-hl-number,
--fe-hl-comment, --fe-hl-operator, --fe-hl-variable, --fe-hl-attribute,
--fe-hl-constant, --fe-hl-punctuation,
--fe-code-bg, --fe-code-border, --fe-code-text, --fe-code-font,
--fe-code-size, --fe-code-line-height */

/* Light mode syntax colors */
.hl-keyword { color: var(--fe-hl-keyword, #7c3aed); }
.hl-type, .hl-type-builtin { color: var(--fe-hl-type, #b45309); }
.hl-type-interface { color: var(--fe-hl-type-interface, #9333ea); }
.hl-type-enum-variant { color: var(--fe-hl-type-variant, #a16207); }
.hl-function, .hl-function-definition, .hl-function-method { color: var(--fe-hl-function, #2563eb); }
.hl-string { color: var(--fe-hl-string, #16a34a); }
.hl-string-escape { color: var(--fe-hl-string-escape, #0d9488); }
.hl-number { color: var(--fe-hl-number, #c2410c); }
.hl-comment, .hl-comment-doc { color: var(--fe-hl-comment, #6b7280); font-style: italic; }
.hl-operator { color: var(--fe-hl-operator, #475569); }
.hl-property, .hl-variable-parameter { color: var(--fe-hl-variable, #dc2626); }
.hl-variable { color: var(--fe-hl-variable, #dc2626); }
.hl-attribute { color: var(--fe-hl-attribute, #c2410c); }
.hl-constant { color: var(--fe-hl-constant, #c2410c); font-weight: bold; }
.hl-punctuation-bracket, .hl-punctuation-delimiter, .hl-punctuation-special { color: var(--fe-hl-punctuation, #64748b); }

/* Dark mode syntax colors */
@media (prefers-color-scheme: dark) {
.hl-keyword { color: var(--fe-hl-keyword, #c678dd); }
.hl-type, .hl-type-builtin { color: var(--fe-hl-type, #e5c07b); }
.hl-type-interface { color: var(--fe-hl-type-interface, #e77ee7); }
.hl-type-enum-variant { color: var(--fe-hl-type-variant, #e7c77e); }
.hl-function, .hl-function-definition, .hl-function-method { color: var(--fe-hl-function, #61afef); }
.hl-string { color: var(--fe-hl-string, #98c379); }
.hl-string-escape { color: var(--fe-hl-string-escape, #56b6c2); }
.hl-number { color: var(--fe-hl-number, #d19a66); }
.hl-comment, .hl-comment-doc { color: var(--fe-hl-comment, #5c6370); font-style: italic; }
.hl-operator { color: var(--fe-hl-operator, #abb2bf); }
.hl-property, .hl-variable-parameter { color: var(--fe-hl-variable, #e06c75); }
.hl-variable { color: var(--fe-hl-variable, #e06c75); }
.hl-attribute { color: var(--fe-hl-attribute, #d19a66); }
.hl-constant { color: var(--fe-hl-constant, #d19a66); font-weight: bold; }
.hl-punctuation-bracket, .hl-punctuation-delimiter, .hl-punctuation-special { color: var(--fe-hl-punctuation, #abb2bf); }
/* Fe syntax highlighting — CSS custom properties define all colors.
Default: Catppuccin Mocha (dark). Light override via prefers-color-scheme
or data-theme="light". Host pages can override any --fe-* variable. */

/* ============================================================================
Color variables
============================================================================ */

/* Default: Catppuccin Mocha (dark) */
:root {
--fe-hl-keyword: #cba6f7;
--fe-hl-variable-builtin: #94e2d5;
--fe-hl-type: #f9e2af;
--fe-hl-type-interface: #cba6f7;
--fe-hl-type-variant: #f9e2af;
--fe-hl-function: #89b4fa;
--fe-hl-string: #a6e3a1;
--fe-hl-string-escape: #94e2d5;
--fe-hl-number: #94e2d5;
--fe-hl-comment: #6c7086;
--fe-hl-operator: #94e2d5;
--fe-hl-variable: #a6e3a1;
--fe-hl-attribute: #cba6f7;
--fe-hl-constant: #94e2d5;
--fe-hl-punctuation: #6c7086;

--fe-code-bg: #1e1e2e;
--fe-code-border: #313244;
--fe-code-text: #cdd6f4;
--fe-code-line-number: #6c7086;
--fe-code-font: "JetBrains Mono", "Fira Code", monospace;
--fe-code-size: 0.8rem;
--fe-code-line-height: 1.6;
--fe-code-radius: 6px;
}

/* Light override: Catppuccin Latte — via OS preference */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--fe-hl-keyword: #7c3aed;
--fe-hl-variable-builtin: #0891b2;
--fe-hl-type: #b08800;
--fe-hl-type-interface: #7c3aed;
--fe-hl-type-variant: #b08800;
--fe-hl-function: #2563eb;
--fe-hl-string: #16a34a;
--fe-hl-string-escape: #0d9488;
--fe-hl-number: #0891b2;
--fe-hl-comment: #9ca0b0;
--fe-hl-operator: #0891b2;
--fe-hl-variable: #0d9488;
--fe-hl-attribute: #7c3aed;
--fe-hl-constant: #0891b2;
--fe-hl-punctuation: #6b7280;

--fe-code-bg: #eff1f5;
--fe-code-border: #ccd0da;
--fe-code-text: #4c4f69;
--fe-code-line-number: #9ca0b0;
}
}

/* Light override: explicit toggle */
:root[data-theme="light"] {
--fe-hl-keyword: #7c3aed;
--fe-hl-variable-builtin: #0891b2;
--fe-hl-type: #b08800;
--fe-hl-type-interface: #7c3aed;
--fe-hl-type-variant: #b08800;
--fe-hl-function: #2563eb;
--fe-hl-string: #16a34a;
--fe-hl-string-escape: #0d9488;
--fe-hl-number: #0891b2;
--fe-hl-comment: #9ca0b0;
--fe-hl-operator: #0891b2;
--fe-hl-variable: #0d9488;
--fe-hl-attribute: #7c3aed;
--fe-hl-constant: #0891b2;
--fe-hl-punctuation: #6b7280;

--fe-code-bg: #eff1f5;
--fe-code-border: #ccd0da;
--fe-code-text: #4c4f69;
--fe-code-line-number: #9ca0b0;
}

/* Smooth transition for SCIP symbol hover highlights.
feHighlight() sets inline styles; the browser tweens the change. */
/* ============================================================================
Syntax highlight rules — read variables, no fallbacks
============================================================================ */

.hl-keyword { color: var(--fe-hl-keyword); }
.hl-variable-builtin { color: var(--fe-hl-variable-builtin); font-style: italic; }
.hl-type, .hl-type-builtin { color: var(--fe-hl-type); }
.hl-type-interface { color: var(--fe-hl-type-interface); }
.hl-type-enum-variant { color: var(--fe-hl-type-variant); }
.hl-function, .hl-function-definition, .hl-function-method { color: var(--fe-hl-function); }
.hl-string { color: var(--fe-hl-string); }
.hl-string-escape { color: var(--fe-hl-string-escape); }
.hl-number { color: var(--fe-hl-number); }
.hl-comment, .hl-comment-doc { color: var(--fe-hl-comment); font-style: italic; }
.hl-operator { color: var(--fe-hl-operator); }
.hl-property, .hl-variable-parameter { color: var(--fe-hl-variable); }
.hl-variable { color: var(--fe-hl-variable); }
.hl-attribute { color: var(--fe-hl-attribute); }
.hl-constant { color: var(--fe-hl-constant); font-weight: bold; }
.hl-punctuation-bracket, .hl-punctuation-delimiter, .hl-punctuation-special { color: var(--fe-hl-punctuation); }

/* SCIP symbol hover highlight transitions */
[class*="sym-"] {
transition: background 0.2s ease-out, text-decoration 0.2s ease-out,
text-decoration-color 0.2s ease-out;
}

/* <fe-code-block> custom element */
/* ============================================================================
<fe-code-block> component layout
============================================================================ */

fe-code-block {
display: block;
margin: 1rem 0;
}

.fe-code-block-wrapper {
display: flex;
background: var(--fe-code-bg, #0d1117);
border: 1px solid var(--fe-code-border, #2d3748);
border-radius: var(--fe-code-radius, 6px);
background: var(--fe-code-bg);
border: 1px solid var(--fe-code-border);
border-radius: var(--fe-code-radius);
overflow-x: auto;
max-width: 100%;
}

/* Tighter margins in inline contexts (member headers, code headers, etc.)
These target the fe-code-block element in the light DOM. The shadow DOM
wrapper styles (border, background) are controlled via CSS custom
properties that inherit through the shadow boundary. */
.member-header fe-code-block,
.code-header fe-code-block,
.implementor-sig fe-code-block {
margin: 0;
}

/* Inline signature contexts: suppress border, keep code background.
(these inherit into the shadow root where .fe-code-block-wrapper reads them). */
.member-header,
.code-header,
.implementor-sig {
Expand All @@ -87,39 +145,45 @@ fe-code-block {
display: flex;
flex-direction: column;
padding: 0.75rem 0.5rem;
border-right: 1px solid var(--fe-code-border, #2d3748);
border-right: 1px solid var(--fe-code-border);
text-align: right;
user-select: none;
color: var(--fe-code-line-number, #9ba3af);
font-family: var(--fe-code-font, "JetBrains Mono", "Fira Code", monospace);
font-size: var(--fe-code-size, 0.8rem);
line-height: var(--fe-code-line-height, 1.6);
color: var(--fe-code-line-number);
font-family: var(--fe-code-font);
font-size: var(--fe-code-size);
line-height: var(--fe-code-line-height);
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}

.fe-line-numbers span {
line-height: var(--fe-code-line-height, 1.6);
line-height: var(--fe-code-line-height);
}

.fe-code-pre {
flex: 1;
min-width: 0;
margin: 0;
padding: 0.75rem 1rem;
background: transparent;
overflow-x: auto;
font-family: var(--fe-code-font, "JetBrains Mono", "Fira Code", monospace);
font-size: var(--fe-code-size, 0.8rem);
line-height: var(--fe-code-line-height, 1.6);
font-family: var(--fe-code-font);
font-size: var(--fe-code-size);
line-height: var(--fe-code-line-height);
/* Prevent iOS Safari TextAutoSizing from inflating font-size in code blocks.
Without this, the algorithm selectively enlarges text in blocks whose
intrinsic width exceeds the viewport (timing-dependent on first load),
causing inconsistent sizes across code blocks with identical styles.
Using 100% (not none) preserves user zoom and accessibility settings. */
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}

.fe-code-pre code {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: var(--fe-code-text, #334155);
}

@media (prefers-color-scheme: dark) {
.fe-code-pre code {
color: var(--fe-code-text, #e4e4e4);
}
color: var(--fe-code-text);
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
Loading
Loading