mirror of
https://codeberg.org/E955corp/atrender.com.git
synced 2026-09-16 20:58:08 +00:00
401 lines
11 KiB
JavaScript
401 lines
11 KiB
JavaScript
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>
|