Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#pragma once
#include <string_view>

namespace webscene_native {
// The pinned WebScene exposes a stable native HTMLStyleElement.sheet object,
// but no rule mutation methods. Its textContent setter *does* replace that
// owner's parsed native rules, recascade, and invalidate layout. Bridge the
// dynamic style-rule operations used by Code OSS to that existing renderer.
// This is a bounded adapter, not a complete CSSOM implementation: constructed
// sheets, imported-sheet inspection and nested rule mutation remain unsupported.
// Semantics: https://www.w3.org/TR/cssom-1/
inline constexpr std::string_view cssCompatibilityScript = R"JS(
// SCENETECH_CSS_COMPATIBILITY_V1
(() => {
'use strict';
if (typeof globalThis.__webSceneAugmentStyleSheet === 'function') return;
const sheets = new WeakMap();
const exception = (message, name) => { throw new DOMException(message, name); };
const text = value => {
if (typeof value === 'symbol') throw new TypeError('A CSS rule cannot be a Symbol');
return String(value);
};
// Split only at top-level CSS rule boundaries. Quoted strings, escaped
// delimiters, comments, URLs, attribute selectors and nested blocks are kept
// intact. The actual declarations/selectors are interpreted by native CSS.
const splitRules = source => {
const rules = [];
let start = -1, open = -1, braces = 0, parentheses = 0, brackets = 0;
let quote = '', comment = false;
for (let index = 0; index < source.length; index++) {
const character = source[index], next = source[index + 1];
if (comment) {
if (character === '*' && next === '/') { comment = false; index++; }
continue;
}
if (quote) {
if (character === '\\') index++;
else if (character === quote) quote = '';
continue;
}
if (character === '/' && next === '*') { comment = true; index++; continue; }
if (start < 0) {
if (/\s/.test(character)) continue;
start = index;
}
if (character === '\\') { index++; continue; }
if (character === '"' || character === "'") { quote = character; continue; }
if (character === '(') parentheses++;
if (character === ')') parentheses--;
if (character === '[') brackets++;
if (character === ']') brackets--;
if (parentheses < 0 || brackets < 0) exception('Unbalanced CSS delimiters', 'SyntaxError');
if (parentheses || brackets) continue;
if (character === '{') { if (braces++ === 0) open = index; }
if (character === '}' && --braces < 0) exception('Unbalanced CSS block', 'SyntaxError');
if ((character === '}' && braces === 0) || (character === ';' && braces === 0)) {
const cssText = source.slice(start, index + 1).trim();
const selectorText = source[start] === '@' || open < 0 ? undefined : source.slice(start, open).trim();
if (open < 0 && source[start] !== '@') exception('A CSS style rule requires a block', 'SyntaxError');
rules.push({ cssText, selectorText, body: open < 0 ? undefined : source.slice(open + 1, index) });
start = -1; open = -1;
}
}
if (start >= 0 || braces || parentheses || brackets || quote || comment)
exception('An incomplete CSS rule is unsupported', 'SyntaxError');
return rules;
};
const stateFor = sheet => {
const state = sheets.get(sheet);
if (!state) throw new TypeError('Illegal stylesheet receiver');
synchronize(state);
return state;
};
const publish = state => {
// Native textContent replacement is the rendering operation. Do not merely
// update JavaScript records: every insertion/deletion reaches the cascade.
const source = state.rules.map(rule => rule.cssText).join('\n');
state.owner.textContent = source;
state.source = source;
};
const makeRule = (state, parsed) => {
let cssText = parsed.cssText;
let parent = state.sheet;
let style;
const rule = {};
Object.defineProperties(rule, {
cssText: { enumerable: true, get: () => cssText },
parentStyleSheet: { enumerable: true, get: () => parent },
parentRule: { enumerable: true, value: null },
detach: { value: () => { parent = null; } }
});
if (parsed.selectorText !== undefined) {
Object.defineProperties(rule, {
type: { enumerable: true, value: 1 },
selectorText: { enumerable: true, value: parsed.selectorText },
style: { enumerable: true, get() {
if (style) return style;
// Reuse WebScene's real CSSStyleDeclaration parser/property methods.
// The scratch element remains detached; only the owning style element
// is published when callers mutate this declaration.
const declaration = state.owner.ownerDocument.createElement('span').style;
declaration.cssText = parsed.body;
const commit = () => {
synchronize(state);
cssText = parsed.selectorText + ' {' + declaration.cssText + '}';
if (parent && state.rules.includes(rule)) publish(state);
};
style = new Proxy(declaration, {
get(target, key) {
const value = Reflect.get(target, key, target);
if (typeof value !== 'function') return value;
return (...args) => {
const result = Reflect.apply(value, target, args);
if (key === 'setProperty' || key === 'removeProperty') commit();
return result;
};
},
set(target, key, value) {
const result = Reflect.set(target, key, value, target);
if (result) commit();
return result;
}
});
return style;
} }
});
}
return rule;
};
const synchronize = state => {
const source = state.owner.textContent || '';
if (source === state.source) return;
const parsed = splitRules(source);
for (const rule of state.rules) rule.detach();
state.rules = parsed.map(rule => makeRule(state, rule));
state.source = source;
};
const augment = sheet => {
if (!sheet || typeof sheet.insertRule === 'function') return sheet;
const state = { sheet, owner: sheet.ownerNode, source: undefined, rules: [] };
const list = new Proxy({}, {
get(_target, key) {
synchronize(state);
if (key === 'length') return state.rules.length;
if (key === 'item') return index => { synchronize(state); return state.rules[Number(index) >>> 0] || null; };
if (key === Symbol.iterator) return function* () {
for (let index = 0; index < state.rules.length; index++) { synchronize(state); yield state.rules[index]; }
};
if (typeof key === 'string' && /^(0|[1-9][0-9]*)$/.test(key)) return state.rules[Number(key)];
},
set() { return false; },
deleteProperty() { return false; },
defineProperty() { return false; }
});
sheets.set(sheet, state);
Object.defineProperties(sheet, {
cssRules: { configurable: true, enumerable: true, get() { stateFor(this); return list; } },
insertRule: { configurable: true, writable: true, value(rule, index = 0) {
if (arguments.length === 0) throw new TypeError('A CSS rule is required');
const current = stateFor(this);
index = Number(index) >>> 0;
if (index > current.rules.length) exception('Rule index is out of bounds', 'IndexSizeError');
const parsed = splitRules(text(rule));
if (parsed.length !== 1) exception('Exactly one CSS rule is required', 'SyntaxError');
// Imported sheets cannot be represented by the native owner-text bridge.
if (/^@(import|namespace)\b/i.test(parsed[0].cssText))
exception('Imported and namespace rules require native CSSOM support', 'NotSupportedError');
current.rules.splice(index, 0, makeRule(current, parsed[0]));
publish(current);
return index;
} },
deleteRule: { configurable: true, writable: true, value(index) {
if (arguments.length === 0) throw new TypeError('A rule index is required');
const current = stateFor(this);
index = Number(index) >>> 0;
if (index >= current.rules.length) exception('Rule index is out of bounds', 'IndexSizeError');
current.rules.splice(index, 1)[0].detach();
publish(current);
} }
});
return sheet;
};
Object.defineProperty(globalThis, '__webSceneAugmentStyleSheet', {
value: augment, configurable: true
});
})();
)JS";
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "webscene_embed_fallback.h"
#include "webscene_performance_timeline_compatibility.h"
#include "webscene_file_reader_compatibility.h"
#include "webscene_stylesheet_cssom_compatibility.h"

#include "webscene_native_dom.h"
#include "webscene_native_style_defaults.h"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@
const auto key = wrapper_key(node);
const auto known = stylesheet_wrappers.find(key);
if (known != stylesheet_wrappers.end() && !known->second.IsEmpty()) {
return known->second.Get(isolate);
return augment_style_sheet(known->second.Get(isolate));
}
const auto local_context = isolate->GetCurrentContext();
auto result = v8::Object::New(isolate);
Expand All @@ -353,7 +353,30 @@
js_string(isolate, "ownerNode"),
wrap_node(node)).Check();
stylesheet_wrappers.emplace(key, v8::Global<v8::Object>(isolate, result));
return result;
return augment_style_sheet(result);
}

v8::Local<v8::Object> augment_style_sheet(v8::Local<v8::Object> sheet)
{
const auto local_context = isolate->GetCurrentContext();
const auto global = local_context->Global();
v8::Local<v8::Value> raw_augment;
if (!global->Get(
local_context,
js_string(isolate, "__webSceneAugmentStyleSheet"))
.ToLocal(&raw_augment)
|| !raw_augment->IsFunction()) {
return sheet;
}
v8::Local<v8::Value> arguments[]{sheet};
v8::Local<v8::Value> augmented;
if (!raw_augment.As<v8::Function>()
->Call(local_context, global, 1, arguments)
.ToLocal(&augmented)
|| !augmented->IsObject()) {
return sheet;
}
return augmented.As<v8::Object>();
}

static void get_style_sheet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@
local_context,
"FileReader",
fileReaderCompatibilityScript);
install_web_api_compatibility_script(
local_context,
"stylesheet CSSOM",
cssCompatibilityScript);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,47 @@ void test_native_performance_timeline_identity(webscene_engine* engine)
+ result);
}

void test_native_mutable_stylesheet_cssom(webscene_engine* engine)
{
const auto result = evaluate(engine, R"JS((() => {
const exercise = (document, view, className, display) => {
const style = document.createElement('style');
const target = document.createElement('div');
target.className = className;
document.head.appendChild(style);
document.body.appendChild(target);
const sheet = style.sheet;
const rules = sheet.cssRules;
const index = sheet.insertRule(`.${className} { display: none; }`, 0);
const rule = rules[0];
const inserted = [index, rules.length, rule.selectorText,
view.getComputedStyle(target).display];
rule.style.setProperty('display', display);
const mutated = [rules.length, rule.style.display,
view.getComputedStyle(target).display];
sheet.deleteRule(0);
const deleted = [rules.length, rule.parentStyleSheet === null,
view.getComputedStyle(target).display];
return { inserted, mutated, deleted };
};
const frame = document.createElement('iframe');
document.body.appendChild(frame);
return {
main: exercise(document, window, 'monaco-main', 'inline-block'),
frame: exercise(
frame.contentDocument,
frame.contentWindow,
'monaco-frame',
'inline-flex')
};
})())JS", "native-mutable-stylesheet-cssom.js");
require(
result
== R"JSON({"main":{"inserted":[0,1,".monaco-main","none"],"mutated":[1,"inline-block","inline-block"],"deleted":[0,true,"block"]},"frame":{"inserted":[0,1,".monaco-frame","none"],"mutated":[1,"inline-flex","inline-flex"],"deleted":[0,true,"block"]}})JSON",
"mutable stylesheet CSSOM did not recascade main/frame styles: "
+ result);
}

void test_animation_frame_dispatch_is_attributed()
{
auto* engine = webscene_engine_create(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ int main()
test_native_websocket_browser_api();
return 0;
}
if (selected == "stylesheet-cssom") {
auto* focused_engine = webscene_engine_create(0);
require(focused_engine != nullptr,
"stylesheet CSSOM engine creation failed");
test_native_mutable_stylesheet_cssom(focused_engine);
webscene_engine_destroy(focused_engine);
return 0;
}
if (selected == "media-query-list") {
auto* focused_engine = webscene_engine_create(0);
require(focused_engine != nullptr, "focused engine creation failed");
Expand Down Expand Up @@ -871,6 +879,7 @@ int main()
test_document_links_is_a_live_named_html_collection(engine);
test_scrollspy_product_neutral_primitives(engine);
test_native_performance_timeline_identity(engine);
test_native_mutable_stylesheet_cssom(engine);
test_component_library_dom_discovery_primitives(engine);
test_document_id_index_preserves_tree_and_root_semantics(engine);
test_dom_selector_apis_throw_syntax_error_for_invalid_selectors(engine);
Expand Down
Loading
Loading