The data half of the extraction: 8 model directories, 13 utils, the shard
stream catalog and the 27-table schema fragment with its purge.
server/core.js is what makes the port a one-line import change per file rather
than a signature change per function. Ported code requires its dependencies at
file scope -- `const { query } = require('../../core')` -- which runs before
register() has been called and before any ctx exists. So every member is a
stable function that resolves ctx when CALLED, and nothing may be destructured
off ctx at init either, because core is free to hand over a getter.
Two helpers are vendored rather than taken from ctx, and the line between them
is the point. utils/excerpt.js is core's deriveExcerpt -- nine lines of pure
text handling. Core's sanitiser next to it was NOT copied: a second copy of a
security control diverges silently the moment either is fixed. announceLinks.js
vendors legError and articleUrl the same way, but baseUrl could not be: core's
reads APP_BASE_URL, and §2.7 forbids a module reading core's environment, so it
comes off ctx.site.baseUrl.
The schema fragment is core's 27 shard_*/uo_link_* statements, verbs CREATE,
ALTER and UPDATE only, every CREATE TABLE guarded. Two of its tables carry a
foreign key INTO users, which is allowed and is why the replay order matters --
core's schema is in place before this runs. The reverse never occurs and must
not: it would make core unable to boot without a module installed.
One real port bug caught by the integration run, not by tests: the atlas art
map resolved `../../../db/data`, which pointed at core's tree when this file
lived there and points outside server/ now. A path that happens to resolve is
exactly what survives a green suite, because the absent-file branch returns {}
and looks like the normal case.
Co-Authored-By: Claude <noreply@anthropic.com>
34 lines
1.5 KiB
JavaScript
34 lines
1.5 KiB
JavaScript
// A plain-text excerpt of a post body, for the town crier and the news gump.
|
|
//
|
|
// **Vendored from core's `utils/sanitizeHtml.js`, deliberately, and it is worth
|
|
// being precise about what was and was not copied.** Core's file exports three
|
|
// things: `cleanBody` (the actual HTML sanitiser, backed by a dependency and a
|
|
// tag allowlist), `OPTIONS`, and this. Only this one came, because only this one
|
|
// is a pure function over a string with no security surface — it strips tags to
|
|
// get at the text, it does not decide what tags are safe to render.
|
|
//
|
|
// Copying the sanitiser would have been the wrong call for exactly the reason
|
|
// this comment exists: a second copy of a security control diverges from the
|
|
// first the moment either is fixed, and the divergence is silent. A module that
|
|
// needs to sanitise HTML for rendering should ask core for it. This one does
|
|
// not — its output goes into a game window and a chat message as text.
|
|
|
|
/**
|
|
* Flatten HTML to a single line of text, truncated with an ellipsis.
|
|
*
|
|
* @param {string|null} html
|
|
* @param {number} max characters, including the ellipsis
|
|
* @returns {string|null} null when there is nothing left after stripping
|
|
*/
|
|
function deriveExcerpt(html, max = 280) {
|
|
if (html == null) return null
|
|
const text = String(html)
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
if (!text) return null
|
|
return text.length > max ? `${text.slice(0, max - 3)}...` : text
|
|
}
|
|
|
|
module.exports = { deriveExcerpt }
|