mirror of
https://codeberg.org/E955corp/atrender.com.git
synced 2026-09-16 20:58:08 +00:00
Init with initial deployment
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
|
||||
<meta http-equiv="302 Moved Temporarily"/>
|
||||
<meta http-equiv="Location" content="/"/>
|
||||
<link rel="canonical" href="https://atrender.com/1/"/>
|
||||
<meta http-equiv="refresh" content="0; url=/" type="text/html"/>
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,400 @@
|
||||
function main(event, page = event.target.body) {
|
||||
;;; check_for_json_file_name: ;;;
|
||||
|
||||
let name = page.ownerDocument.location.pathname;
|
||||
if (!name.endsWith(".json") && !name.endsWith(".json.html")) {
|
||||
logDiagnostic("not treated as JSON: " + name);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
;;; json_extraction_and_parsing: ;;;
|
||||
|
||||
// assert(page.ownerDocument.compatMode == "BackCompat")
|
||||
|
||||
let json = getJSONEncoding(page); // showing on old page
|
||||
page = fixQuirks(page.ownerDocument); // empty, new page
|
||||
|
||||
// assert(page.ownerDocument.compatMode == "CSS1Compat")
|
||||
|
||||
page.innerHTML = (`<${'pre'}>`);
|
||||
page.querySelector("pre").textContent = json;
|
||||
|
||||
let error = null;
|
||||
let processor = new WatchlistProcessor(page);
|
||||
try {
|
||||
var data = JSON.parse(json, $revive);
|
||||
} catch (ex) {
|
||||
logDiagnostic("file contains invalid JSON: " + name);
|
||||
error = ex;
|
||||
}
|
||||
|
||||
function $revive(key, value, ...rest) {
|
||||
try {
|
||||
processor.observe(key, value, ...rest);
|
||||
} catch (ex) {
|
||||
logDiagnostic("process failed during observation: " + name);
|
||||
error = error || ex; // keep the first error (if we saw one)
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
;;; finished_parsing: ;;;
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
||||
|
||||
;;; parse_succeeded: ;;;
|
||||
|
||||
processor.render(json, data, page.querySelector("pre").firstChild);
|
||||
}
|
||||
// <script>
|
||||
class WatchlistProcessor {
|
||||
constructor(page) {
|
||||
this.controls = new WatchlistDataControls(page);
|
||||
|
||||
this.currentSourceGroup = [];
|
||||
|
||||
this.sourceItems = [];
|
||||
|
||||
this.renderValue = null;
|
||||
}
|
||||
|
||||
observe(key, value, context) {
|
||||
// NB: This JSON processor is NOT written to handle pathological cases
|
||||
// (e.g. adversarial inputs); the "watchlib.js" in this demo is not a
|
||||
// general-purpose library fit for production use. It will work on the
|
||||
// input in this demo (and will work on some other inputs, too), but this
|
||||
// remains just a demo and will fall apart at the slightest perturbation.
|
||||
//
|
||||
// (To give one example: we assume that all keys are unescaped. And our
|
||||
// helpers don't fare well on object definitions with duplicate keys.)
|
||||
if ("source" in context) {
|
||||
let item = new WatchlistProcessor.SourceItem(context.source, key);
|
||||
this.sourceItems.push(item);
|
||||
if (key == "id" || key == "title") {
|
||||
this.currentSourceGroup.push(item);
|
||||
}
|
||||
} else if (!Array.isArray(value)) {
|
||||
if ("id" in value && "title" in value) {
|
||||
let count = this.currentSourceGroup.length;
|
||||
let items = this.currentSourceGroup.splice(0, count);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
items[i].object = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(unparsed, parsed, node) {
|
||||
// assert(node.nodeValue == unparsed)
|
||||
let doc = node.ownerDocument;
|
||||
|
||||
let caret = 0;
|
||||
let newNodes = doc.createDocumentFragment();
|
||||
for (let i = 0, n = this.sourceItems.length; i < n; ++i) {
|
||||
let item = this.sourceItems[i];
|
||||
// assert(item.source.length > 0)
|
||||
|
||||
item.offset = unparsed.indexOf(item.source, caret);
|
||||
// assert(item.offset >= 0 && item.offset < unparsed.length)
|
||||
|
||||
let span = doc.createElement("span");
|
||||
span.className = "source-item";
|
||||
span.append(doc.createTextNode(item.source));
|
||||
newNodes.append(
|
||||
doc.createTextNode(unparsed.substring(caret, item.offset)),
|
||||
span
|
||||
);
|
||||
|
||||
item.node = span;
|
||||
|
||||
if (item.key == "title") {
|
||||
span.classList.add("movie-title");
|
||||
let title = item.object["title"];
|
||||
if (item.source != JSON.stringify(title)) {
|
||||
span.setAttribute("title", title);
|
||||
span.classList.add("escaped-value");
|
||||
}
|
||||
} else if (item.key == "id") {
|
||||
span.classList.add("foreign-key");
|
||||
let id = item.object["id"];
|
||||
if (id.startsWith("[") && id.endsWith("]")) {
|
||||
let pair = id.substring(1, id.length - 1);
|
||||
let split = pair.split(":");
|
||||
if (split.length == 2) {
|
||||
switch (split[0]) {
|
||||
case "wikidata":
|
||||
this.controls.linkPropertyToURL(
|
||||
"title", item.object,
|
||||
"https://www.wikidata.org/wiki/Special:GoToLinkedPage?" +
|
||||
"site=enwiki&itemid=" + split[1]
|
||||
);
|
||||
this.controls.linkPropertyToURL(
|
||||
"id", item.object,
|
||||
"https://www.wikidata.org/wiki/Special:EntityPage/" +
|
||||
split[1],
|
||||
pair
|
||||
);
|
||||
break;
|
||||
case "tmdb":
|
||||
this.controls.linkPropertyToURL(
|
||||
"title", item.object,
|
||||
"https://themoviedb.org/" +
|
||||
split[1]
|
||||
);
|
||||
this.controls.linkPropertyToURL(
|
||||
"id", item.object,
|
||||
"https://themoviedb.org/" +
|
||||
split[1],
|
||||
pair
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (item.key == "@render") {
|
||||
span.classList.add("loader-string");
|
||||
// Fun fact: If you do it right, you can apply a similar trick to JS
|
||||
// files; the browser doesn't care about the content type (or the file
|
||||
// extension) of a script loaded with the src attribute, so you could
|
||||
// make polyglot JS+HTML files, too (and even use it as the loader...)
|
||||
}
|
||||
|
||||
caret = item.offset + item.source.length;
|
||||
}
|
||||
newNodes.append(doc.createTextNode(unparsed.substring(caret)));
|
||||
|
||||
node.parentElement.className = "json-source";
|
||||
WatchlistDataControls.applySkin(doc.head);
|
||||
|
||||
this.controls.insertLinks(this.sourceItems);
|
||||
|
||||
node.parentNode.replaceChild(newNodes, node);
|
||||
}
|
||||
}
|
||||
|
||||
// A "source" refers to the source code of a (primitive) value from a
|
||||
// key/value pair. We go ahead and keep track of the name of the key it's
|
||||
// defined for and the object that the key is defined on because this is
|
||||
// something we already have access to, and it's useful information.
|
||||
//
|
||||
// (These are NOT for complex/composite values--only primitives.)
|
||||
WatchlistProcessor.SourceItem = class {
|
||||
constructor(source, kind, object = null) {
|
||||
this.node = null;
|
||||
this.offset = -1;
|
||||
|
||||
this.source = source;
|
||||
this.key = kind;
|
||||
this.object = object;
|
||||
}
|
||||
}
|
||||
// <script>
|
||||
class WatchlistDataControls {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.links = [];
|
||||
}
|
||||
|
||||
static applySkin(parent) {
|
||||
let styleElement = parent.ownerDocument.createElement("style");
|
||||
parent.appendChild(styleElement);
|
||||
|
||||
let $ = styleElement.sheet.insertRule.bind(styleElement.sheet);
|
||||
$("body {" +
|
||||
"background-color: #FDFDFD;" +
|
||||
"}");
|
||||
|
||||
$(".json-source {" +
|
||||
"color: #222222;" +
|
||||
"}");
|
||||
|
||||
$(".source-item:not(.loader-string) {" +
|
||||
"color: #000000;" +
|
||||
"}");
|
||||
|
||||
$(".movie-title {" +
|
||||
"font-weight: bold;" +
|
||||
"}");
|
||||
|
||||
$(".movie-title a {" +
|
||||
"color: #090909;" +
|
||||
"}");
|
||||
|
||||
$(".foreign-key a {" +
|
||||
"color: #000000;" +
|
||||
"}");
|
||||
|
||||
$(".source-item a:not(:hover) {" +
|
||||
"text-decoration: none;" +
|
||||
"}");
|
||||
|
||||
$(".escaped-value {" +
|
||||
"text-decoration: dashed underline;" +
|
||||
"}");
|
||||
}
|
||||
|
||||
// Expects span to have exactly one text node as a child.
|
||||
linkContentsToURL(span, start, end, url) {
|
||||
span.firstChild.splitText(end);
|
||||
let content = span.firstChild.splitText(start);
|
||||
|
||||
if (!span.firstChild.textContent.length) {
|
||||
span.removeChild(span.firstChild);
|
||||
}
|
||||
if (!span.lastChild.textContent.length) {
|
||||
span.removeChild(span.lastChild);
|
||||
}
|
||||
|
||||
let link = span.ownerDocument.createElement("a");
|
||||
link.target = "_top";
|
||||
link.append(span.replaceChild(link, content));
|
||||
link.href = url;
|
||||
}
|
||||
|
||||
linkPropertyToURL(key, owner, url, text = null) {
|
||||
const { LinkTransform } = WatchlistDataControls;
|
||||
let transform = new LinkTransform(key, owner, url, text);
|
||||
this.links.push(transform);
|
||||
}
|
||||
|
||||
insertLinks(sourceCollection, linksCollection = this.links) {
|
||||
// We need to go from a given key & object definition to the source span
|
||||
// for the appropriate value in the key/value pair.
|
||||
//
|
||||
// Basic strategy: figure out which key/value pairs belong together (i.e.
|
||||
// they're properties defined on the the same object), and then when we
|
||||
// have a given transform, we go from transform to containing object to
|
||||
// property value to its corresponding span in the DOM.
|
||||
let propertyGroups = new Map();
|
||||
for (let i = 0, n = sourceCollection.length; i < n; ++i) {
|
||||
let obj = sourceCollection[i].object;
|
||||
if (!obj) continue;
|
||||
|
||||
if (!propertyGroups.has(obj)) {
|
||||
propertyGroups.set(obj, new Set());
|
||||
}
|
||||
let group = propertyGroups.get(obj);
|
||||
group.add(sourceCollection[i]);
|
||||
}
|
||||
|
||||
for (let i = 0, n = linksCollection.length; i < n; ++i) {
|
||||
let transform = linksCollection[i];
|
||||
if (transform.finished) continue;
|
||||
|
||||
let group = propertyGroups.get(transform.owner);
|
||||
if (!group) {
|
||||
logDiagnostic("property definitions unexpectedly unavailable");
|
||||
continue;
|
||||
}
|
||||
|
||||
let found = ([ ...group ]).filter((x) => (
|
||||
x.key == transform.key
|
||||
));
|
||||
|
||||
if (found.length != 1) {
|
||||
logDiagnostic(
|
||||
"couldn't find key to link property for url: " + transform.url
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let span = found[0].node;
|
||||
let start = 0;
|
||||
let end = span.textContent.length;
|
||||
|
||||
if (transform.text) {
|
||||
start = span.textContent.indexOf(transform.text);
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
} else {
|
||||
end = start + transform.text.length;
|
||||
}
|
||||
} else if (span.textContent.startsWith(`"`) &&
|
||||
span.textContent.endsWith(`"`)) {
|
||||
++start;
|
||||
--end;
|
||||
}
|
||||
this.linkContentsToURL(span, start, end, transform.url); // XXX
|
||||
|
||||
transform.finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WatchlistDataControls.LinkTransform = class {
|
||||
constructor(key, owner, url, text) {
|
||||
this.finished = false;
|
||||
this.key = key;
|
||||
this.owner = owner;
|
||||
this.url = url;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
// <script>
|
||||
function getJSONEncoding(page) {
|
||||
let elements = [ ...page.querySelectorAll("*") ];
|
||||
if (elements.length != 1) {
|
||||
return fail();
|
||||
}
|
||||
|
||||
let [ script ] = elements.filter((x) => (
|
||||
x.nodeName.toLowerCase() == "script" &&
|
||||
x.previousSibling.nodeType == page.TEXT_NODE &&
|
||||
x.previousSibling.nodeValue.trim().replace((/ /g), "").endsWith(
|
||||
`"@render":"`
|
||||
)
|
||||
));
|
||||
if (typeof(script) == "undefined") {
|
||||
return fail();
|
||||
}
|
||||
|
||||
let allKids = ([ ...page.childNodes ]);
|
||||
let holding = [];
|
||||
holding.push(...allKids.map((x) => {
|
||||
if (x.nodeType == page.TEXT_NODE) {
|
||||
return x.nodeValue.replace((/</g), "\\u003c");
|
||||
} else if (x == script) {
|
||||
return script.outerHTML.replace((/"/g), "'");
|
||||
}
|
||||
return fail();
|
||||
}));
|
||||
|
||||
return holding.join("");
|
||||
|
||||
function fail() {
|
||||
throw Error("payload is malformed; expecting loader script element");
|
||||
}
|
||||
}
|
||||
// <script>
|
||||
function fixQuirks(doc, title = "") {
|
||||
doc.open();
|
||||
doc.write(
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'><title>" + title
|
||||
);
|
||||
doc.close();
|
||||
return doc.body;
|
||||
}
|
||||
// <script>
|
||||
function logDiagnostic(...args) {
|
||||
if (typeof(__WATCHLIB_LOGGING__) != "undefined" &&
|
||||
typeof(console) != "undefined") {
|
||||
console.debug(...args);
|
||||
}
|
||||
}
|
||||
// <script>
|
||||
void function _start() {
|
||||
if (typeof(document) == "undefined") throw Error("panic!");
|
||||
if (typeof(window) == "undefined") throw Error("panic!");
|
||||
|
||||
if (document.readyState == "loading") {
|
||||
return void(window.onload = main);
|
||||
}
|
||||
return void(main(null, document.body));
|
||||
} ()
|
||||
// </script>
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"@render": "<script src='./watchlib.js'></script>",
|
||||
"name": "Alice's watchlist",
|
||||
"url": "https://alice.example.net/movies",
|
||||
"already_watched": [
|
||||
{
|
||||
"title": "The Iron Giant", "id": "[wikidata:Q867283]",
|
||||
"score": ":thumbsup:"
|
||||
},
|
||||
{
|
||||
"title": "Under the Skin", "id": "[wikidata:Q4366287]",
|
||||
"score": ":thumbsdown:"
|
||||
},
|
||||
{
|
||||
"title": "The Long Kiss Goodnight", "id": "[wikidata:Q1168399]",
|
||||
"score": ":thumbsup:"
|
||||
}
|
||||
],
|
||||
"up_next": [
|
||||
{"title": "In the Heat of the Night", "id":"[wikidata:Q622240]"},
|
||||
{"title": "Thelma \u0026 Louise", "id":"[wikidata:Q658041]"},
|
||||
{"title": "Patriot Games", "id":"[wikidata:Q855222]"},
|
||||
{"title": "Crash", "id":"[wikidata:Q188000]"},
|
||||
{"title": "The Pope of Greenwich Village", "id":"[wikidata:Q583221]"},
|
||||
{"title": "The Lost Bus", "id":"[wikidata:Q125153299]"},
|
||||
{"title": "Quisling: The Final Days", "id":"[wikidata:Q125417255]"},
|
||||
{"title": "The Railway Man", "id":"[wikidata:Q4178880]"},
|
||||
{"title": "Speed", "id":"[wikidata:Q108006]"},
|
||||
{"title": "Blue Velvet", "id":"[wikidata:Q660950]"},
|
||||
{"title": "Eyes Wide Shut", "id":"[wikidata:Q209481]"},
|
||||
{"title": "Casablanca", "id":"[wikidata:Q132689]"},
|
||||
{"title": "It Happened One Night", "id":"[wikidata:Q208632]"},
|
||||
{"title": "Incendies", "id":"[wikidata:Q1212650]"},
|
||||
{"title": "Roofman", "id":"[wikidata:Q130742503]"},
|
||||
{"title": "Point Break", "id":"[wikidata:Q1146552]"},
|
||||
{"title": "Dead End", "id":"[wikidata:Q676341]"},
|
||||
{"title": "Sophie's Choice", "id":"[wikidata:Q165627]"},
|
||||
{"title": "A Quiet Place Part II", "id":"[wikidata:Q53911403]"},
|
||||
{"title": "The Silence of the Lambs", "id":"[wikidata:Q133654]"},
|
||||
{"title": "Maria Bamford: Local Act", "id":"[tmdb:movie/1215158]"},
|
||||
{"title": "Mr. Jones", "id":"[wikidata:Q30963297]"},
|
||||
{"title": "First Cow", "id":"[wikidata:Q65082453]"},
|
||||
{"title": "Fried Green Tomatoes", "id":"[wikidata:Q118375]"},
|
||||
{"title": "Glass Onion: A Knives Out Mystery", "id":"[wikidata:Q84712797]"},
|
||||
{"title": "Wake Up Dead Man", "id":"[wikidata:Q115931717]"},
|
||||
{"title": "Dead Poets Society", "id":"[wikidata:Q106316]"},
|
||||
{"title": "The English Patient", "id":"[wikidata:Q63026]"},
|
||||
{"title": "Atonement", "id":"[wikidata:Q1626186]"}
|
||||
]
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>About @render for JSON</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<style>
|
||||
|
||||
body {
|
||||
max-width: 99ch;
|
||||
margin: auto;
|
||||
padding: 2em;
|
||||
line-height: 1.5em;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 2em 0 1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
line-height: 1.45;
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 24em;
|
||||
margin: 2em 0 1em;
|
||||
border: 4px solid #676767;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
border-top: 1px solid #AAAAAA;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<link rel="canonical" href="https://atrender.com/1/"/>
|
||||
|
||||
<meta name="generator" content="me">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
|
||||
<h1>About @render for JSON</h1>
|
||||
|
||||
<p>
|
||||
You can use @render in your JSON to provide rich controls to help with
|
||||
viewing, filtering, and editing what would otherwise be inert data.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The @render trick depends on well-defined behavior implemented by all
|
||||
browsers.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Read on to understand @render and how it works.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
|
||||
<h2 id="1-what-does-it-look-like">1. What does @render look
|
||||
like?</h2>
|
||||
|
||||
<p>In a text editor, it looks like an @render attribute annotating
|
||||
your JSON. This references your desired loader to bootstrap the UI builder
|
||||
(or "hydration" logic) for working with the JSON data. For example:
|
||||
|
||||
<pre>
|
||||
|
||||
{
|
||||
"@render": "<script src='https://cdn.example/watchlib.js'></script>",
|
||||
"name": "Alice's watchlist",
|
||||
"url": "https://alice.example.net/movies",
|
||||
"already_watched": [
|
||||
{
|
||||
"title": "The Iron Giant", "id": "[wikidata:Q867283]",
|
||||
"score": ":thumbsup:"
|
||||
},
|
||||
{
|
||||
"title": "Under the Skin", "id": "[wikidata:Q4366287]",
|
||||
"score": ":thumbsdown:"
|
||||
},
|
||||
/* ... */
|
||||
</pre>
|
||||
|
||||
<p>In a browser, it looks however you want it to look—subject to what your
|
||||
@render loader chooses to build and put on the screen.
|
||||
|
||||
<p>Here's a live example of Alice's watchlist data, showing how the
|
||||
post-render data looks in the browser:</p>
|
||||
|
||||
<iframe src="./demo/watchlist.json.html"></iframe>
|
||||
|
||||
<p>(You can also follow a <a href="./demo/watchlist.json.html">direct link to
|
||||
the JSON payload</a> to see it outside the iframe shown here.)</p>
|
||||
|
||||
|
||||
<h2 id="2-how-does-it-work">2. How does it work?</h2>
|
||||
|
||||
<p>To a JSON parser, @render is just an ordinary property whose value is a string
|
||||
(albeit one that we know happens to look like HTML).
|
||||
|
||||
<p>To an HTML parser, your JSON just looks like a bunch of text surrounding a
|
||||
bit of markup describing a lone script element near the beginning of the file
|
||||
(in the body rather than the head, but that's no big deal).
|
||||
|
||||
<p>The trick is to convince browsers to treat the JSON as HTML, whether by
|
||||
sending an HTML media type from the server, or by using an HTML file extension
|
||||
(instead of .json) for local files opened from your computer.
|
||||
|
||||
<p>In an HTML parsing context, the browser sees the markup for your loader
|
||||
script and then loads and executes the script source. The script, if
|
||||
self-aware enough, can read out the JSON data and then attach purpose-built,
|
||||
in-browser controls for viewing, editing, and otherwise working with the
|
||||
specific type of data contained within the JSON payload (or other data that it
|
||||
references, or anything else that you want to put on the screen).
|
||||
|
||||
|
||||
<h2 id="3-why-would-you-want-this">3. Why would you want this?</h2>
|
||||
|
||||
<p>JSON is an acceptable format for authoring and data exchange, but sometimes
|
||||
big blobs of JSON can get unwieldy. Not all user agents have perfect (or your
|
||||
preferred) JSON viewing tools, and even browsers that have tools for
|
||||
dealing with JSON can't have application-specific affordances for all the
|
||||
different shapes of JSON they might be absked to show.
|
||||
|
||||
<p>The @render trick lets you put your data on the screen in the way that you
|
||||
prefer to show it while remaining valid JSON.
|
||||
|
||||
<p>And of course, the latitude that this gives you means you can use @render
|
||||
to make the actual editing experience for JSON-based formats nicer, too.
|
||||
|
||||
|
||||
<h2 id="4-the-app-could-just-do-that-though">4. Shouldn't the apps we're
|
||||
building already provide those editing affordances?</h2>
|
||||
|
||||
<p>Maybe. Using @render can be useful for providing a lower-level interface to
|
||||
the data encoded in the file that ordinary users might not be interested in
|
||||
(or ever even see).
|
||||
|
||||
|
||||
<h2 id="5-what-else">5. What else?</h2>
|
||||
|
||||
<p>There are some caveats.</p>
|
||||
|
||||
<p>In addition to getting it to load in a parsing context that's expecting
|
||||
markup, you'll need to make sure to properly escape your JSON content for
|
||||
HTML. Doing this successfully isn't super-complicated. It's enough to escape
|
||||
all occurrences of <small>U+0026 AMPERSAND</small> and <small>U+003C LESS-THAN
|
||||
SIGN</small> using the Unicode escape sequence notation for string literals in
|
||||
JSON, i.e., <code>\u0026</code> and \<code>u003c,</code> respectively.</p>
|
||||
|
||||
<p>There are additional <a title="Authoring notes (using @render with JSON)"
|
||||
href="/notes/">authoring notes and tips relevant to @render</a> collected on
|
||||
a separate page.</p>
|
||||
|
||||
|
||||
</article>
|
||||
|
||||
<footer class="colophon">
|
||||
<dl>
|
||||
<dt>Document identifier</dt>
|
||||
<dd><a href="https://atrender.com/1/">https://atrender.com/1/</a></dd>
|
||||
<dt>Publication date</dt>
|
||||
<dd>2026 September 09</dd>
|
||||
<dt>Latest revision</dt>
|
||||
<dd><a href="https://atrender.com/latest/">https://atrender.com/latest/</a></dd>
|
||||
</dl>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
|
||||
<meta http-equiv="302 Moved Temporarily"/>
|
||||
<meta http-equiv="Location" content="/"/>
|
||||
<meta http-equiv="refresh" content="0; url=/" type="text/html"/>
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
|
||||
<meta http-equiv="302 Moved Temporarily"/>
|
||||
<meta http-equiv="Location" content="/notes/"/>
|
||||
<link rel="canonical" href="https://atrender.com/notes/1/"/>
|
||||
<meta http-equiv="refresh" content="0; url=/notes/" type="text/html"/>
|
||||
</head>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>Authoring notes (using @render with JSON)</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<style>
|
||||
|
||||
body {
|
||||
max-width: 99ch;
|
||||
margin: auto;
|
||||
padding: 2em;
|
||||
line-height: 1.5em;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 2em 0 1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
line-height: 1.45;
|
||||
background: #F4F4F4;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
border-top: 1px solid #AAAAAA;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<link rel="canonical" href="https://atrender.com/notes/1/"/>
|
||||
|
||||
<meta name="generator" content="me">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<article>
|
||||
|
||||
<h1>Authoring notes</h1>
|
||||
|
||||
<p>Is there anything else to be mindful of with the use of
|
||||
<span class="name">@render</span>? Yes. There
|
||||
are a few things—</p>
|
||||
|
||||
<p>To re-iterate the most important: forcing JSON into an HTML parsing context
|
||||
means that any text that's not escaped for HTML poses a risk to whether and
|
||||
how the loader does its job. In the worst cases, <strong>it could lead to
|
||||
cross-site scripting if you act without care. It's important to escape the
|
||||
data to prevent this</strong>. A good rule of thumb is to treat any open
|
||||
angle bracket in your payload (after the @render line, that is) as an
|
||||
oversight that needs immediate correction, regardless of the provenance of the
|
||||
data.</p>
|
||||
|
||||
<p>While consistently escaping <small>U+003C LESS-THAN-SIGN</small> (as
|
||||
<code>\u003c</code>) in all other parts of the JSON payload is enough to
|
||||
eliminate the cross-site scripting threat, for good measure, you may wish to
|
||||
always escape <small>U+0026 AMPERSAND</small> (as <code>\u0026</code>), too,
|
||||
since if the input goes through multiple rounds of parsing, then the sequence
|
||||
<code>&lt;script</code> can become <code><script</code> if you are
|
||||
inconsistent.</p>
|
||||
|
||||
<p>Secondly, if you don't have enough influence over the server configuration
|
||||
to control the Content-Type header (example: you have a static site), then
|
||||
you'll probably want to save your data with a .json.html file extension. This
|
||||
is also/already more or less required for any files that you intend to share
|
||||
that are "unhosted" and expected to be opened straight from the file
|
||||
system—it's what will get the correct double-click behavior in most system
|
||||
file managers and hint to the browser how you want it to be parsed.</p>
|
||||
|
||||
<p>Additionally, when your loader runs, be aware that browsers will by default
|
||||
put any UI elements that you add to the page into a document in quirks mode
|
||||
rather than standards mode. This can affect layout and cause
|
||||
hard-to-track-down issues. It's not impossible to address, but your
|
||||
application logic does have to be aware of it if you hope to be able get out
|
||||
of quirks mode.</p>
|
||||
|
||||
<p>Lastly, be aware that, although @render heretofore has had no particular
|
||||
significance in JSON, we do want to play well with the JSON-LD ecosystem.
|
||||
It's anticipated that it will be useful to define semantics for the fragment
|
||||
in the script element src attribute. (It may be beneficial for the fragment
|
||||
to serve as shorthand for a @type annotation, for example.) For that reason,
|
||||
use of the fragment identifier in the script element src attribute should be
|
||||
avoided for now until there has been an opportunity work out the exact
|
||||
semantics.</p>
|
||||
|
||||
<p>Remember that wherever feasible, you should probably try to use existing
|
||||
standards like JSON Schema and JSON-LD attributes to describe the data, but
|
||||
this is not necessary for @render to work.</p>
|
||||
|
||||
</article>
|
||||
|
||||
<footer class="colophon">
|
||||
<dl>
|
||||
<dt>Document identifier</dt>
|
||||
<dd><a href="https://atrender.com/notes/1/">https://atrender.com/notes/1/</a></dd>
|
||||
<dt>Publication date</dt>
|
||||
<dd>2026 September 09</dd>
|
||||
<dt>Latest revision</dt>
|
||||
<dd><a href="https://atrender.com/notes/">https://atrender.com/notes/</a></dd>
|
||||
</dl>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user