From 537d7e2cefd28ec07e9bbafd0e9ecca57b69938d Mon Sep 17 00:00:00 2001 From: Perska Date: Sun, 22 Feb 2026 07:13:36 +0200 Subject: [PATCH 1/3] emote tag mvp --- markup.css | 36 ++++++++++++++++++++++++++++ parse.js | 6 ++++- render.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/markup.css b/markup.css index d3d3350..8d4b990 100644 --- a/markup.css +++ b/markup.css @@ -119,6 +119,42 @@ L + ratio { } height: auto; } +.Markup .M-emote { + display: inline-block; + object-fit: contain; + width: calc(var(--size)*var(--emote-size, 1.125em)); + height: calc(var(--size)*var(--emote-size, 1.125em)); + vertical-align: middle; + max-height: unset; + border: none; +} + +.M-filter-h { + transform: scaleX(-1); +} +.M-filter-v { + transform: scaleY(-1); +} +.M-filter-h.M-image-filter-v { + transform: scaleX(-1) scaleY(-1); +} +.M-filter-r { + transform: rotate(90deg); +} +.M-filter-h.M-filter-r { + transform: scaleX(-1) rotate(90deg); +} +.M-filter-v.M-filter-r { + transform: scaleY(-1) rotate(90deg); +} +.M-filter-h.M-filter-v.M-filter-r { + transform: rotate(270deg); +} +.M-filter-p { + image-rendering: pixelated; + image-rendering: crisp-edges; +} + /* ruby text doesn't work if set to white-space: pre */ .Markup rt { white-space: pre-line; diff --git a/parse.js b/parse.js index 53764f7..f6ad2ba 100644 --- a/parse.js +++ b/parse.js @@ -422,7 +422,7 @@ class Markup_12y2 { constructor() { read_args() if (token==='\\link') { read_body(false) - } else { + } else if (token!=='\\e') { // Emote should not have body read_body(true) if (NO_ARGS===rargs && false===body) { NEVERMIND() @@ -504,6 +504,10 @@ class Markup_12y2 { constructor() { let [lang=""] = rargs OPEN('language', {lang}) word_maybe() + } break; case '\\e': { + let [id="",name="",role="emote",source=""] = rargs.reverse() + OPEN('emote', {source, name, id, role}) + CLOSE() }} } break; case 'STYLE': { let c = check_style(token, text.charAt(match.index-1)||"\n", text.charAt(REGEX.lastIndex)||"\n") diff --git a/render.js b/render.js index 95fa7a9..b3a7d6e 100644 --- a/render.js +++ b/render.js @@ -29,6 +29,12 @@ class Markup_Render_Dom { constructor() { ERROR: (href, thing)=> "about:blank#"+href, } + let EMOTE_SOURCES = { + __proto__: null, + "url": (id, role) => id, + "discord": (id, role) => role == "sticker" ? `https://media.discordapp.net/stickers/${id}` : `https://cdn.discordapp.com/emojis/${id}` + } + function filter_url(url, thing) { try { let u = new URL(url, "no-scheme:/") @@ -112,6 +118,68 @@ class Markup_Render_Dom { constructor() { }) return e }, + + emote: function({source, name, id, role}) { + let url = "data:image/gif;base64,R0lGODlhAQABAIAAANDL5NDL5CH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" + if (id == "") id = name + if (role == "") role = "emote" + let options + [id, options=""] = id.split("#") + let pixel = options.indexOf("p") != -1 + if (id) { + if (source == "") { + url = pixel ? `sbs:image/${id}` : `sbs:image/${id}?size=128` + } else if (EMOTE_SOURCES[source]) { + url = EMOTE_SOURCES[source](id, role) + } + } + let src = filter_url(url, 'image') + let e = document.createElement('img') + e.classList.add('M-emote') + if (name!=null) + e.alt = e.title = name + e.tabIndex = 0 + const set_size = (state, width=e.naturalWidth, height=e.naturalHeight)=>{ + if (state=="size") { + e.width = width + e.height = height + } + e.style.setProperty('--width', width) + e.style.setProperty('--height', height) + e.dataset.state = state + } + let size = 2 + switch (role) { + case "icon": + size = 1 + break + case "emote": + size = 2 + break + case "medium": + size = 4 + break + case "sticker": + size = 8 + break + } + e.style.setProperty('--size', size) + set_size('size', size * 16, size * 16) + e.src = src + options.split("").forEach(x => e.classList.add(`M-filter-${x}`)) + // check whether the image is "available" (i.e. size is known) by looking at naturalHeight + // https://html.spec.whatwg.org/multipage/images.html#img-available + // this will happen here if the image is VERY cached, i guess + if (e.naturalHeight) + set_size('loaded-emote') + else // otherwise wait for load + e.decode().then(ok=>{ + set_size('loaded-emote') + }, no=>{ + e.dataset.state = 'error' + }) + return e + }, error: 𐀶`
🕯error🕯🕯message🕯
🕯stack🕯`,
 		
@@ -416,6 +484,7 @@ we should create our own fake bullet elements instead.*/
 		@member {Object}
 	**/
 	this.url_scheme = URL_SCHEME
+	this.emote_sources = EMOTE_SOURCES
 	this.filter_url = filter_url
 }}
 

From fcd27d88c3ccc9b813776e4ccaa594594e517782 Mon Sep 17 00:00:00 2001
From: Perska 
Date: Mon, 23 Feb 2026 05:49:05 +0200
Subject: [PATCH 2/3] change default sources and mark unknown source

discord split into discordemote and discordsticker
default source is now part of the EMOTE_SOURCES object
addfallback source that always returns null
---
 render.js | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/render.js b/render.js
index b3a7d6e..4ab5b12 100644
--- a/render.js
+++ b/render.js
@@ -31,8 +31,11 @@ class Markup_Render_Dom { constructor() {
 	
 	let EMOTE_SOURCES = {
 		__proto__: null,
-		"url": (id, role) => id,
-		"discord": (id, role) => role == "sticker" ? `https://media.discordapp.net/stickers/${id}` : `https://cdn.discordapp.com/emojis/${id}`
+		"": (id, options) => options.pixel ? `sbs:image/${id}` : `sbs:image/${id}?size=128`,
+		"url": (id, options) => id,
+		"discordemote": (id, options) => `https://cdn.discordapp.com/emojis/${id}`,
+		"discordsticker": (id, options) => `https://media.discordapp.net/stickers/${id}`,
+		UNKNOWN: (id, options) => null
 	}
 	
 	function filter_url(url, thing) {
@@ -120,22 +123,18 @@ class Markup_Render_Dom { constructor() {
 		},
 
 		emote: function({source, name, id, role}) {
-			let url = "data:image/gif;base64,R0lGODlhAQABAIAAANDL5NDL5CH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
 			if (id == "") id = name
 			if (role == "") role = "emote"
 			let options
 			[id, options=""] = id.split("#")
-			let pixel = options.indexOf("p") != -1
-			if (id) {
-				if (source == "") {
-					url = pixel ? `sbs:image/${id}` : `sbs:image/${id}?size=128`
-				} else if (EMOTE_SOURCES[source]) {
-					url = EMOTE_SOURCES[source](id, role)
-				}
-			}
-			let src = filter_url(url, 'image')
+			let url, unknown = false
+			if (id) 
+				url = (EMOTE_SOURCES[source] || EMOTE_SOURCES.UNKNOWN)(id, {pixel: options.indexOf("p") != -1})
+			let src = filter_url(url || "data:image/gif;base64,R0lGODlhAQABAIAAANDL5NDL5CH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", 'image')
 			let e = document.createElement('img')
 			e.classList.add('M-emote')
+			e.dataset.emotesource = source
+			if (url == null) e.dataset.emoteunknown = true
 			if (name!=null)
 				e.alt = e.title = name
 			e.tabIndex = 0

From d2202aa328aa75ddc62754dc6227d3617a8481b7 Mon Sep 17 00:00:00 2001
From: Perska 
Date: Tue, 24 Feb 2026 07:06:01 +0200
Subject: [PATCH 3/3] add extra filter args and make options cancel out

---
 render.js | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/render.js b/render.js
index 4ab5b12..6682928 100644
--- a/render.js
+++ b/render.js
@@ -122,14 +122,18 @@ class Markup_Render_Dom { constructor() {
 			return e
 		},
 
-		emote: function({source, name, id, role}) {
+		emote: function({source, name, id, role, filter}, filter2) {
 			if (id == "") id = name
 			if (role == "") role = "emote"
 			let options
 			[id, options=""] = id.split("#")
+			options+=(filter || "") + (filter2 || "")
+			let opt = {}
+			// Duplicate filters cancel out
+			options.split('').forEach(c=>opt[c] = !opt[c])
 			let url, unknown = false
 			if (id) 
-				url = (EMOTE_SOURCES[source] || EMOTE_SOURCES.UNKNOWN)(id, {pixel: options.indexOf("p") != -1})
+				url = (EMOTE_SOURCES[source] || EMOTE_SOURCES.UNKNOWN)(id, {pixel: opt["p"]})
 			let src = filter_url(url || "data:image/gif;base64,R0lGODlhAQABAIAAANDL5NDL5CH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", 'image')
 			let e = document.createElement('img')
 			e.classList.add('M-emote')
@@ -165,7 +169,7 @@ class Markup_Render_Dom { constructor() {
 			e.style.setProperty('--size', size)
 			set_size('size', size * 16, size * 16)
 			e.src = src
-			options.split("").forEach(x => e.classList.add(`M-filter-${x}`))
+			Object.keys(opt).forEach(x => x ? e.classList.add(`M-filter-${x}`) : undefined)
 			// check whether the image is "available" (i.e. size is known) by looking at naturalHeight
 			// https://html.spec.whatwg.org/multipage/images.html#img-available
 			// this will happen here if the image is VERY cached, i guess