From 889b8b561a418e6145dc7d6891a554e67d31301f Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:05:46 -0700 Subject: [PATCH 01/13] Update css_map.js --- src/utils/css_map.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/css_map.js b/src/utils/css_map.js index 4a5926e6f5..33be465b55 100644 --- a/src/utils/css_map.js +++ b/src/utils/css_map.js @@ -1,5 +1,9 @@ import { inject } from './inject.js'; +/** + * @see https://github.com/tumblr/docs/blob/master/web-platform.md#getcssmap + * @type {Record} + */ export const cssMap = await inject('/main_world/css_map.js'); /** From d767b77204e1d7c5a6b520c1feee0b1e628818b9 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:03:29 -0700 Subject: [PATCH 02/13] Update interface.js --- src/utils/interface.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/interface.js b/src/utils/interface.js index 2b90a429d5..d3d4a2a00a 100644 --- a/src/utils/interface.js +++ b/src/utils/interface.js @@ -48,6 +48,8 @@ export const getPopoverWrapper = element => { */ /** + * Filter an array of post elements (or descendants) to determine which to process. + * Usually used inside an onNewPosts listener callback function. * @param {Element[]} postElements Post elements (or descendants) to filter * @param {PostFilterOptions} [postFilterOptions] Post filter options * @returns {HTMLDivElement[]} Matching post elements @@ -181,6 +183,13 @@ const isVerticallyOverflowing = element => { return elementRect.bottom > document.documentElement.clientHeight || elementRect.top < 0; }; +/** + * Append a DOM element to a target container, attempting to position it so it + * is fully visible rather than overflowing into a hidden area. + * @param {Element} element Element to append + * @param {Element} target Target container + * @param {'below'|'above'} defaultPosition Above/below position to use if both are valid + */ export const appendWithoutOverflow = (element, target, defaultPosition = 'below') => { element.dataset.position = defaultPosition; element.style.removeProperty('--horizontal-offset'); @@ -205,5 +214,12 @@ export const appendWithoutOverflow = (element, target, defaultPosition = 'below' } }; +/** + * Navigate up the React component tree to find an element's closest rendered + * parent matching a given selector. Follows React "portals". + * @param {Element} element A target element, such as a portalled menu + * @param {string} selector CSS selector + * @returns {element?} An element matching the selector and "containing" the target element + */ export const getClosestRenderedElement = (element, selector) => inject('/main_world/closest_rendered_element.js', [selector], element); From 16d0313f84a4b8aee547525254f4518d651a8192 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:03:32 -0700 Subject: [PATCH 03/13] Update language_data.js --- src/utils/language_data.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utils/language_data.js b/src/utils/language_data.js index 7c76b49322..9d89b7cbed 100644 --- a/src/utils/language_data.js +++ b/src/utils/language_data.js @@ -1,5 +1,13 @@ import { inject } from './inject.js'; +/** + * @see https://github.com/tumblr/docs/blob/master/web-platform.md#languagedata + * @typedef {object} TumblrLanguageData + * @property {string} code Current Tumblr locale code + * @property {Record} translations Per docs, "a map of English root strings to the equivalents in the current language" + */ + +/** @type {TumblrLanguageData} */ export const languageData = await inject('/main_world/language_data.js'); /** From e397e65212dc38b640be0f557bde2f5a03200842 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:08 -0700 Subject: [PATCH 04/13] Update memoize.js research credit for jsdoc generic syntax: ai (gemma 4) --- src/utils/memoize.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/memoize.js b/src/utils/memoize.js index 8e12956c10..a0cd2cb39d 100644 --- a/src/utils/memoize.js +++ b/src/utils/memoize.js @@ -1,3 +1,10 @@ +/** + * Create a version of a function that returns results for repeated calls with + * the same argument from a cache, improving performance. + * @template T, R + * @param {function(T): R} func A function with one argument + * @returns {function(T): R} A memoized version of the function + */ export const memoize = (func) => { const cache = new Map(); return (arg) => { From 8f2ca5ac8ca7a3e28054ca399f546ad95455369d Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:19 -0700 Subject: [PATCH 05/13] Update modals.js research credit for jsdoc generic syntax: ai (gemma 4) --- src/utils/modals.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/utils/modals.js b/src/utils/modals.js index 6c5ffdc846..d8f1292e86 100644 --- a/src/utils/modals.js +++ b/src/utils/modals.js @@ -35,6 +35,9 @@ export const showModal = ({ title, message = [], buttons = [] }) => { modalElement.focus(); }; +/** + * Hide the current takeover prompt. + */ export const hideModal = () => { document.getElementById('xkit-modal')?.remove(); lastFocusedElement?.focus(); @@ -44,6 +47,10 @@ export const hideModal = () => { export const modalCancelButton = dom('button', null, { click: hideModal }, ['Cancel']); export const modalCompleteButton = dom('button', { class: 'blue' }, { click: hideModal }, ['OK']); +/** + * Show a takeover prompt to the user with details about a thrown exception. + * @param {Error} exception Thrown exception to describe + */ export const showErrorModal = exception => { console.error('XKit Rewritten error:', exception); @@ -63,6 +70,12 @@ export const showErrorModal = exception => { }); }; +/** + * Create a version of a function that shows a takeover prompt to the user if it throws. + * @template T, R + * @param {function(...T): R} func A function + * @returns {function(...T): R} A version of the function with explicit error handling + */ export const withModalOnError = func => async (...args) => { try { From 568a0cb0c3c9fbdeb7aab5ea397d22513179a7ed Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:22 -0700 Subject: [PATCH 06/13] Update mutations.js --- src/utils/mutations.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utils/mutations.js b/src/utils/mutations.js index 04a4cd26da..13286a4a2f 100644 --- a/src/utils/mutations.js +++ b/src/utils/mutations.js @@ -16,11 +16,15 @@ const isolateErrors = callback => { } }; +/** + * Utilities to run specified code when a modification to the page results in + * the addition of elements matching a specified CSS selector. + */ export const pageModifications = Object.freeze({ /** @type {Map<(elements: Element[]) => void, string>} */ listeners: new Map(), /** - * Register a page modification + * Register a page modification callback * @param {string} selector CSS selector for elements to target * @param {(elements: Element[]) => void} modifierFunction Function to handle matching elements */ @@ -32,7 +36,7 @@ export const pageModifications = Object.freeze({ }, /** - * Unregister a page modification + * Unregister a page modification callback * @param {(elements: Element[]) => void} modifierFunction Previously-registered function to remove */ unregister (modifierFunction) { @@ -40,7 +44,7 @@ export const pageModifications = Object.freeze({ }, /** - * Run a page modification on all existing matching elements + * Run a page modification callback on all existing matching elements * @param {(elements: Element[]) => void} modifierFunction Previously-registered function to run */ trigger (modifierFunction) { @@ -64,11 +68,17 @@ export const pageModifications = Object.freeze({ }, }); +/** + * Utilities to run specified code when new posts are added to the page. + */ export const onNewPosts = Object.freeze({ addListener: callback => pageModifications.register(`${postSelector}:not(.sortable-fallback) article`, callback), removeListener: callback => pageModifications.unregister(callback), }); +/** + * Utilities to run specified code when new notification elements are added to the page. + */ export const onNewNotifications = Object.freeze({ addListener: callback => pageModifications.register(notificationSelector, callback), removeListener: callback => pageModifications.unregister(callback), From ed3b6cc367dca1df28e29a1c992692667263dc8e Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:29 -0700 Subject: [PATCH 07/13] Update preferences.js --- src/utils/preferences.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/preferences.js b/src/utils/preferences.js index 705f40bef1..411359cee3 100644 --- a/src/utils/preferences.js +++ b/src/utils/preferences.js @@ -1,4 +1,6 @@ /** + * Get all of a feature's preference values that are automatically registered by + * the "preferences" field in its feature.json metadata file. * @param {string} featureName Internal name of feature * @returns {Promise} The feature's preference values */ From 713faeace5a1abb672c2856cef403c0c3d7af555 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:40 -0700 Subject: [PATCH 08/13] Update remixicon.js --- src/utils/remixicon.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/remixicon.js b/src/utils/remixicon.js index 4ae7998452..113df69b8e 100644 --- a/src/utils/remixicon.js +++ b/src/utils/remixicon.js @@ -13,6 +13,11 @@ if (document.querySelector(`svg[data-src="${symbolsUrl}"]`) === null) { }); } +/** + * @see https://remixicon.com/ + * @param {string} symbolId RemixIcon symbol id to use + * @returns {SVGElement} an SVG element that renders the specified icon + */ export const buildSvg = symbolId => dom('svg', { xmlns: 'http://www.w3.org/2000/svg' }, null, [ dom('use', { xmlns: 'http://www.w3.org/2000/svg', href: `#${symbolId}` }), ]); From aef19f72f5bda1d06cfdcc2ae978dc1ccd292a94 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:43 -0700 Subject: [PATCH 09/13] Update sidebar.js --- src/utils/sidebar.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/sidebar.js b/src/utils/sidebar.js index 955466110e..047d653ffa 100644 --- a/src/utils/sidebar.js +++ b/src/utils/sidebar.js @@ -46,6 +46,9 @@ const buildSidebarRow = ({ label, onclick, href, count, carrot }) => ]); /** + * Add an interactable sidebar item control that will be visible in the page's + * right sidebar in the desktop Tumblr layout, or in the left mobile drawer in + * the tablet/mobile Tumblr layouts. * @param {object} options Sidebar item options * @param {string} options.id Unique ID for the sidebar item * @param {string} options.title Human-readable sidebar item heading From 52e2beae22b338bf318580bde5e1a2b1be0e9e6c Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:49 -0700 Subject: [PATCH 10/13] Update text_format.js --- src/utils/text_format.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/text_format.js b/src/utils/text_format.js index 94c6dac4e7..0af092cbfd 100644 --- a/src/utils/text_format.js +++ b/src/utils/text_format.js @@ -9,6 +9,10 @@ const thresholds = [ ]; const relativeTimeFormat = new Intl.RelativeTimeFormat(document.documentElement.lang, { style: 'long' }); +/** + * @param {number} unixTime Second-resolution unix time value (as used in Tumblr post timestamps) + * @returns {string} e.g. "13 minutes ago" + */ export const constructRelativeTimeString = function (unixTime) { const now = Math.trunc(Date.now() / 1000); const unixDiff = unixTime - now; @@ -24,6 +28,9 @@ export const constructRelativeTimeString = function (unixTime) { return relativeTimeFormat.format(-0, 'second'); }; +/** + * Produces e.g. "July 11, 2020 at 9:58 PM EDT" + */ export const dateTimeFormat = new Intl.DateTimeFormat(document.documentElement.lang, { year: 'numeric', month: 'long', From 1422e2ddec383d225a012b0fa623d25a6207dbc6 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:52 -0700 Subject: [PATCH 11/13] Update timeline_id.js --- src/utils/timeline_id.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/timeline_id.js b/src/utils/timeline_id.js index 362f9b7846..1096426de3 100644 --- a/src/utils/timeline_id.js +++ b/src/utils/timeline_id.js @@ -1,3 +1,9 @@ +/** + * TimelineFilter functions exported from this module can be run on a timeline DOM + * element to identify its type from its data attributes. These attributes are not + * part of the official Tumblr API, which provides no clean way to do this. + */ + const createSelector = (...components) => `:is(${components.filter(Boolean).join(', ')})`; export const timelineSelector = ':is([data-timeline], [data-timeline-id])'; From d7cc31369a24c44b97ef998669d39607d61b322e Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:04:55 -0700 Subject: [PATCH 12/13] Update tumblr_helpers.js --- src/utils/tumblr_helpers.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/tumblr_helpers.js b/src/utils/tumblr_helpers.js index 15235576a3..b37304d1b6 100644 --- a/src/utils/tumblr_helpers.js +++ b/src/utils/tumblr_helpers.js @@ -68,8 +68,18 @@ export const isNpfCompatible = postData => { return isBlocksPostFormat || shouldOpenInLegacy === false; }; +/** + * Performs a "soft" navigation within Tumblr's single-page-application. + * @see https://github.com/tumblr/docs/blob/master/web-platform.md#navigate + * @param {string} location Path to navigate to + * @returns {void} + */ export const navigate = location => inject('/main_world/navigate.js', [location]); +/** + * A click event handler that can be applied to anchor elements to automate soft navigation. + * @param {PointerEvent} event Click event object + */ export const onClickNavigate = event => { if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) { event.stopImmediatePropagation(); From e906541bc1afc01658367b189c293e46b2405ff3 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 17 May 2026 17:07:49 -0700 Subject: [PATCH 13/13] Update react_props.js uhhhhh what exactly is this actually supposed to represent again? I forgor --- src/utils/react_props.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index f0f3d5fda3..280db05d5a 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -36,6 +36,10 @@ export const notificationObject = notificationElement => { */ export const blogData = async (meatballMenu) => inject('/main_world/unbury_blog.js', [], meatballMenu); +/** + * @param {Element} postElement An on-screen post element + * @returns {Promise} + */ export const isMyPost = async (postElement) => { const { blog, isSubmission, postAuthor, community } = await timelineObject(postElement); const userIsMember = userBlogNames.includes(blog.name);