diff --git a/package.json b/package.json index 05a85a9..eb85489 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "1.3.3", + "version": "1.3.5", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", diff --git a/src/backend/backend.js b/src/backend/backend.js index 335bd71..f1e9b50 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -186,9 +186,9 @@ function pickLeader(cands = []) { const safeArr = v => Array.isArray(v) ? v : []; const safeText = v => String(v || '').trim(); -const { safeReturnTo, safeRefererRedirect, pickMsgKeys, isMsgKey, publicModeGuard, isClearnetPath, isLocalPath } = require('./request_guards'); +const { safeReturnTo, safeRefererRedirect, pickMsgKeys, isMsgKey, publicModeGuard, isClearnetPath, isLocalPath, isImageSize, refererPath } = require('./request_guards'); -const { stripDangerousTags, sanitizeHtml } = require('./sanitizeHtml'); +const { stripDangerousTags } = require('./sanitizeHtml'); const longText = require('./long_text'); const sharedState = require('../configs/shared-state'); @@ -207,6 +207,7 @@ const sanitizeMessages = (msgs) => Array.isArray(msgs) ? msgs.map(sanitizeMsgTex const parseBool01 = v => String(Array.isArray(v) ? v[v.length - 1] : v || '') === '1'; const ERROR_PATTERNS = [ + [/received opinions/i, 'mediaLockedAfterOpinions'], [/invalid or expired invite code|invite already used|could not decrypt invite/i, 'inviteCodeInvalid'], [/already a (member|participant)/i, 'inviteAlreadyMember'], [/^forbidden\.?$/i, 'errorForbidden'], @@ -274,6 +275,59 @@ const sendErrorPage = (ctx, rawMessage, { title, status, keep, to, exact } = {}) ctx.body = errorView({ title, message, backHref }); }; +const PENDING_PICTURE_SIZE = 240; +const PENDING_FONT_SIZE = 16; +const PENDING_LINE_HEIGHT = 22; +const PENDING_LINE_CHARS = 22; +const xmlText = (s) => String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +const pendingPicture = (text) => { + const lines = []; + for (const word of String(text).split(/\s+/).filter(Boolean)) { + const last = lines[lines.length - 1]; + if (last && (last + ' ' + word).length <= PENDING_LINE_CHARS) lines[lines.length - 1] = last + ' ' + word; + else lines.push(word); + } + const mid = PENDING_PICTURE_SIZE / 2; + const top = mid - ((lines.length - 1) * PENDING_LINE_HEIGHT) / 2; + const spans = lines.map((line, i) => `${xmlText(line)}`).join(''); + return `${spans}`; +}; +const PENDING_MEDIA_RE = /<(video|audio)\b[^>]*?\ssrc="\/(?:c\/)?blob\/(%26[^"?#]+)[^"]*"[^>]*>[\s\S]*?<\/\1>/g; +const wantedMedia = new Set(); +const markPendingMedia = async (html) => { + const idOf = (enc) => { try { return decodeURIComponent(enc); } catch (_) { return ''; } }; + const ids = [...new Set([...html.matchAll(PENDING_MEDIA_RE)].map(m => idOf(m[2])).filter(id => isBlob(id)))]; + if (!ids.length) return html; + const ssb = await cooler.open(); + const missing = new Set(); + await Promise.all(ids.map(id => new Promise(resolve => ssb.blobs.has(id, (err, has) => { + if (!err && !has) { + missing.add(id); + if (!wantedMedia.has(id)) { wantedMedia.add(id); try { ssb.blobs.want(id, () => wantedMedia.delete(id)); } catch (_) { wantedMedia.delete(id); } } + } + resolve(); + })))); + if (!missing.size) return html; + const notice = `
${xmlText(require('../views/main_views').i18n.contentNotAvailableYet)}
`; + return html.replace(PENDING_MEDIA_RE, (whole, tag, enc) => missing.has(idOf(enc)) ? notice : whole); +}; +const sendPendingContent = (ctx) => { + const text = require('../views/main_views').i18n.contentNotAvailableYet; + const dest = String(ctx.get('sec-fetch-dest') || ''); + const accept = String(ctx.get('accept') || ''); + ctx.set('Cache-Control', 'no-store'); + if (dest === 'image' || (!dest && /^image\//.test(accept))) { + ctx.status = 200; + ctx.type = 'image/svg+xml'; + ctx.set('X-Content-Type-Options', 'nosniff'); + ctx.body = pendingPicture(text); + return; + } + if (dest === 'document' || (!dest && accept.includes('text/html'))) return sendErrorPage(ctx, text, { status: 404, exact: true }); + ctx.status = 404; + ctx.body = ''; +}; + const exceedsSsbLimit = (content, extraOverhead = 0) => { try { return Buffer.byteLength(JSON.stringify(content), 'utf8') + 512 + extraOverhead > 8192; } catch (_) { return false; } @@ -640,30 +694,76 @@ const clearnetMapPoint = async (mapUrl, author) => { if (!Number.isFinite(lat) || !Number.isFinite(lng) || (lat === 0 && lng === 0)) return null; return { lat: lat.toFixed(5), lng: lng.toFixed(5), title: String(map.title || '') }; }; +const COMMENT_INDEX_TTL_MS = 30000; +const COMMENT_INDEX_WAIT_MS = 2000; +const COMMENT_INDEX_SKIP_RE = /^\/(?:assets|blob|image|avatar|c|game-assets|games|maptiles|pdf|phone|rss)(?:\/|$)/; +const cachedIndex = (build) => { + let cache = { at: 0, data: null, pending: null, gen: 0 }; + const get = () => { + if (cache.data && Date.now() - cache.at < COMMENT_INDEX_TTL_MS) return Promise.resolve(cache.data); + if (cache.pending) return cache.pending; + const gen = cache.gen; + cache.pending = build() + .then((data) => { if (cache.gen === gen) { cache.data = data; cache.at = Date.now(); } return data; }) + .catch(() => cache.data || new Map()) + .finally(() => { if (cache.gen === gen) cache.pending = null; }); + return cache.pending; + }; + const forget = () => { cache = { at: 0, data: cache.data, pending: null, gen: cache.gen + 1 }; }; + return { get, forget }; +}; +const buildCommentIndex = async () => { + const ssbComments = await cooler.open(); + const { readTyped } = require('../models/typed_log'); + const { buildValidatedTombstoneSet } = require('../models/tombstone_validator'); + const posts = await readTyped(ssbComments, ['post', 'feed-action', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); + const gone = buildValidatedTombstoneSet(posts || []); + const byRoot = new Map(); + for (const m of posts || []) { + const c = m && m.value && m.value.content; + if (!c || c.type === 'tombstone' || gone.has(m.key)) continue; + if (c.type === 'feed-action' && c.action !== 'comment') continue; + const root = c.root || c.fork; + if (!root || !String(c.text || '').trim()) continue; + const ts = (m.value && m.value.timestamp) || 0; + const prev = byRoot.get(root); + if (!prev) byRoot.set(root, { count: 1, lastKey: m.key, lastTs: ts }); + else { + prev.count += 1; + if (ts >= prev.lastTs) { prev.lastKey = m.key; prev.lastTs = ts; } + } + } + return byRoot; +}; +const buildOpinionIndex = async () => { + const ssbOpinions = await cooler.open(); + const { readTyped } = require('../models/typed_log'); + const { buildValidatedTombstoneSet } = require('../models/tombstone_validator'); + const categories = require('./opinion_categories'); + const types = [...new Set(Object.values(categories.messageTypes))]; + const msgs = await readTyped(ssbOpinions, [...types, 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); + const gone = buildValidatedTombstoneSet(msgs || []); + const byTarget = new Map(); + for (const m of msgs || []) { + const c = m && m.value && m.value.content; + if (!c || !types.includes(c.type) || !c.target || gone.has(m.key)) continue; + let tally = byTarget.get(c.target); + if (!tally) byTarget.set(c.target, tally = { opinions: {}, opinions_inhabitants: [] }); + if (tally.opinions_inhabitants.includes(m.value.author)) continue; + tally.opinions_inhabitants.push(m.value.author); + if (categories.includes(c.category)) tally.opinions[c.category] = (tally.opinions[c.category] || 0) + 1; + } + return byTarget; +}; +const commentIndexCache = cachedIndex(buildCommentIndex); +const opinionIndexCache = cachedIndex(buildOpinionIndex); +const commentIndex = commentIndexCache.get; +const forgetEngagementIndexes = () => { commentIndexCache.forget(); opinionIndexCache.forget(); }; const attachCommentMeta = async (actions) => { const list = Array.isArray(actions) ? actions : []; if (!list.length) return list; try { - const ssbComments = await cooler.open(); - const { readTyped } = require('../models/typed_log'); - const { buildValidatedTombstoneSet } = require('../models/tombstone_validator'); - const posts = await readTyped(ssbComments, ['post', 'feed-action', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); - const gone = buildValidatedTombstoneSet(posts || []); - const byRoot = new Map(); - for (const m of posts || []) { - const c = m && m.value && m.value.content; - if (!c || c.type === 'tombstone' || gone.has(m.key)) continue; - if (c.type === 'feed-action' && c.action !== 'comment') continue; - const root = c.root || c.fork; - if (!root || !String(c.text || '').trim()) continue; - const ts = (m.value && m.value.timestamp) || 0; - const prev = byRoot.get(root); - if (!prev) byRoot.set(root, { count: 1, lastKey: m.key, lastTs: ts }); - else { - prev.count += 1; - if (ts >= prev.lastTs) { prev.lastKey = m.key; prev.lastTs = ts; } - } - } + const byRoot = await commentIndex(); for (const a of list) { if (!a) continue; const hit = byRoot.get(a.id) || byRoot.get(a.rootId) || byRoot.get(a.key); @@ -806,7 +906,7 @@ const clearnetRootOf = async (id) => { return cur; }; const clearnetBlockedContent = (c) => !c || typeof c !== 'object' || !!c.tribeId || c.encrypted === true || !!c.encryptedPayload || c.private === true || Array.isArray(c.recps) - || tribeCrypto.isTribeMsg(c) + || cryptoModel.isSealedMsg(c) || CLEARNET_CLOSED_VISIBILITY.has(String(c.visibility || '').toUpperCase()) || String(c.isPublic || '').toLowerCase() === 'private' || ((c.type === 'schoolCourse' || c.courseType !== undefined) && Number(c.price) > 0) @@ -940,7 +1040,7 @@ const markerMapId = async (markerId) => { const ssb = await cooler.open(); const msg = await new Promise((resolve) => ssb.get(markerId, (err, m) => resolve(err ? null : m))); let c = msg && msg.content; - if (c && c.encryptedPayload && tribeCrypto) c = await tribeCrypto.decryptFromTribe(c, tribesModel).catch(() => c); + if (c && c.encryptedPayload && cryptoModel) c = await cryptoModel.decryptFromGroup(c, tribesModel).catch(() => c); return c && typeof c.mapId === 'string' ? c.mapId : null; }; const syncTribeModuleExposure = async (ctx, next) => { @@ -1150,7 +1250,9 @@ const collectClearnetItems = async (feedId, prefs, { max = 5 } = {}) => { if (wants('shops')) { try { const shops = await shopsModel.listAll({ filter: 'all' }).catch(() => []); - items.shops = (shops || []).filter(s => s.author === feedId && String(s.visibility || '').toUpperCase() !== 'CLOSED').filter(on('shops')).slice(0, MAX_PER_SECTION).map(s => dated({ + const openShops = (shops || []).filter(s => s.author === feedId && String(s.visibility || '').toUpperCase() !== 'CLOSED').filter(on('shops')).slice(0, MAX_PER_SECTION); + for (const s of openShops) for (const product of await shopsModel.listProducts(s.rootId || s.key).catch(() => [])) notePublic(product); + items.shops = openShops.map(s => dated({ id: s.key, title: s.title || 'Untitled', image: s.image || null, @@ -1212,14 +1314,15 @@ const collectClearnetItems = async (feedId, prefs, { max = 5 } = {}) => { if (err || !Array.isArray(arr)) return resolve([]); resolve(arr.filter(m => { const ok = on('posts')({ key: m.key, ts: m.value && m.value.timestamp }); if (ok) notePublic(m.value.content); return ok; }).slice(0, MAX_PER_SECTION).map(m => { const c = m.value.content; - const cleanText = String(c.text || '').replace(/<[^>]+>/g, '').replace(/!\[[^\]]*\]\([^)]*\)/g, ''); + const cleanText = String(c.text || '').replace(/<[^>]+>/g, '').replace(/!?\[(?:pdf|audio|video|image|file|torrent):[^\]]*\]\([^)]*\)/g, '').replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1'); const firstLine = cleanText.split('\n').find(l => l.trim()) || ''; const postTs = tsOf(m.value.timestamp, c.createdAt); return dated({ id: m.key, title: c.contentWarning || firstLine.slice(0, 80) || 'Blog', image: null, - snippet: c.contentWarning ? firstLine.slice(0, 200) : cleanText.slice(0, 200) + snippet: c.contentWarning ? firstLine.slice(0, 200) : cleanText.slice(0, 200), + pdfs: c.contentWarning ? [] : [...String(c.text || '').matchAll(/\[pdf:([^\]]*)\]\(\s*(&[A-Za-z0-9+/=]+\.sha256)\s*\)/g)].map(m => ({ name: m[1], blob: m[2] })) }, postTs); })); }) @@ -1445,7 +1548,7 @@ const collectClearnetItems = async (feedId, prefs, { max = 5 } = {}) => { }; const QR_ACTION_BASE = 'http://localhost:3000'; const sendFeedQr = async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); const reqSize = parseInt(ctx.query.size, 10); const width = Number.isFinite(reqSize) ? Math.max(64, Math.min(512, reqSize)) : 240; try { @@ -1722,7 +1825,7 @@ const koaBody = (opts) => { return (ctx, next) => parse(ctx, () => { normalizeBodyNewlines(ctx.request.body); normalizeBodyTags(ctx.request.body); return next(); }); }; const longTextBody = koaBody({ formLimit: '2mb', textLimit: '2mb' }); -const { nav, ul, li, a, form, button, div, section, h2, p } = require("../server/node_modules/hyperaxe"); +const { ul, li, a, form, button, div, section, h2, p } = require("../server/node_modules/hyperaxe"); const open = require("../server/node_modules/open"); const pull = require("../server/node_modules/pull-stream"); const koaRouter = require("../server/node_modules/@koa/router"); @@ -1865,7 +1968,7 @@ const { about, blob, friend, meta, post, vote, spreads, lifetime } = models({ cooler, isPublic: config.public, }); -const { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, FileTooLargeError } = require('../backend/blobHandler.js'); +const { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, FileTooLargeError, WrongFileTypeError, stripFileMetadata } = require('../backend/blobHandler.js'); const { mergeGallery } = require('../models/media_gallery'); const extractBlobId = (md) => md ? (md.match(/\((\/blob\/sealed\/[^)\s]+|&[^)]+)\)/)?.[1] ?? null) : null; const exportmodeModel = require('../models/exportmode_model'); @@ -2234,31 +2337,31 @@ const suggestedPubs = async ({ probe = false } = {}) => { const entries = listed.filter((_, i) => answers[i]).map((x) => x.e); return pubInvites.readStatus(ssb, entries).catch(() => entries.map((e) => ({ ...e, invites: null, exhausted: false }))); }; -const tribeCrypto = require('../models/crypto')(ssbConfig.path, 'tribes'); +const cryptoModel = require('../models/crypto_model')(ssbConfig.path); const onboardingModel = require('../models/onboarding_model')({ cooler, ssbPath: ssbConfig.path }); -const chatCrypto = require('../models/crypto')(ssbConfig.path, 'chats'); -const schoolCrypto = require('../models/crypto')(ssbConfig.path, 'school'); -const padCrypto = require('../models/crypto')(ssbConfig.path, 'pads'); -const roomCrypto = require('../models/crypto')(ssbConfig.path, 'rooms'); -const mapCrypto = require('../models/crypto')(ssbConfig.path, 'maps'); -const calendarCrypto = require('../models/crypto')(ssbConfig.path, 'calendars'); -const eventCrypto = require('../models/crypto')(ssbConfig.path, 'events'); -const forumCrypto = require('../models/crypto')(ssbConfig.path, 'forum'); -const tribesModel = require('../models/tribes_model')({ cooler, isPublic: config.public, tribeCrypto }); -const eventsModel = require('../models/events_model')({ cooler, isPublic: config.public, tribeCrypto, eventCrypto, tribesModel }); -const larpModel = require('../models/larp_model')({ cooler, tribesModel, tribeCrypto }); -const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); +const chatCrypto = cryptoModel.scope('chats'); +const schoolCrypto = cryptoModel.scope('school'); +const padCrypto = cryptoModel.scope('pads'); +const roomCrypto = cryptoModel.scope('rooms'); +const mapCrypto = cryptoModel.scope('maps'); +const calendarCrypto = cryptoModel.scope('calendars'); +const eventCrypto = cryptoModel.scope('events'); +const forumCrypto = cryptoModel.scope('forum'); +const tribesModel = require('../models/tribes_model')({ cooler, isPublic: config.public, cryptoModel }); +const eventsModel = require('../models/events_model')({ cooler, isPublic: config.public, cryptoModel, eventCrypto, tribesModel }); +const larpModel = require('../models/larp_model')({ cooler, tribesModel, cryptoModel }); +const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public, cryptoModel, tribesModel }); const reportsModel = require('../models/reports_model')({ cooler, isPublic: config.public }); const transfersModel = require('../models/transfers_model')({ cooler, isPublic: config.public }); -const calendarsModel = require('../models/calendars_model')({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }); +const calendarsModel = require('../models/calendars_model')({ cooler, pmModel, cryptoModel, calendarCrypto, tribesModel }); const cvModel = require('../models/cv_model')({ cooler, isPublic: config.public }); const dataModel = require('../models/data_model')({ cooler, favoriteIdsFor: (kind) => contentFavorites.getFavoriteSet(kind) }); const inhabitantsModel = require('../models/inhabitants_model')({ cooler, isPublic: config.public, tribesModel, dataModel }); const feedModel = require('../models/feed_model')({ cooler, isPublic: config.public }); const imagesModel = require("../models/images_model")({ cooler, isPublic: config.public }); const audiosModel = require("../models/audios_model")({ cooler, isPublic: config.public }); -const torrentsModel = require("../models/torrents_model")({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); -const filesModel = require("../models/files_model")({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); +const torrentsModel = require("../models/torrents_model")({ cooler, isPublic: config.public, cryptoModel, tribesModel }); +const filesModel = require("../models/files_model")({ cooler, isPublic: config.public, cryptoModel, tribesModel }); const torrentDownloads = require("../models/torrent_downloads_model")({ cooler }); const seedSpreadContent = (ssb, message) => { if (config.public) return; @@ -2277,6 +2380,7 @@ const seedSpreadContent = (ssb, message) => { }; const encryptUploadForTribe = async (file, { filename, mime }) => { const safeName = String(filename || 'file').replace(/[\[\]\r\n]+/g, ' ').trim().slice(0, 120) || 'file'; + await stripFileMetadata(file.filepath, mime); const pointer = await fileshareModel.createShareFromFile({ filepath: file.filepath, filename: safeName, mime: mime || 'application/octet-stream' }); let ids = [pointer.manifestBlobId]; try { const manifest = await fileshareModel.openManifest(pointer); ids = ids.concat(manifest.chunks || []); } catch (_) {} @@ -2300,6 +2404,84 @@ const sealedPointerOf = (manifestBlobId, query) => { const name = String((query && query.n) || 'file').replace(/[\r\n"\\/]+/g, ' ').trim().slice(0, 120) || 'file'; return { type: 'fileShare', v: 1, key, manifestBlobId: String(manifestBlobId), filename: name, mime }; }; +const pdfView = require('./pdf_view'); +const PDF_BLOB_WAIT_MS = 8000; +const PDF_TITLE_MAX = 200; +const PDF_PLACEHOLDER = { w: 600, h: 800 }; +const pdfSourceOf = (src) => { + const s = String(src || '').slice(0, 4096); + const sealed = s.match(/^\/blob\/sealed\/([^/?#]+)\?([^#]*)$/); + if (sealed) { + if (config.public) return null; + let id; + try { id = decodeURIComponent(sealed[1]); } catch (_) { return null; } + const pointer = sealedPointerOf(id, Object.fromEntries(new URLSearchParams(sealed[2]))); + if (!pointer) return null; + return { src: s, key: pdfView.keyOf(s), load: async () => ((await fileshareModel.ensureAvailable(pointer).catch(() => false)) ? fileshareModel.reassembleToBuffer(pointer) : null) }; + } + const plain = s.match(/^\/blob\/([^/?#]+)$/); + if (!plain) return null; + let id; + try { id = decodeURIComponent(plain[1]); } catch (_) { return null; } + if (!/^&[A-Za-z0-9+/=]+\.sha256$/.test(id)) return null; + return { src: `/blob/${encodeURIComponent(id)}`, key: id, load: () => blob.getResolved({ blobId: id, timeout: PDF_BLOB_WAIT_MS }) }; +}; +const clearnetPdfSourceOf = async (src) => { + const m = String(src || '').slice(0, 4096).match(/^\/c\/blob\/([^/?#]+)$/); + if (!m) return null; + let id; + try { id = decodeURIComponent(m[1]); } catch (_) { return null; } + if (!/^&[A-Za-z0-9+/=]+\.sha256$/.test(id) || !(await clearnetBlobAllowed(id).catch(() => false))) return null; + return { src: `/c/blob/${encodeURIComponent(id)}`, key: id, load: () => blob.getLocal({ blobId: id }) }; +}; +const pdfTitleOf = (value) => String(value || '').replace(/[\r\n]+/g, ' ').trim().slice(0, PDF_TITLE_MAX); +const pdfBackOf = (ctx, prefix = '') => { + const usable = (v) => isLocalPath(v) && !v.startsWith(`${prefix}/pdf`) && (!prefix || v === prefix || v.startsWith(`${prefix}/`) || v.startsWith(`${prefix}?`)); + const given = String(ctx.query.back || ''); + if (usable(given)) return given; + const ref = refererPath(ctx.get('referer'), ctx.host); + return ref && usable(ref) ? ref : ''; +}; +const pdfViewState = async (ctx, resolve = pdfSourceOf) => { + const q = ctx.query || {}; + const source = await resolve(q.src); + const base = { src: source ? source.src : String(q.src || ''), fid: pdfView.isFrameId(q.fid) ? q.fid : '', zoom: pdfView.zoomOf(q.z), rotation: pdfView.rotationOf(q.r), full: q.full === '1' }; + if (!source) return { ...base, error: 'missing' }; + try { + const info = await pdfView.info(source.key, source.load); + const page = Math.min(Math.max(1, parseInt(q.p, 10) || 1), info.pages); + const width = pdfView.widthOf(base.zoom); + pdfView.page(source.key, source.load, page, width, base.rotation) + .then(() => (q.p && page < info.pages ? pdfView.page(source.key, source.load, page + 1, width, base.rotation) : null)) + .catch(() => {}); + return { ...base, pages: info.pages, page }; + } catch (e) { + return { ...base, error: e && e.message === 'UNAVAILABLE' ? 'unavailable' : 'unreadable' }; + } +}; +let pdfPlaceholderImage = null; +const pdfPlaceholder = async () => { + if (pdfPlaceholderImage) return pdfPlaceholderImage; + const sharpLib = getSharp(); + if (!sharpLib) return null; + pdfPlaceholderImage = await sharpLib({ create: { width: PDF_PLACEHOLDER.w, height: PDF_PLACEHOLDER.h, channels: 3, background: { r: 40, g: 40, b: 40 } } }).webp().toBuffer(); + return pdfPlaceholderImage; +}; +const sendPdfPage = async (ctx, source) => { + const page = Number(ctx.query.p); + const width = Number(ctx.query.w); + if (!source || !Number.isInteger(page) || page < 1 || !pdfView.isPdfWidth(width)) { ctx.status = 404; ctx.body = ''; return; } + try { + const out = await pdfView.page(source.key, source.load, page, width, ctx.query.r); + ctx.type = out.type; + ctx.set('Cache-Control', 'private, max-age=86400'); + ctx.body = out.body; + } catch (_) { + const placeholder = await pdfPlaceholder().catch(() => null); + ctx.set('Cache-Control', 'no-store'); + if (placeholder) { ctx.type = 'image/webp'; ctx.body = placeholder; } else { ctx.status = 404; ctx.body = ''; } + } +}; const materializeSealed = async (text) => { const src = String(text || ''); const refs = [...new Set(src.match(/\/blob\/sealed\/[^)\s"']+/g) || [])]; @@ -2349,6 +2531,66 @@ const createTorrentForFile = async (ctx, blobMarkdown, { title }) => { return generated ? { torrentUrl: generated.torrentBlobId } : null; } catch (_) { return null; } }; +const PODCAST_FEED_MAX = 300; +const PODCAST_ITUNES_CATEGORY = { NEWS: 'News', MUSIC: 'Music', TALK: 'Society & Culture', EDUCATION: 'Education', TECH: 'Technology', CULTURE: 'Arts', COMMUNITY: 'Society & Culture', OASIS: 'Technology' }; +const VERSIONS_MAX = 100; +const RATED_MEDIA_FIELDS = ['image', 'images', 'video', 'media', 'cover', 'blob']; +const changesRatedMedia = (ctx) => { + const files = ctx.request.files || {}; + const b = ctx.request.body || {}; + return RATED_MEDIA_FIELDS.some(f => [].concat(files[f] || []).some(x => x && Number(x.size) > 0)) + || (b.removePhoto !== undefined && b.removePhoto !== '') + || String(b.action || '') === 'removeVideo'; +}; +const versionChainOf = async (id) => { + const ssbV = await cooler.open(); + const get = (k) => new Promise(res => ssbV.get(k, (e, m) => res(e ? null : m))); + const out = []; + let cur = await get(id); + const author = cur && cur.author; + for (let key = id; cur && cur.author === author && !out.some(v => v.key === key) && out.length < VERSIONS_MAX; ) { + out.push({ key, content: cur.content }); + const prev = cur.content && typeof cur.content === 'object' ? cur.content.replaces : null; + if (typeof prev !== 'string') break; + key = prev; + cur = await get(prev); + } + return out; +}; +const isRatedMedia = async (id, item = null) => { + if (!isMsgKey(String(id || ''))) return false; + const rules = require('./opinion_categories'); + if (rules.hasOpinions(item)) return true; + const chain = await versionChainOf(id).catch(() => []); + return chain.some(v => rules.hasOpinions(v.content)) || rules.hasOpinions(await opinionsOf(chain.map(v => v.key))); +}; +const guardRatedMedia = async (ctx, id, item = null) => { + if (!changesRatedMedia(ctx) || !(await isRatedMedia(id, item))) return false; + failWith(ctx, 'mediaLockedAfterOpinions'); + return true; +}; +const CLEARNET_RSS_ITEMS = 50; +const sendClearnetRss = async (ctx, mods, label, query) => { + const { escapeHtml: esc } = require('../views/clearnet_view'); + const { clearnetSlugFor, i18n } = require('../views/main_views'); + const base = resolveExternalBaseUrl(ctx); + const index = await getClearnetIndex(); + const items = []; + for (const mod of mods) { + for (const entry of index) { + for (const it of (entry.items[mod.key] || [])) items.push({ ...it, mod, authorName: entry.name || '', feedId: entry.feedId }); + } + } + items.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); + const rssItems = items.slice(0, CLEARNET_RSS_ITEMS).map(it => { + const link = `${base}/c/${it.mod.modulePath || it.mod.key}/${encodeURIComponent(clearnetSlugFor(it.title, it.id))}`; + const date = new Date(Number(it.ts) || Date.now()).toUTCString(); + return ` \n ${esc(it.title || i18n.cnUntitled)}\n ${esc(link)}\n ${esc(String(it.id || link))}\n ${date}\n ${esc(it.authorName || it.feedId || '')}\n ${esc(String(it.snippet || '').slice(0, 500))}\n `; + }).join('\n'); + ctx.type = 'application/rss+xml'; + ctx.set('Cache-Control', 'public, max-age=600'); + ctx.body = `\n\n \n Oasis HUB ยท ${esc(label)}\n ${esc(`${base}/c${query}`)}\n ${esc(`${label} ${i18n.cnRssDescription}`)}\n${rssItems}\n \n\n`; +}; const clearnetPublicBase = () => { const u = String((require('../configs/config-manager').defaultInvite() || {}).url || '').replace(/\/+$/, ''); return /^https?:\/\//i.test(u) ? u : ''; @@ -2380,40 +2622,71 @@ const buildTorrentFromBlob = async (blobId, name, baseUrls = []) => { if (webseeds.length) metainfo['url-list'] = webseeds; const torrentBuf = bencode.encode(metainfo); const torrentBlobId = await new Promise((resolve, reject) => pull(pull.values([torrentBuf]), ssbClient.blobs.add((err, id) => err ? reject(err) : resolve(id)))); - return { markdown: `[torrent:${fileName}.torrent](${torrentBlobId})`, size: buffer.length, fileName, mime, torrentBlobId }; + return { markdown: `[torrent:${fileName}.torrent](${torrentBlobId})`, size: buffer.length, fileName, mime, torrentBlobId, infoHash: require('../models/torrents_model').parseTorrent(torrentBuf).infoHash }; +}; +const TORRENT_INFO_MAX = 4 * 1024 * 1024; +const TORRENT_INFO_CACHE_MAX = 200; +const torrentInfoCache = new Map(); +const torrentInfoOf = async (torrent) => { + if (!torrent || typeof torrent.url !== 'string' || !torrent.url.startsWith('&')) return null; + if (torrentInfoCache.has(torrent.url)) return torrentInfoCache.get(torrent.url); + try { + let buf = null; + if (torrent.cipher) { + const pointer = { type: 'fileShare', v: Number(torrent.cipher.v) || 1, key: torrent.cipher.key, manifestBlobId: torrent.cipher.manifestBlobId }; + if (!(await fileshareModel.isAvailable(pointer))) return null; + buf = await fileshareModel.reassembleToBuffer(pointer); + } else { + const ssbT = await cooler.open(); + if (!(await new Promise(res => ssbT.blobs.has(torrent.url, (e, h) => res(!e && !!h))))) return null; + buf = await new Promise((resolve, reject) => pull(ssbT.blobs.get(torrent.url), pull.collect((err, chunks) => err ? reject(err) : resolve(Buffer.concat((chunks || []).map(c => Buffer.isBuffer(c) ? c : Buffer.from(c))))))); + } + if (!buf || !buf.length || buf.length > TORRENT_INFO_MAX) return null; + const { parseTorrent, magnetFor } = require('../models/torrents_model'); + const parsed = parseTorrent(buf); + const info = { ...parsed, magnet: magnetFor(parsed) }; + if (torrentInfoCache.size >= TORRENT_INFO_CACHE_MAX) torrentInfoCache.delete(torrentInfoCache.keys().next().value); + torrentInfoCache.set(torrent.url, info); + return info; + } catch (_) { return null; } +}; +const readTorrentUpload = async (file) => { + try { return require('../models/torrents_model').parseTorrent(await fs.promises.readFile(file.filepath)); } + catch (_) { throw new WrongFileTypeError(file.originalFilename || 'file', 'application/x-bittorrent'); } }; const videosModel = require("../models/videos_model")({ cooler, isPublic: config.public }); const documentsModel = require("../models/documents_model")({ cooler, isPublic: config.public }); const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public }); -const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); -const padsModel = require('../models/pads_model')({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }); -const roomsModel = require('../models/rooms_model')({ cooler, tribeCrypto, roomCrypto, tribesModel }); -const wikiModel = require('../models/wiki_model')({ cooler, tribeCrypto, tribesModel }); +const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public, cryptoModel, tribesModel }); +const padsModel = require('../models/pads_model')({ cooler, cipherModel, cryptoModel, padCrypto, tribesModel }); +const roomsModel = require('../models/rooms_model')({ cooler, cryptoModel, roomCrypto, tribesModel }); +const wikiModel = require('../models/wiki_model')({ cooler, cryptoModel, tribesModel }); const emergenciesModel = require('../models/emergencies_model')({ cooler }); const logisticsModel = require('../models/logistics_model')({ cooler }); const podcastsModel = require('../models/podcasts_model')({ cooler }); const campaignsModel = require('../models/campaigns_model')({ cooler }); const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public, padsModel, tribesModel }); -const tribesContentModel = require('../models/tribes_content_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel }); -const searchModel = require('../models/search_model')({ cooler, isPublic: config.public, padsModel, tribeCrypto, tribesModel }); +const tribesContentModel = require('../models/tribes_content_model')({ cooler, isPublic: config.public, cryptoModel, tribesModel }); +const searchModel = require('../models/search_model')({ cooler, isPublic: config.public, padsModel, cryptoModel, tribesModel }); const industryModel = require("../models/industry_model")({ cooler, isPublic: config.public }); -const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel, padsModel, industryModel, larpModel }); +const activityModel = require('../models/activity_model')({ cooler, isPublic: config.public, cryptoModel, tribesModel, padsModel, industryModel, larpModel }); const pixeliaModel = require('../models/pixelia_model')({ cooler, isPublic: config.public }); const melodyModel = require('../models/melody_model')({ cooler }); -const marketModel = require('../models/market_model')({ cooler, isPublic: config.public, tribeCrypto }); -const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public, tribeCrypto, forumCrypto }); +const marketModel = require('../models/market_model')({ cooler, isPublic: config.public, cryptoModel }); +const forumModel = require('../models/forum_model')({ cooler, isPublic: config.public, cryptoModel, forumCrypto }); const blogModel = require('../models/blog_model')({ cooler, isPublic: config.public }); +const commentsModel = require('../models/comments_model')({ cooler, post }); const workflowsModel = require('../models/workflows_model'); const mentionsModel = require('../models/mentions_model')({ cooler }); -const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public, tribeCrypto }); -const housingModel = require('../models/housing_model')({ cooler, tribeCrypto }); -const shopsModel = require('../models/shops_model')({ cooler, isPublic: config.public, tribeCrypto }); -const chatsModel = require('../models/chats_model')({ cooler, tribeCrypto, chatCrypto, tribesModel }); -const pollsModel = require('../models/polls_model')({ cooler, isPublic: config.public, tribeCrypto, chatsModel }); +const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public, cryptoModel }); +const housingModel = require('../models/housing_model')({ cooler, cryptoModel }); +const shopsModel = require('../models/shops_model')({ cooler, isPublic: config.public, cryptoModel }); +const chatsModel = require('../models/chats_model')({ cooler, cryptoModel, chatCrypto, tribesModel }); +const pollsModel = require('../models/polls_model')({ cooler, isPublic: config.public, cryptoModel, chatsModel }); const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public }); const schoolModel = require('../models/school_model')({ cooler, transfersModel, schoolCrypto, chatsModel }); const agendaModel = require("../models/agenda_model")({ cooler, isPublic: config.public, calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel, industryModel, housingModel, schoolModel, campaignsModel, logisticsModel }); -const mapsModel = require("../models/maps_model")({ cooler, isPublic: config.public, tribeCrypto, mapCrypto, tribesModel }); +const mapsModel = require("../models/maps_model")({ cooler, isPublic: config.public, cryptoModel, mapCrypto, tribesModel }); const gamesModel = require('../models/games_model')({ cooler }); const notifyUbiPaid = async ({ to, amount, epochId, txid, transferKey }) => { const i18nAll = require('../client/assets/translations/i18n'); @@ -2916,7 +3189,7 @@ const enrichItemLifetime = async (item, opts = {}) => { } catch (_) {} return item; }; -const courtsModel = require('../models/courts_model')({ cooler, services: { votes: votesModel, inhabitants: inhabitantsModel, tribes: tribesModel, banking: bankingModel, parliament: parliamentModel }, tribeCrypto }); +const courtsModel = require('../models/courts_model')({ cooler, services: { votes: votesModel, inhabitants: inhabitantsModel, tribes: tribesModel, banking: bankingModel, parliament: parliamentModel }, cryptoModel }); tribesModel.processIncomingKeys().then(async () => { try { const viewerId = getViewerId(); @@ -2944,7 +3217,7 @@ const rawContentOf = async (msgId) => { }; const isTribeContent = async (msgId) => { const c = await rawContentOf(msgId); - return !!(c && (tribeCrypto.isTribeMsg(c) || (typeof c.tribeId === 'string' && c.tribeId))); + return !!(c && (cryptoModel.isSealedMsg(c) || (typeof c.tribeId === 'string' && c.tribeId))); }; const denyTribeContent = async (ctx, msgId) => { if (!(await isTribeContent(msgId))) return false; @@ -2955,7 +3228,7 @@ const denyTribeContent = async (ctx, msgId) => { const tribeOutsider = async (msgId) => { const c = await rawContentOf(msgId); if (!c) return false; - if (tribeCrypto.isTribeMsg(c)) return !tribeCrypto.unwrapMsg(c, tribeCrypto.buildFingerprintIndex()); + if (cryptoModel.isSealedMsg(c)) return !cryptoModel.unwrapMsg(c, cryptoModel.buildFingerprintIndex()); if (typeof c.tribeId === 'string' && c.tribeId) { const t = await tribesModel.getTribeById(c.tribeId).catch(() => null); return !(t && Array.isArray(t.members) && t.members.includes(getViewerId())); @@ -2975,28 +3248,93 @@ const pollTribeGate = async (pollId) => { const tribe = await tribesModel.getTribeById(poll.tribeId).catch(() => null); return { ok: !!(tribe && Array.isArray(tribe.members) && tribe.members.includes(getViewerId())), poll, tribe }; }; -const getVoteComments = async (voteId) => { - const raw = await post.topicComments(voteId); - return dropHidden((raw || []).filter(c => c?.value?.content?.type === 'post' && c.value.content.root === voteId) - .sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0))); +const commentsFor = async (voteId) => dropHidden(await commentsModel.listFor(voteId)); +const COMMENTABLE = { + wiki: (page) => !!page && !page.tribeId && !page.encrypted, + maps: (map) => CLEARNET_REACH_OK.maps(map), + school: (course) => !!course && !course.tribeId && course.visibility === 'PUBLIC', + industry: (bp) => !!bp && !bp.tribeId, + parliament: (type) => type === 'parliamentProposal' || type === 'parliamentLaw' }; -const commentCountsFor = async (ids) => { - const want = new Set(ids.filter(Boolean)); - const counts = new Map(); - if (!want.size) return counts; +const OPINABLE = { + wiki: COMMENTABLE.wiki, + maps: COMMENTABLE.maps, + industry: COMMENTABLE.industry, + jobs: (job) => !!job && !job.tribeId && String(job.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN' +}; +const opinionsOf = async (versionIds) => opinionsModel.tallyOf(versionIds).catch(() => ({ opinions: {}, opinions_inhabitants: [] })); +const opineOn = async (ctx, rootId, versionIds, back) => { + const category = String(ctx.params.category || ''); + if (!require('./opinion_categories').includes(category)) return actionFail(ctx); + if ((await opinionsOf(versionIds)).opinions_inhabitants.includes(getViewerId())) return failWith(ctx, 'opinionAlreadyGiven', back); + try { await opinionsModel.createVote(rootId, category); } + catch (e) { return /already/i.test(String(e && e.message)) ? failWith(ctx, 'opinionAlreadyGiven', back) : actionFail(ctx); } + try { activityModel.invalidateCache(); } catch (_) {} + ctx.redirect(safeReturnTo(ctx, back, [`/${back.split('/')[1]}`])); +}; +const commentsByRoot = async (ids) => { + const found = await commentsModel.listForMany(ids).catch(() => new Map()); + const out = new Map(); + for (const [root, list] of found) out.set(root, await dropHidden(list)); + return out; +}; +const commentCountsFor = async (ids) => commentsModel.countsFor(ids).catch(() => new Map()); +const inhabitantBankSummary = async (u) => { try { - const ssbC = await cooler.open(); - const { readTyped } = require('../models/typed_log'); - const { buildValidatedTombstoneSet } = require('../models/tombstone_validator'); - const msgs = await readTyped(ssbC, ['post', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); - const gone = buildValidatedTombstoneSet(msgs || []); - for (const m of msgs || []) { - const c = m && m.value && m.value.content; - if (!c || c.type !== 'post' || gone.has(m.key) || !want.has(c.root)) continue; - counts.set(c.root, (counts.get(c.root) || 0) + 1); - } - } catch (_) {} - return counts; + const bank = await bankingModel.getBankingData(u.id); + return { karmaScore: bank?.karmaScore || 0, estimatedUBI: bank?.estimatedUBI || 0, lastClaimedDate: bank?.lastClaimedDate || null, totalClaimed: bank?.totalClaimed || 0 }; + } catch { + return { karmaScore: 0, estimatedUBI: 0, lastClaimedDate: null, totalClaimed: 0 }; + } +}; +const inhabitantActivityBuckets = async (users) => new Map(await Promise.all(users.map(async (u) => { + if (u.lastActivityBucket) return [u.id, u.lastActivityBucket]; + try { + const ts = await inhabitantsModel.getLastActivityTimestampByUserId(u.id); + return [u.id, inhabitantsModel.bucketLastActivity(ts || null).bucket]; + } catch { + return [u.id, 'red']; + } +}))); +const inhabitantCardDetails = async (users) => { + const addrMap = new Map((await bankingModel.listAddressesMerged()).map(x => [x.id, x.address])); + const profiles = config.public ? null : await about.profiles().catch(() => null); + return new Map(await Promise.all(users.map(async (u) => { + const pr = profiles ? (profiles.get(u.id) || {}) : null; + const prefsNow = pr ? about.visibilityFrom(pr.visibilityPrefs) : await about.visibilityPrefs(u.id).catch(() => null); + const published = Number(u.karmaScore) > 0; + const [kd, prefs, rel, fp, src, carbon, kr] = await Promise.all([ + (prefsNow && prefsNow.ubi === true) || !published ? inhabitantBankSummary(u) : { karmaScore: Number(u.karmaScore), estimatedUBI: 0, lastClaimedDate: null, totalClaimed: 0 }, + prefsNow, + friend.getRelationship(u.id).catch(() => null), + pr ? (typeof pr.gpgFingerprint === 'string' ? pr.gpgFingerprint : '') : about.gpgFingerprint(u.id).catch(() => ''), + pr ? (typeof pr.deviceSource === 'string' && pr.deviceSource.trim() ? pr.deviceSource : null) : about.deviceSource(u.id).catch(() => null), + getCarbonGramsForFeed(u.id).catch(() => 0), + bankingModel.getKarmaReceived(u.id).catch(() => null) + ]); + return [u.id, { + ecoAddress: addrMap.get(u.id) || null, + karmaReceived: kr, + karmaScore: kd.karmaScore ?? (typeof u.karmaScore === 'number' ? u.karmaScore : 0), + estimatedUBI: kd.estimatedUBI || 0, + lastClaimedDate: kd.lastClaimedDate || null, + totalClaimed: kd.totalClaimed || 0, + visibilityPrefs: prefs || null, + relationship: rel || null, + gpgFingerprint: fp || '', + deviceSource: src || null, + carbonGrams: carbon || 0 + }]; + }))); +}; +const commentLocationHref = async (id) => { + const at = await commentsModel.locate(id).catch(() => null); + if (!at || !at.rootType) return null; + if (at.rootType === 'forum') return `/forum/${encodeURIComponent(at.rootId)}${at.commentId ? `#${require('../views/main_views').forumReplyAnchor(at.commentId)}` : ''}`; + const base = require('../views/activity_view').getViewDetailsAction(at.rootType, { id: at.rootId, author: at.author, content: at.content || {} }); + if (!base || base === '/activity') return null; + const open = base.includes('#') ? base : `${base}${base.includes('?') ? '&' : '?'}comments=open`; + return at.commentId && !open.includes('#') ? `${open}#${encodeURIComponent(at.commentId)}` : open; }; const enrichWithComments = async (items, idKey = 'id') => { const idOf = (x) => x[idKey] || x.key || x.rootId; @@ -3037,6 +3375,7 @@ const tribeUpkeep = async (tribeId) => { }; const resizedImages = new Map(); let resizedImagesBytes = 0; +const THUMB_QUALITY = 80; const RESIZED_IMAGES_MAX_BYTES = 32 * 1024 * 1024; const pageOf = (ctx, list) => slicePage(Array.isArray(list) ? list : [], new URLSearchParams(ctx.querystring).get('page'), listPerPage(ctx.querystring)).items; const renderedPage = async (render) => { @@ -3092,7 +3431,7 @@ const contentResolvers = { market: async id => id, school: async id => schoolModel.resolveRootId(id).catch(() => id) }; -const contentModCheck = { images: 'imagesMod', audios: 'audiosMod', videos: 'videosMod', documents: 'documentsMod', bookmarks: 'bookmarksMod', market: 'marketMod', jobs: 'jobsMod', projects: 'projectsMod', shops: 'shopsMod', shopProducts: 'shopsMod', chats: 'chatsMod', maps: 'mapsMod', pads: 'padsMod', rooms: 'roomsMod', calendars: 'calendarsMod', wiki: 'wikiMod', emergencies: 'emergenciesMod', mailing: 'mailingMod', logistics: 'logisticsMod', podcasts: 'podcastsMod', campaigns: 'campaignsMod', torrents: 'torrentsMod', files: 'filesMod', events: 'eventsMod', forum: 'forumMod', blogs: 'blogsMod', logs: 'logsMod', polls: 'pollsMod', tasks: 'tasksMod', reports: 'reportsMod', votes: 'votesMod', housing: 'housingMod', transfers: 'transfersMod', school: 'schoolMod' }; +const contentModCheck = { images: 'imagesMod', audios: 'audiosMod', videos: 'videosMod', documents: 'docsMod', bookmarks: 'bookmarksMod', market: 'marketMod', jobs: 'jobsMod', projects: 'projectsMod', shops: 'shopsMod', shopProducts: 'shopsMod', chats: 'chatsMod', maps: 'mapsMod', pads: 'padsMod', rooms: 'roomsMod', calendars: 'calendarsMod', wiki: 'wikiMod', emergencies: 'emergenciesMod', mailing: 'mailingMod', logistics: 'logisticsMod', podcasts: 'podcastsMod', campaigns: 'campaignsMod', torrents: 'torrentsMod', files: 'filesMod', events: 'eventsMod', forum: 'forumMod', blogs: 'blogsMod', logs: 'logsMod', polls: 'pollsMod', tasks: 'tasksMod', reports: 'reportsMod', votes: 'votesMod', housing: 'housingMod', transfers: 'transfersMod', school: 'schoolMod', industry: 'industryMod', parliament: 'parliamentMod' }; const favAction = async (ctx, kind, action) => { if (!checkMod(ctx, contentModCheck[kind])) { ctx.redirect('/modules'); return; } if (action === 'add' && await denyTribeOutsider(ctx, ctx.params.id)) return; @@ -3531,7 +3870,7 @@ const commentAction = async (ctx, kind, idParam) => { const rt = safeReturnTo(ctx, `/${kind}/${encodeURIComponent(itemId)}`, [`/${kind}`]); const blobMarkdown = await handleBlobUpload(ctx, 'blob'); if (blobMarkdown) text += blobMarkdown; - if (isBlankText(text)) { ctx.redirect(rt); return; } + if (isBlankText(text)) { failWith(ctx, 'publishEmpty', rt); return; } const keepComment = (e) => keepText(ctx, e, { title: require('../views/main_views').i18n.voteNewCommentLabel, action: ctx.path, @@ -3541,7 +3880,7 @@ const commentAction = async (ctx, kind, idParam) => { backHref: rt }); if (longText.tooLong(text)) { keepComment(new Error('Text too long')); return; } - try { await post.publish({ text, root: itemId, dest: itemId }); } + try { await commentsModel.publish({ rootId: itemId, text }); } catch (e) { keepComment(e); return; } ctx.redirect(rt); }; @@ -3568,7 +3907,7 @@ const formLicense = (b, { keep = false } = {}) => (keep && (!b || b.license === const mediaCreateAction = async (ctx, kind) => { const modKey = contentModCheck[kind]; if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } - const blob = await handleBlobUpload(ctx, kind.slice(0, -1)); + const blob = await handleBlobUpload(ctx, kind.slice(0, -1), null, kind.slice(0, -1)); const { tags, title, description, mapUrl } = ctx.request.body; const created = await mediaCreateModels[kind][`create${kind.charAt(0).toUpperCase()}${kind.slice(1, -1)}`](blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), stripDangerousTags(mapUrl || ""), formLicense(ctx.request.body)); await applyClearnetChoice(ctx, kind, created); @@ -3579,7 +3918,7 @@ const mediaUpdateAction = async (ctx, kind) => { if (modKey && !checkMod(ctx, modKey)) { ctx.redirect('/modules'); return; } const { tags, title, description, mapUrl } = ctx.request.body; const singular = kind.slice(0, -1); - const blob = ctx.request.files?.[singular] ? await handleBlobUpload(ctx, singular) : null; + const blob = ctx.request.files?.[singular] ? await handleBlobUpload(ctx, singular, null, singular) : null; await mediaCreateModels[kind][`update${kind.charAt(0).toUpperCase()}${kind.slice(1, -1)}ById`](ctx.params.id, blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), stripDangerousTags(mapUrl || ""), formLicense(ctx.request.body, { keep: true })); await applyClearnetChoice(ctx, kind, ctx.params.id); ctx.redirect(safeReturnTo(ctx, `/${kind}?filter=mine`, [`/${kind}`])); @@ -3605,12 +3944,15 @@ const dedupeLarpHouseTribes = (list) => { }; const nameWarmup = about._startNameWarmup(); const FEED_RE = /^@.+\.ed25519$/; +const nameCache = require('./nameCache'); +const AVATAR_SIZE = 64; +const AVATAR_CACHE_SECONDS = 600; const AUTHOR_FIELDS = ['author', 'from', 'to', 'organizer', 'proposer', 'seller', 'recipient', 'judgeId', 'accuser', 'respondentId', 'createdBy']; const warmNames = async (ids) => { const uniq = [...new Set((ids || []).filter(v => v && FEED_RE.test(String(v))).map(String))]; if (!uniq.length) return; if (uniq.length > 10 && !config.public && await about.profiles().then(() => true).catch(() => false)) return; - await Promise.all(uniq.slice(0, 600).map(fid => about.name(fid).catch(() => {}))); + await Promise.all(uniq.slice(0, 600).map(fid => Promise.all([about.name(fid).catch(() => {}), about.image(fid).catch(() => {})]))); }; const collectAuthorIds = (items) => { const out = []; @@ -3971,7 +4313,7 @@ async function renderBlobMarkdown(text, mentions = {}, myFeedId, myUsername) { .replace(/\[pdf:([^\]]*)\]\(([^)]+)\)/g, (_, name, id) => { const { i18n } = require("../views/main_views"); const label = name || (i18n && i18n.pdfFallbackLabel) || 'PDF'; - return `${escHtml(label)}`; + return `${escHtml(label)}`; }); return text; } @@ -4394,10 +4736,11 @@ const koaBodyFileshare = koaBody({ }, parsedMethods: ['POST'], }); +const paramId = (value) => { const v = String(value || ''); if (!v.startsWith('%25')) return v; try { return decodeURIComponent(v); } catch (_) { return v; } }; const resolveCommentComponents = async function (ctx) { let parentId; try { - parentId = decodeURIComponent(ctx.params.message); + parentId = paramId(ctx.params.message); } catch { parentId = ctx.params.message; } @@ -4426,7 +4769,7 @@ const resolveCommentComponents = async function (ctx) { } return { messages, myFeedId, parentMessage, contentWarning }; }; -const { clearnetHubView, authorView, previewCommentView, commentView, editProfileView, likesView, supportersView, threadView, privateView, previewSubtopicView, subtopicView, imageSearchView, setLanguage, tribeAccessDeniedView, inviteRequiredView, clearnetInhabitantView, clearnetBlogView, listPerPage, slicePage } = require("../views/main_views"); +const { clearnetHubView, authorView, previewCommentView, commentView, editProfileView, likesView, supportersView, threadView, privateView, previewSubtopicView, subtopicView, setLanguage, tribeAccessDeniedView, clearnetInhabitantView, clearnetBlogView, listPerPage, slicePage } = require("../views/main_views"); const { activityView } = require("../views/activity_view"); const { cvView, createCVView } = require("../views/cv_view"); const { indexingView } = require("../views/indexing_view"); @@ -4439,11 +4782,11 @@ const { tribesView, tribeView, renderInvitePage } = require("../views/tribes_vie const { agendaView } = require("../views/agenda_view"); const { documentView, singleDocumentView } = require("../views/document_view"); const { inhabitantsView, inhabitantsProfileView } = require("../views/inhabitants_view"); -const { walletViewRender, walletView, walletHistoryView, walletReceiveView, walletSendFormView, walletSendConfirmView, walletSendResultView, walletErrorView } = require("../views/wallet_view"); +const { walletView, walletHistoryView, walletReceiveView, walletSendFormView, walletSendConfirmView, walletSendResultView, walletErrorView } = require("../views/wallet_view"); const { pmView } = require("../views/pm_view"); const { tagsView } = require("../views/tags_view"); const { videoView, singleVideoView } = require("../views/video_view"); -const { audioView, singleAudioView, audiosTranscodeView, audioTranscodeDetailView } = require("../views/audio_view"); +const { audioView, singleAudioView, audioTranscodeDetailView } = require("../views/audio_view"); const { torrentsView, singleTorrentView } = require("../views/torrents_view"); const { filesView, singleFileView } = require("../views/files_view"); const { eventView, singleEventView, clearnetEventView, renderEventInvitePage } = require("../views/event_view"); @@ -4477,7 +4820,7 @@ const { dataView } = require("../views/data_view"); const { pollsView, singlePollView } = require("../views/polls_view"); const { mentionsView } = require("../views/mentions_view"); const { renderBlockchainView, renderSingleBlockView } = require("../views/blockchain_view"); -const { jobsView, singleJobsView, renderJobForm, clearnetJobView } = require("../views/jobs_view"); +const { jobsView, singleJobsView, clearnetJobView } = require("../views/jobs_view"); const { housingView, singleHousingView, clearnetHousingView } = require("../views/housing_view"); const { shopsView, singleShopView, singleProductView, editProductView, shopOrdersView, myPurchasesView, clearnetShopView, renderShopInvitePage } = require("../views/shops_view"); const { chatsView, singleChatView, renderChatInvitePage } = require("../views/chats_view"); @@ -4527,6 +4870,7 @@ const publishErrorText = (e) => { const t = require('../views/main_views').i18n; const msg = String((e && e.message) || e || ''); if (isSsbTooLargeError(e) || /too long|does not fit/i.test(msg)) return t.publishTooLong; + if (/required/i.test(msg)) return t.publishEmpty; return msg ? `${t.actionFailed} (${msg.slice(0, 200)})` : t.actionFailed; }; const keepText = (ctx, error, form) => { @@ -4538,7 +4882,7 @@ const forumPageBody = async (ctx, forumId, replyId = null, params = {}) => { const spreadInfo = await spreads.forMessage(forumId).catch(() => null); const forumObj = await forumModel.getForumById(forumId); try { const oi = await forumModel.getOpenInvite(forumId).catch(() => null); if (oi && forumObj) forumObj.openInviteCode = oi.code; } catch (_) {} - const forumMsgs = await forumModel.getMessagesByForumId(forumId); + const forumMsgs = await forumModel.getMessagesByForumId(forumId, await commentsModel.listFor(forumId).catch(() => [])); const forumHidden = await hiddenView(); if (forumMsgs && Array.isArray(forumMsgs.messages) && (forumHidden.authors.size || forumHidden.ids.size)) { const isHiddenReply = (r) => !!(r && ((r.author && forumHidden.authors.has(r.author)) || (r.key && forumHidden.ids.has(r.key)))); @@ -4591,15 +4935,7 @@ const isBlockRestricted = (block, effPrivateChainIds) => { router .param("imageSize", (imageSize, ctx, next) => { - const size = Number(imageSize); - const isInteger = size % 1 === 0; - const overMinSize = size > 2; - const underMaxSize = size <= 256; - ctx.assert( - isInteger && overMinSize && underMaxSize, - 400, - "Invalid image size" - ); + ctx.assert(isImageSize(imageSize), 400, "Invalid image size"); return next(); }) .param("blobId", (blobId, ctx, next) => { @@ -4716,7 +5052,7 @@ router if (filter === 'create') { ctx.body = await schoolView([], 'create', null, { q, sort, reach: String(ctx.query.courseType || '') }); return; } if (filter === 'edit') { const course = await schoolModel.getCourseById(String(ctx.query.courseId || ''), getViewerId()); - if (course) course.clearnet = await clearnetPublic('school', course).catch(() => false); + if (course) Object.assign(course, { clearnet: await clearnetPublic('school', course).catch(() => false), mediaLocked: await isRatedMedia(course.id) }); ctx.body = await schoolView([], 'edit', course, { q, sort, reach: String(ctx.query.courseType || '') }); return; } @@ -4748,7 +5084,7 @@ router const progress = course.author === getViewerId() ? await schoolModel.progressForCourse(course.rootId).catch(() => ({})) : {}; const approved = course.students.includes(getViewerId()) ? await schoolModel.hasPassedCourse(course.rootId, getViewerId()).catch(() => false) : false; if (course) course.clearnet = await clearnetPublic('school', course).catch(() => false); - ctx.body = await singleCourseView({ ...course, isFavorite: fav.has(String(course.rootId || course.id)) }, lessons, certificates, { exams, progress, approved, certStudent: String(ctx.query.cert || ''), subscription: await subscriptionStateFor(course.rootId || course.id, course.author), spreads: await spreads.forMessage(course.id).catch(() => null), modesAvail: await schoolModesAvailFor() }); + ctx.body = await singleCourseView({ ...course, isFavorite: fav.has(String(course.rootId || course.id)) }, lessons, certificates, { exams, progress, approved, comments: COMMENTABLE.school(course) ? await commentsFor(course.rootId || course.id) : null, certStudent: String(ctx.query.cert || ''), subscription: await subscriptionStateFor(course.rootId || course.id, course.author), spreads: await spreads.forMessage(course.id).catch(() => null), modesAvail: await schoolModesAvailFor() }); }) .post('/school/create', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'schoolMod')) { ctx.redirect('/modules'); return; } @@ -4764,6 +5100,7 @@ router }) .post('/school/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'schoolMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; if (clearnetConflict(ctx.request.body)) { const existing = await schoolModel.getCourseById(ctx.params.id, getViewerId()).catch(() => null); if (!existing) { ctx.redirect('/school'); return; } @@ -5001,12 +5338,12 @@ router ctx.redirect(href); }) .get('/qr-action/follow/:feedId', async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); if (!ssbRef.isFeedId(feedId)) { ctx.status = 400; ctx.body = 'Invalid feed id'; return; } confirmPage(ctx, { message: require('../views/main_views').i18n.confirmFollowText, action: `/qr-action/follow/${encodeURIComponent(feedId)}`, backHref: `/author/${encodeURIComponent(feedId)}` }); }) .post('/qr-action/follow/:feedId', koaBody(), async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); if (!ssbRef.isFeedId(feedId)) { ctx.status = 400; ctx.body = 'Invalid feed id'; return; } try { await friend.follow(feedId); } catch (_) {} ctx.redirect(`/author/${encodeURIComponent(feedId)}`); @@ -5186,9 +5523,9 @@ router const effPrivateChainIds = await buildEffectivePrivateChainIds(); restricted = isBlockRestricted(block, effPrivateChainIds); const c = block.content || {}; - if (!restricted && tribeCrypto && (c.encryptedPayload || tribeCrypto.isTribeMsg(c))) { + if (!restricted && cryptoModel && (c.encryptedPayload || cryptoModel.isSealedMsg(c))) { try { - const decrypted = await tribeCrypto.decryptFromTribe(c, tribesModel); + const decrypted = await cryptoModel.decryptFromGroup(c, tribesModel); if (decrypted && !decrypted._undecryptable) { block = { ...block, content: decrypted }; } @@ -5198,7 +5535,7 @@ router ctx.body = renderSingleBlockView(block, filter, userId, search, viewMode, restricted); }) .get('/author/:feed', async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feed || ''), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1); + const feedId = paramId(ctx.params.feed), gt = Number(ctx.request.query.gt || -1), lt = Number(ctx.request.query.lt || -1); if (lt > 0 && gt > 0 && gt >= lt) throw new Error('Given search range is empty'); const visibilityPrefs = await about.visibilityPrefs(feedId).catch(() => null); const deviceSource = await about.deviceSource(feedId).catch(() => null); @@ -5325,6 +5662,12 @@ router if (Number.isFinite(toTs)) scoped = scoped.filter(m => (m.value?.timestamp || m.timestamp || 0) <= toTs); if (scoped.length > 0) finalResults[type] = scoped; } + if (Array.isArray(finalResults.post)) { + const comments = finalResults.post.filter(m => commentsModel.isComment(m.value?.content)); + const blogs = finalResults.post.filter(m => !commentsModel.isComment(m.value?.content)); + if (blogs.length) finalResults.post = blogs; else delete finalResults.post; + if (comments.length) finalResults.comment = comments; + } ctx.body = await searchView({ results: finalResults, query, types, ...(await searchExtras(finalResults, ctx)) }); }) .get("/images", async (ctx) => { @@ -5356,7 +5699,7 @@ router const { imageId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const img = await imagesModel.getImageById(imageId, getViewerId()); const fav = await contentFavorites.getFavoriteSet('images'); - const comments = await getVoteComments(img.key); + const comments = await commentsFor(img.key); const imgAuthorPrefs = await about.visibilityPrefs(img.author).catch(() => null); await enrichItemLifetime(img, { key: img.key }); const singleFav = await contentFavorites.getFavoriteSet('images'); @@ -5393,7 +5736,7 @@ router const fav = await contentFavorites.getFavoriteSet('maps'); mapItem.clearnet = await clearnetPublic('maps', mapItem).catch(() => false); const { lat, lng, zoom, title, description, markerLabel, tags, mapType, clearnet, pick, view, place } = ctx.query; - ctx.body = await mapsView([{ ...mapItem, isFavorite: fav.has(String(mapItem.rootId || mapItem.key)) }], 'edit', mapItem.key, { returnTo: ctx.query.returnTo || '', lat, lng, zoom, pick, view, place, title, description, markerLabel, tags, clearnet, reach: String(mapType || '') }); + ctx.body = await mapsView([{ ...mapItem, mediaLocked: await isRatedMedia(mapItem.key, mapItem), isFavorite: fav.has(String(mapItem.rootId || mapItem.key)) }], 'edit', mapItem.key, { returnTo: ctx.query.returnTo || '', lat, lng, zoom, pick, view, place, title, description, markerLabel, tags, clearnet, reach: String(mapType || '') }); }) .get("/maps/:mapId", async (ctx) => { if (!checkMod(ctx, 'mapsMod')) { ctx.redirect('/modules'); return; } @@ -5427,7 +5770,9 @@ router ctx.body = tribeAccessDeniedView(parentTribe); return; } mapItem.clearnet = await clearnetPublic('maps', mapItem).catch(() => false); - ctx.body = await singleMapView({ ...mapItem, isFavorite: fav.has(String(mapItem.rootId || mapItem.key)) }, filter, { q, zoom, clat, clng, mkLat, mkLng, mkMarkerLabel, pick, view, place, focus, tribeMembers, spreads: await spreads.forMessage(mapItem.key).catch(() => null), returnTo: safeReturnTo(ctx, `/maps?filter=${encodeURIComponent(filter)}`, ['/maps']) }); + const mapComments = COMMENTABLE.maps(mapItem) ? await commentsFor(mapItem.rootId || mapItem.key) : null; + const mapOpinions = OPINABLE.maps(mapItem) ? await opinionsOf([mapItem.rootId, mapItem.key]) : null; + ctx.body = await singleMapView({ ...mapItem, isFavorite: fav.has(String(mapItem.rootId || mapItem.key)) }, filter, { comments: mapComments, opinions: mapOpinions, q, zoom, clat, clng, mkLat, mkLng, mkMarkerLabel, pick, view, place, focus, tribeMembers, spreads: await spreads.forMessage(mapItem.key).catch(() => null), returnTo: safeReturnTo(ctx, `/maps?filter=${encodeURIComponent(filter)}`, ['/maps']) }); }) .get("/audios", async (ctx) => { if (!checkMod(ctx, 'audiosMod')) { ctx.redirect('/modules'); return; } @@ -5458,7 +5803,7 @@ router const { audioId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const audio = await audiosModel.getAudioById(audioId, getViewerId()); const fav = await contentFavorites.getFavoriteSet('audios'); - const comments = await getVoteComments(audio.key); + const comments = await commentsFor(audio.key); const audioAuthorPrefs = await about.visibilityPrefs(audio.author).catch(() => null); await enrichItemLifetime(audio, { key: audio.key }); const singleFav = await contentFavorites.getFavoriteSet('audios'); @@ -5584,7 +5929,7 @@ router if (await tribeOutsider(fileId)) { ctx.redirect('/tribes'); return; } const file = await filesModel.getFileById(fileId, getViewerId()); const fav = await contentFavorites.getFavoriteSet('files'); - const comments = await getVoteComments(file.key); + const comments = await commentsFor(file.key); const fileAuthorPrefs = await about.visibilityPrefs(file.author).catch(() => null); await enrichItemLifetime(file, { key: file.key }); const singleCensus = (await censusOf(`files:${String(ctx.query.sort || 'recent')}`, () => filesModel.listAll({ filter: 'all', q: '', sort: String(ctx.query.sort || 'recent'), viewerId: getViewerId() })).catch(() => [])).map(x2 => ({ ...x2, isFavorite: fav.has(String(x2.rootId || x2.key)) })); @@ -5597,13 +5942,14 @@ router if (await tribeOutsider(torrentId)) { ctx.redirect('/tribes'); return; } const torrent = await torrentsModel.getTorrentById(torrentId, getViewerId()); const fav = await contentFavorites.getFavoriteSet('torrents'); - const comments = await getVoteComments(torrent.key); + const comments = await commentsFor(torrent.key); const torrentAuthorPrefs = await about.visibilityPrefs(torrent.author).catch(() => null); await enrichItemLifetime(torrent, { key: torrent.key }); const singleFav = await contentFavorites.getFavoriteSet('torrents'); const singleCensus = (await censusOf(`torrents:${String(ctx.query.sort || 'recent')}`, () => torrentsModel.listAll({ filter: 'all', q: '', sort: String(ctx.query.sort || 'recent'), viewerId: getViewerId() })).catch(() => [])).map(x2 => ({ ...x2, isFavorite: (singleFav).has(String(x2.rootId || x2.key)) })); if (torrent) torrent.clearnet = await clearnetPublic('torrents', torrent).catch(() => false); - ctx.body = await singleTorrentView({ ...torrent, isFavorite: fav.has(String(torrent.rootId || torrent.key)), commentCount: comments.length }, filter, comments, { censusList: singleCensus, q, sort, returnTo: safeReturnTo(ctx, `/torrents?filter=${encodeURIComponent(filter)}`, ['/torrents']), spreads: await spreads.forMessage(torrent.key), authorPrefs: torrentAuthorPrefs , download: torrent.source ? torrentDownloads.get(torrent.source) : null, hasLocal: torrent.source ? await torrentDownloads.hasLocal(torrent.source) : false, isPublic: !!config.public }); + const torrentInfo = await torrentInfoOf(torrent); + ctx.body = await singleTorrentView({ ...torrent, isFavorite: fav.has(String(torrent.rootId || torrent.key)), commentCount: comments.length }, filter, comments, { torrentInfo, censusList: singleCensus, q, sort, returnTo: safeReturnTo(ctx, `/torrents?filter=${encodeURIComponent(filter)}`, ['/torrents']), spreads: await spreads.forMessage(torrent.key), authorPrefs: torrentAuthorPrefs , download: torrent.source ? torrentDownloads.get(torrent.source) : null, hasLocal: torrent.source ? await torrentDownloads.hasLocal(torrent.source) : false, isPublic: !!config.public }); }) .get("/videos", async (ctx) => { if (!checkMod(ctx, 'videosMod')) { ctx.redirect('/modules'); return; } @@ -5634,7 +5980,7 @@ router const { videoId } = ctx.params; const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const video = await videosModel.getVideoById(videoId, getViewerId()); const fav = await contentFavorites.getFavoriteSet('videos'); - const comments = await getVoteComments(video.key); + const comments = await commentsFor(video.key); const videoAuthorPrefs = await about.visibilityPrefs(video.author).catch(() => null); await enrichItemLifetime(video, { key: video.key }); const singleFav = await contentFavorites.getFavoriteSet('videos'); @@ -5643,6 +5989,7 @@ router ctx.body = await singleVideoView({ ...video, isFavorite: fav.has(String(video.rootId || video.key)), commentCount: comments.length }, filter, comments, { censusList: singleCensus, q, sort, returnTo: safeReturnTo(ctx, `/videos?filter=${encodeURIComponent(filter)}`, ['/videos']), spreads: await spreads.forMessage(video.key), authorPrefs: videoAuthorPrefs, mapData: await resolveMapUrl(video.mapUrl) }); }) .get("/documents", async (ctx) => { + if (!checkMod(ctx, 'docsMod')) { ctx.redirect('/modules'); return; } const { filter = 'all', q = '', sort = 'recent' } = ctx.query; const viewerPrefs = await about.visibilityPrefs(getViewerId()).catch(() => null); const items = await documentsModel.listAll({ filter: filter === 'favorites' ? 'all' : filter, q, sort }); @@ -5659,17 +6006,19 @@ router ctx.body = await documentView(enriched, filter, null, { censusList: censusMedia, q, sort, viewerPrefs, spreadMap }); }) .get("/documents/edit/:id", async (ctx) => { + if (!checkMod(ctx, 'docsMod')) { ctx.redirect('/modules'); return; } const doc = await documentsModel.getDocumentById(ctx.params.id); const fav = await contentFavorites.getFavoriteSet('documents'); if (doc) doc.clearnet = await clearnetPublic('documents', doc).catch(() => false); ctx.body = await documentView([{ ...doc, isFavorite: fav.has(String(doc.rootId || doc.key)) }], 'edit', doc.key, { returnTo: ctx.query.returnTo || '' }); }) .get("/documents/:documentId", async (ctx) => { + if (!checkMod(ctx, 'docsMod')) { ctx.redirect('/modules'); return; } const { filter = "all", q = "", sort = "recent" } = ctx.query; const document = await documentsModel.getDocumentById(ctx.params.documentId); const fav = await contentFavorites.getFavoriteSet('documents'); Object.assign(document, { isFavorite: fav.has(String(document.rootId || document.key)) }); - const comments = await getVoteComments(document.rootId || document.key); + const comments = await commentsFor(document.rootId || document.key); const docAuthorPrefs = await about.visibilityPrefs(document.author).catch(() => null); await enrichItemLifetime(document, { key: document.key }); const singleFav = fav; @@ -6034,7 +6383,7 @@ router const emergency = await emergenciesModel.getEmergencyById(ctx.params.id); if (!emergency) { ctx.redirect('/emergencies'); return; } emergency.isFavorite = (await contentFavorites.getFavoriteSet('emergencies')).has(String(emergency.id)); - const comments = await getVoteComments(emergency.id); + const comments = await commentsFor(emergency.id); const censusList = await censusOf('emergencies', () => emergenciesModel.listAll({ filter: 'all', q: '', viewerId: uid })).catch(() => []); const subscription = await subscriptionStateFor(emergency.id, String(emergency.author) === String(uid)).catch(() => null); await warmAuthorNames([emergency, ...emergency.updates, ...comments]); @@ -6109,7 +6458,7 @@ router const route = await logisticsModel.getRouteById(ctx.params.id); if (!route) { ctx.redirect('/logistics'); return; } route.isFavorite = (await contentFavorites.getFavoriteSet('logistics')).has(String(route.id)); - const comments = await getVoteComments(route.id); + const comments = await commentsFor(route.id); const censusList = await censusOf('logistics', () => logisticsModel.listAll({ filter: 'all' })).catch(() => []); const zones = logisticsModel.zonesOf(censusList); await warmAuthorNames([route, ...route.bookings.map(b => ({ author: b.booker })), ...route.ratings, ...comments]); @@ -6143,21 +6492,28 @@ router return; } let channels; + const sort = ['recent', 'oldest', 'top'].includes(String(ctx.query.sort || '')) ? String(ctx.query.sort) : 'recent'; if (filter === 'TOP') channels = censusList.filter(c => c.opinionCount > 0 || c.spreadCount > 0).sort((a, b) => b.spreadCount - a.spreadCount || b.opinionCount - a.opinionCount); + else if (filter === 'FAVORITES') channels = censusList.filter(c => c.isFavorite); else channels = (filter === 'ALL' && !q) ? censusList : decorate(await podcastsModel.listAll({ filter, q })); + if (filter !== 'TOP' && filter !== 'VIEWERS' && sort !== 'recent') { + channels = channels.slice().sort(sort === 'oldest' + ? (a, b) => new Date(a.createdAt) - new Date(b.createdAt) + : (a, b) => (b.opinionCount || 0) - (a.opinionCount || 0) || (b.spreadCount || 0) - (a.spreadCount || 0)); + } if (q && filter === 'TOP') channels = channels.filter(c => [c.title, c.description, ...c.tags].some(v => String(v || '').toLowerCase().includes(q.toLowerCase()))); const shownList = await applyWishScope(channels); const spreadMap = needAllSpreads ? allSpreads : await spreads.forMessages(pageOf(ctx, shownList).map(c => c && c.id)).catch(() => new Map()); await warmAuthorNames(pageOf(ctx, shownList)); await annotateClearnet('podcasts', channels); - ctx.body = await podcastsView(shownList, filter, { q, censusList, spreadMap, viewerPrefs: await about.visibilityPrefs(getViewerId()).catch(() => null) }); + ctx.body = await podcastsView(shownList, filter, { q, sort, censusList, spreadMap, viewerPrefs: await about.visibilityPrefs(getViewerId()).catch(() => null) }); }) .get('/podcasts/episode/:id', async ctx => { if (!checkMod(ctx, 'podcastsMod')) { ctx.redirect('/modules'); return; } const ep = await podcastsModel.getEpisodeById(ctx.params.id); if (!ep) { ctx.redirect('/podcasts'); return; } ep.isFavorite = (await contentFavorites.getFavoriteSet('podcasts')).has(String(ep.id)); - const comments = await getVoteComments(ep.id); + const comments = await commentsFor(ep.id); const censusList = await censusOf('podcasts', () => podcastsModel.listAll({ filter: 'all' })).catch(() => []); await warmAuthorNames([ep, ...comments]); ctx.body = await singleEpisodeView(ep, { comments, censusList, mode: String(ctx.query.mode || ''), spread: await spreads.forMessage(ep.id).catch(() => null), authorPrefs: await about.visibilityPrefs(ep.author).catch(() => null) }); @@ -6170,7 +6526,7 @@ router const favPodcasts = await contentFavorites.getFavoriteSet('podcasts'); ch.isFavorite = favPodcasts.has(String(ch.id)); ch.episodes = (ch.episodes || []).map(e => ({ ...e, isFavorite: favPodcasts.has(String(e.id)) })); - const comments = await getVoteComments(ch.id); + const comments = await commentsFor(ch.id); const censusList = await censusOf('podcasts', () => podcastsModel.listAll({ filter: 'all' })).catch(() => []); const subscription = await subscriptionStateFor(ch.id, String(ch.author) === String(uid)).catch(() => null); const spreadMap = await spreads.forMessages(ch.episodes.map(e => e.id)).catch(() => new Map()); @@ -6221,7 +6577,7 @@ router const cp = await campaignsModel.getCampaignById(ctx.params.id, uid); if (!cp) { ctx.redirect('/campaigns'); return; } cp.isFavorite = (await contentFavorites.getFavoriteSet('campaigns')).has(String(cp.id)); - const comments = await getVoteComments(cp.id); + const comments = await commentsFor(cp.id); const censusList = await censusOf('campaigns', () => campaignsModel.listAll({ filter: 'all', viewerId: uid })).catch(() => []); const subscription = await subscriptionStateFor(cp.id, String(cp.author) === String(uid)).catch(() => null); await warmAuthorNames([cp, ...cp.signatures, ...cp.updates, ...comments]); @@ -6230,12 +6586,12 @@ router }) .get('/qr-action/sign/:id', async ctx => { if (!checkMod(ctx, 'campaignsMod')) { ctx.redirect('/modules'); return; } - const id = decodeURIComponent(ctx.params.id || ''); + const id = paramId(ctx.params.id); confirmPage(ctx, { message: require('../views/main_views').i18n.confirmSignText, action: `/qr-action/sign/${encodeURIComponent(id)}`, backHref: `/campaigns/${encodeURIComponent(id)}` }); }) .post('/qr-action/sign/:id', koaBody(), async ctx => { if (!checkMod(ctx, 'campaignsMod')) { ctx.redirect('/modules'); return; } - const id = decodeURIComponent(ctx.params.id || ''); + const id = paramId(ctx.params.id); try { await campaignsModel.sign(id, ''); } catch (e) { if (!/already/i.test(String(e && e.message))) return actionFail(ctx); } ctx.redirect(`/campaigns/${encodeURIComponent(id)}`); }) @@ -6260,7 +6616,7 @@ router }) .get('/reports/:reportId', async ctx => { const { reportId } = ctx.params, filter = qf(ctx), report = await reportsModel.getReportById(reportId); - const comments = await getVoteComments(reportId); + const comments = await commentsFor(reportId); await enrichMsgSize([report]); await enrichItemLifetime(report); const singleCensus = await censusOf('reports', () => reportsModel.listAll()).catch(() => []); @@ -6285,8 +6641,9 @@ router const favIndex = await contentFavorites.getFavoriteIndex().catch(() => new Map()); const pageItems = await renderedPage(async () => trendingView(filtered, filter, trendingModel.categories, new Map(), q, allTrending, { favIndex })); const spreadMap = await spreads.forMessages(pageItems.map(it => it && it.key)).catch(() => new Map()); + const commentCounts = new Map((await attachCommentMeta(pageItems.filter(Boolean))).map(it => [it.key, it.commentCount || 0])); await warmAuthorNames(pageItems); - ctx.body = await trendingView(filtered, filter, trendingModel.categories, spreadMap, q, allTrending, { favIndex }); + ctx.body = await trendingView(filtered, filter, trendingModel.categories, spreadMap, q, allTrending, { favIndex, commentCounts }); }) .get('/agenda', async (ctx) => { const filter = qf(ctx); @@ -6325,27 +6682,7 @@ router }); } const inhabitants = await inhabitantsModel.listInhabitants({ filter, ...query }); - const bankFor = async (u) => { - try { - const bank = await bankingModel.getBankingData(u.id); - return { karmaScore: bank?.karmaScore || 0, estimatedUBI: bank?.estimatedUBI || 0, lastClaimedDate: bank?.lastClaimedDate || null, totalClaimed: bank?.totalClaimed || 0 }; - } catch { - return { karmaScore: 0, estimatedUBI: 0, lastClaimedDate: null, totalClaimed: 0 }; - } - }; - const activityList = await Promise.all( - inhabitants.map(async (u) => { - if (u.lastActivityBucket) return { id: u.id, lastActivityBucket: u.lastActivityBucket }; - try { - const ts = await inhabitantsModel.getLastActivityTimestampByUserId(u.id); - const { bucket } = inhabitantsModel.bucketLastActivity(ts || null); - return { id: u.id, lastActivityBucket: bucket }; - } catch { - return { id: u.id, lastActivityBucket: 'red' }; - } - }) - ); - const activityMap = new Map(activityList.map(x => [x.id, x.lastActivityBucket])); + const activityMap = await inhabitantActivityBuckets(inhabitants); let listed = inhabitants .map(u => ({ ...u, lastActivityBucket: activityMap.get(u.id) })) .filter(u => u.id === userId || u.lastActivityBucket !== 'red'); @@ -6359,35 +6696,7 @@ router ); } const pageUsers = slicePage(listed, new URLSearchParams(ctx.querystring).get('page'), listPerPage(ctx.querystring)).items; - const addrMap = new Map((await bankingModel.listAddressesMerged()).map(x => [x.id, x.address])); - const profiles = config.public ? null : await about.profiles().catch(() => null); - const details = new Map(await Promise.all(pageUsers.map(async (u) => { - const pr = profiles ? (profiles.get(u.id) || {}) : null; - const prefsNow = pr ? about.visibilityFrom(pr.visibilityPrefs) : await about.visibilityPrefs(u.id).catch(() => null); - const published = Number(u.karmaScore) > 0; - const [kd, prefs, rel, fp, src, carbon, kr] = await Promise.all([ - (prefsNow && prefsNow.ubi === true) || !published ? bankFor(u) : { karmaScore: Number(u.karmaScore), estimatedUBI: 0, lastClaimedDate: null, totalClaimed: 0 }, - prefsNow, - friend.getRelationship(u.id).catch(() => null), - pr ? (typeof pr.gpgFingerprint === 'string' ? pr.gpgFingerprint : '') : about.gpgFingerprint(u.id).catch(() => ''), - pr ? (typeof pr.deviceSource === 'string' && pr.deviceSource.trim() ? pr.deviceSource : null) : about.deviceSource(u.id).catch(() => null), - getCarbonGramsForFeed(u.id).catch(() => 0), - bankingModel.getKarmaReceived(u.id).catch(() => null) - ]); - return [u.id, { - ecoAddress: addrMap.get(u.id) || null, - karmaReceived: kr, - karmaScore: kd.karmaScore ?? (typeof u.karmaScore === 'number' ? u.karmaScore : 0), - estimatedUBI: kd.estimatedUBI || 0, - lastClaimedDate: kd.lastClaimedDate || null, - totalClaimed: kd.totalClaimed || 0, - visibilityPrefs: prefs || null, - relationship: rel || null, - gpgFingerprint: fp || '', - deviceSource: src || null, - carbonGrams: carbon || 0 - }]; - }))); + const details = await inhabitantCardDetails(pageUsers); const enriched = listed.map(u => details.has(u.id) ? { ...u, ...details.get(u.id) } : u); const censusBase = filter === 'all' ? enriched : await censusOf('inhabitants:all', () => inhabitantsModel.listInhabitants({ filter: 'all' })).catch(() => []); const cvCensus = filter === 'CVs' ? enriched : await censusOf('inhabitants:CVs', () => inhabitantsModel.listInhabitants({ filter: 'CVs' })).catch(() => []); @@ -6512,6 +6821,7 @@ router houseNames, candidatures, proposals: proposalsLive, + comments: await commentsByRoot((filter === 'proposals' ? pageOf(ctx, proposalsLive) : filter === 'laws' ? pageOf(ctx, laws || []) : []).map(x => x && x.rootId).filter(Boolean)), futureLaws: futureLawsLive, canPropose, laws, @@ -6662,7 +6972,9 @@ router let sectionData = null; if (section === 'inhabitants') { const allInhabitants = await inhabitantsModel.listInhabitants({ filter: 'all', includeInactive: true, ids: tribe.members }); - sectionData = allInhabitants.filter(u => tribe.members.includes(u.id)); + const members = allInhabitants.filter(u => tribe.members.includes(u.id)); + const [buckets, details] = await Promise.all([inhabitantActivityBuckets(members), inhabitantCardDetails(members)]); + sectionData = members.map(u => ({ ...u, lastActivityBucket: buckets.get(u.id), ...(details.get(u.id) || {}) })); } else if (section === 'feed') { sectionData = await listByTribeAllChain(tribe.id, 'feed').catch(() => []); } else if (section === 'forum') { @@ -6930,6 +7242,7 @@ router } catch (_) {} try { await warmAuthorNames(Array.isArray(sectionData) ? sectionData : (sectionData && sectionData.items) || []); } catch (_) {} try { tribe.subscription = await subscriptionStateFor(tribe.id, tribe.author); } catch (_) {} + if (!tribe.isAnonymous) { try { tribe.spreads = await spreads.forMessage(tribe.id); } catch (_) {} } try { if (larpModel && typeof larpModel.getUserHouse === 'function') tribe.viewerHouse = await larpModel.getUserHouse(uid); } catch (_) {} if (tribe.mapUrl) tribe.mapData = await resolveMapUrl(tribe.mapUrl); ctx.body = await tribeView(tribe, uid, query, section, sectionData); @@ -6989,6 +7302,7 @@ router allActions = await applyListFilters(allActions, ctx); const favIndex = await contentFavorites.getFavoriteIndex().catch(() => new Map()); await attachCommentMeta(allActions); + await commentsModel.annotate(allActions).catch(() => null); const actPage = parseInt(String((ctx.query && ctx.query.page) || ''), 10); const actPer = ctx.query && ctx.query.perPage ? listPerPage(ctx.querystring) : ''; const returnTo = `/activity?filter=${encodeURIComponent(filter)}${q ? `&q=${encodeURIComponent(q)}` : ''}${actPer ? `&perPage=${actPer}` : ''}${actPage > 1 ? `&page=${actPage}` : ''}`; @@ -7291,22 +7605,46 @@ router .get("/blob/sealed/:manifestId", async (ctx) => { if (config.public) { ctx.status = 404; return; } const pointer = sealedPointerOf(ctx.params.manifestId, ctx.query); - if (!pointer || !(await fileshareModel.ensureAvailable(pointer))) { ctx.status = 404; return; } + if (!pointer) { ctx.status = 404; return; } + if (!(await fileshareModel.ensureAvailable(pointer))) return sendPendingContent(ctx); ctx.type = pointer.mime; ctx.set('X-Content-Type-Options', 'nosniff'); ctx.set('Cache-Control', 'private, max-age=3600'); ctx.set('Content-Disposition', contentDisposition(SEALED_INLINE.test(pointer.mime) ? 'inline' : 'attachment', pointer.filename)); ctx.body = fileshareModel.readShareStream(pointer); }) - .get("/blob/:blobId", async (ctx) => { blobCacheModel.touch(ctx.params.blobId); return serveBlob(ctx); }) + .get("/pdf", async (ctx) => { + const state = await pdfViewState(ctx); + ctx.body = await require('../views/main_views').pdfViewerView(state, { title: pdfTitleOf(ctx.query.title), back: pdfBackOf(ctx) }); + }) + .get("/pdf/embed", async (ctx) => { + const state = await pdfViewState(ctx); + ctx.type = 'text/html'; + ctx.body = require('../views/main_views').pdfEmbedView(state, { title: pdfTitleOf(ctx.query.title), back: pdfBackOf(ctx) }); + }) + .get("/pdf/page", async (ctx) => sendPdfPage(ctx, pdfSourceOf(ctx.query.src))) + .get("/c/pdf", async (ctx) => { + const state = await pdfViewState(ctx, clearnetPdfSourceOf); + ctx.type = 'text/html'; + ctx.body = require('../views/clearnet_view').renderClearnetPdfView(state, { title: pdfTitleOf(ctx.query.title), back: pdfBackOf(ctx, '/c') }); + }) + .get("/c/pdf/embed", async (ctx) => { + const state = await pdfViewState(ctx, clearnetPdfSourceOf); + ctx.type = 'text/html'; + ctx.body = require('../views/clearnet_view').renderClearnetPdfEmbed(state, { title: pdfTitleOf(ctx.query.title), back: pdfBackOf(ctx, '/c') }); + }) + .get("/c/pdf/page", async (ctx) => sendPdfPage(ctx, await clearnetPdfSourceOf(ctx.query.src))) + .get("/blob/:blobId", async (ctx) => { + blobCacheModel.touch(ctx.params.blobId); + await serveBlob(ctx); + if (ctx.status === 504) sendPendingContent(ctx); + }) .get("/c/blob/:cnBlobId", async (ctx) => { const blobId = ctx.params.cnBlobId; if (!isBlob(blobId) || !(await clearnetBlobAllowed(blobId))) { ctx.status = 404; ctx.body = ''; return; } let buffer; try { buffer = await blob.getLocal({ blobId }); } catch (_) {} - if (!buffer) { - ctx.status = 404; ctx.body = ''; return; - } + if (!buffer) return sendPendingContent(ctx); let mime = 'application/octet-stream'; try { const ft = await FileType.fromBuffer(buffer); @@ -7368,7 +7706,7 @@ router } catch (e) { ctx.status = 500; ctx.body = ''; } }) .get("/qr-clearnet/:feedId", async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); const reqSize = parseInt(ctx.query.size, 10); const width = Number.isFinite(reqSize) ? Math.max(64, Math.min(512, reqSize)) : 240; try { @@ -7383,11 +7721,20 @@ router ctx.body = ''; } }) + .get("/avatar/:feedId", async (ctx) => { + const feedId = String(ctx.params.feedId || ""); + const cached = FEED_RE.test(feedId) && !config.public ? nameCache.image(feedId) : null; + const img = cached || (FEED_RE.test(feedId) ? await about.image(feedId).catch(() => null) : null); + const link = typeof img === "string" && img.startsWith("&") && img !== nullImageId ? img : null; + ctx.set("Cache-Control", `private, max-age=${AVATAR_CACHE_SECONDS}`); + ctx.redirect(link ? `/image/${AVATAR_SIZE}/${encodeURIComponent(link)}?fallback=avatar` : "/assets/images/default-avatar.png"); + }) .get("/image/:imageSize/:blobId", async (ctx) => { const { blobId, imageSize } = ctx.params; - blobCacheModel.touch(blobId); const size = Number(imageSize); - const cacheKey = `${size}:${blobId}`; + blobCacheModel.touch(blobId); + const inside = ctx.query.fit === 'inside'; + const cacheKey = `${size}:${inside ? 'inside:' : ''}${blobId}`; const hit = resizedImages.get(cacheKey); if (hit) { resizedImages.delete(cacheKey); @@ -7428,12 +7775,16 @@ router try { const buffer = avatarFallback ? await blob.getCached({ blobId }) : await blob.getResolved({ blobId, timeout: 8000 }); if (!buffer) { + if (!avatarFallback) return sendPendingContent(ctx); ctx.body = await fallbackImage(); return; } let type = (await FileType.fromBuffer(buffer))?.mime || "application/octet-stream"; let body = buffer; - if (getSharp()) { + if (getSharp() && inside) { + body = await getSharp()(buffer).rotate().resize(size, size, { fit: 'inside', withoutEnlargement: true }).webp({ quality: THUMB_QUALITY }).toBuffer(); + type = "image/webp"; + } else if (getSharp()) { body = await getSharp()(buffer) .resize(size, size) .png() @@ -7654,7 +8005,8 @@ router if (tr && tr.openInviteCode) h.openInviteCode = tr.openInviteCode; } } catch (_) {} - ctx.body = larpListView({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q: String(ctx.query.q || '').trim() }); + const tribeReady = canPost && !!governingKey && await larpModel.houseTribeReady(governingKey).catch(() => false); + ctx.body = larpListView({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, tribeReady, q: String(ctx.query.q || '').trim() }); }) .get("/larp/test", async (ctx) => { if (!checkMod(ctx, 'larpMod')) return ctx.redirect('/modules'); @@ -7737,12 +8089,13 @@ router const canPost = myHouseKey === houseKey; const posts = await larpModel.listHousePosts(houseKey, { viewerHouse: myHouseKey, isGoverning: houseKey === governingKey }); const testStatus = houseKey === 'academia' ? await larpModel.canTakeTest(myFeedId) : null; + const tribeReady = myHouseKey === houseKey && await larpModel.houseTribeReady(houseKey).catch(() => false); const questions = (houseKey === 'academia' && myHouseKey === 'academia' && testStatus && testStatus.allowed) ? larpModel.getProfileTest() : []; const house = { key: houseKey, ...houseRaw }; const rawInvite = String(ctx.query?.invite || '').trim(); const inviteCode = /^[0-9a-f]{32}$/i.test(rawInvite) && myHouseKey === houseKey ? rawInvite : null; - ctx.body = larpHouseView({ house, members, myHouseKey, cycle, governingKey, houses, posts, canPost, testStatus, inviteCode, questions }); + ctx.body = larpHouseView({ house, members, myHouseKey, cycle, governingKey, houses, posts, canPost, testStatus, inviteCode, questions, tribeReady }); }) .post("/larp/join", koaBody(), async (ctx) => { if (!checkMod(ctx, 'larpMod')) return ctx.redirect('/modules'); @@ -7808,6 +8161,7 @@ router const counts = await mentionsModel.countTypes(all); const items = filter === 'ALL' ? all : all.filter(x => x.type === filter); await warmAuthorNames(pageOf(ctx, items)); + await commentsModel.annotate(pageOf(ctx, items)).catch(() => null); try { sharedState.setMentionsTotal(all.length); sharedState.setMentionsCount(mentionsModel.unseenOf(all).length); } catch (_) {} ctx.body = await mentionsView(items, filter, { counts, total: all.length, q, readKeys: Array.from(mentionsModel.readKeys()), spreadMap: await spreads.forMessages(pageOf(ctx, items).map(x => x && (x.id || x.key))).catch(() => new Map()) }); }) @@ -7847,8 +8201,9 @@ router const favIndex = await contentFavorites.getFavoriteIndex().catch(() => new Map()); const pageItems = await renderedPage(async () => opinionsView(opinions, filter, new Map(), q, allOpinions, { favIndex })); const spreadMap = await spreads.forMessages(pageItems.map(it => it && it.key)).catch(() => new Map()); + const commentCounts = new Map((await attachCommentMeta(pageItems.filter(Boolean))).map(it => [it.key, it.commentCount || 0])); await warmAuthorNames(pageItems); - ctx.body = await opinionsView(opinions, filter, spreadMap, q, allOpinions, { favIndex }); + ctx.body = await opinionsView(opinions, filter, spreadMap, q, allOpinions, { favIndex, commentCounts }); }) .get("/feed", async (ctx) => { const filter = String(ctx.query.filter || "ALL").toUpperCase(); @@ -8274,7 +8629,7 @@ router if (!gate.tribe) { ctx.redirect('/tribes'); return; } if (!gate.ok) { ctx.body = tribeAccessDeniedView(gate.tribe); return; } } - const comments = await getVoteComments(poll.id); + const comments = await commentsFor(poll.id); const fav = await contentFavorites.getFavoriteSet('polls'); await enrichItemLifetime(poll, { key: poll.id }); await warmAuthorNames([poll], comments); @@ -8372,15 +8727,19 @@ router ctx.body = await blogView(blogs, filter, { q, spreadMap, censusList: blogCensus, viewerPrefs: await about.visibilityPrefs(getViewerId()).catch(() => null), viewerId: getViewerId() }); }) .get('/blogs/:blogId', async ctx => { - if (!checkMod(ctx, 'blogsMod')) { ctx.redirect('/modules'); return; } + const elsewhere = async () => { + const target = await commentLocationHref(ctx.params.blogId); + if (!target || target.startsWith(`/blogs/${encodeURIComponent(ctx.params.blogId)}`)) return false; + ctx.redirect(target); + return true; + }; + if (!checkMod(ctx, 'blogsMod')) { if (!(await elsewhere())) ctx.redirect('/modules'); return; } let blog = await blogModel.getBlogById(ctx.params.blogId).catch(() => null); if (!blog) { - const target = await blogModel.blogHrefFor(ctx.params.blogId).catch(() => null); - if (target && !target.startsWith(`/blogs/${encodeURIComponent(ctx.params.blogId)}`)) { ctx.redirect(target); return; } - ctx.redirect('/blogs'); + if (!(await elsewhere())) ctx.redirect('/blogs'); return; } - const comments = await getVoteComments(blog.id); + const comments = await commentsFor(blog.id); const fav = await contentFavorites.getFavoriteSet('blogs'); await warmAuthorNames([blog], comments); if (blog) blog.clearnet = await clearnetPublic('posts', blog).catch(() => false); @@ -8708,7 +9067,7 @@ router .get('/bookmarks/:bookmarkId', async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) return ctx.redirect('/modules'); const filter = qf(ctx), q = ctx.query.q || '', sort = ctx.query.sort || 'recent', favs = await contentFavorites.getFavoriteSet("bookmarks"); - const bookmark = await bookmarksModel.getBookmarkById(ctx.params.bookmarkId), root = bookmark.rootId || bookmark.id, comments = await getVoteComments(root); + const bookmark = await bookmarksModel.getBookmarkById(ctx.params.bookmarkId), root = bookmark.rootId || bookmark.id, comments = await commentsFor(root); await enrichItemLifetime(bookmark); const singleCensus = (await censusOf(`bookmarks:${sort}`, () => bookmarksModel.listAll({ viewerId: getViewerId(), filter: 'all', q: '', sort })).catch(() => [])).map(b2 => ({ ...b2, isFavorite: favs.has(String(b2.rootId || b2.id)) })); if (bookmark) bookmark.clearnet = await clearnetPublic('bookmarks', bookmark).catch(() => false); @@ -8740,7 +9099,7 @@ router }) .get('/tasks/:taskId', async ctx => { const { taskId } = ctx.params, filter = qf(ctx), task = await tasksModel.getTaskById(taskId); - const comments = await getVoteComments(taskId); + const comments = await commentsFor(taskId); await enrichMsgSize([task]); await enrichItemLifetime(task); const singleCensus = await censusOf('tasks', () => tasksModel.listAll()).catch(() => []); @@ -8782,9 +9141,11 @@ router }) .get('/events/:eventId', async ctx => { await eventsModel.ingestKeys().catch(() => {}); - const { eventId } = ctx.params, filter = qf(ctx), event = await eventsModel.getEventById(eventId); + const { eventId } = ctx.params, filter = qf(ctx); + const event = await eventsModel.getEventById(eventId).catch(() => null); + if (!event) { sendErrorPage(ctx, `Event not found: ${eventId}`, { status: 404 }); return; } const [comments, mapData, linkedCalendarId] = await Promise.all([ - getVoteComments(eventId), + commentsFor(eventId), resolveMapUrl(event.mapUrl), calendarsModel.findCalendarByLinkText(`/events/${eventId}`).catch(() => null) ]); @@ -8838,12 +9199,12 @@ router const id = ctx.params.id; const activeFilter = (ctx.query.filter || 'mine'); const voteData = await votesModel.getVoteById(id); - if (require('../models/votes_model').isVoteLocked(voteData)) { failWith(ctx, 'voteLocked', `/votes/${encodeURIComponent(id)}`); return; } + if (require('../models/votes_model').isVoteEditLocked(voteData)) { failWith(ctx, 'voteLocked', `/votes/${encodeURIComponent(id)}`); return; } ctx.body = await voteView([voteData], 'edit', id, [], activeFilter, voteFormState(ctx)); }) .get('/votes/:voteId', async ctx => { const { voteId } = ctx.params, filter = qf(ctx), voteData = await votesModel.getVoteById(voteId); - const comments = await getVoteComments(voteId); + const comments = await commentsFor(voteId); await enrichMsgSize([voteData]); await enrichItemLifetime(voteData, { author: voteData.createdBy }); ctx.body = await voteView([await withFavorite(withCount(voteData, comments), 'votes')], 'detail', voteId, comments, filter, { spreads: await spreads.forMessage(voteId).catch(() => null) }); @@ -8883,7 +9244,7 @@ router item = await marketModel.getItemById(itemId) if (!item) ctx.throw(404, "Item not found") const zoom = parseInt(ctx.query.zoom) || 2; - const [comments, mapData] = await Promise.all([getVoteComments(itemId), resolveMapUrl(item.mapUrl)]) + const [comments, mapData] = await Promise.all([commentsFor(itemId), resolveMapUrl(item.mapUrl)]) const returnTo = (() => { const params = [] if (filter) params.push(`filter=${encodeURIComponent(filter)}`) @@ -8896,7 +9257,8 @@ router await enrichItemLifetime(item, { author: item.seller, createdAt: item.updatedAt || item.createdAt }) const singleCensus = await censusOf('market', () => marketModel.listAllItems('all')).catch(() => []); if (item) item.clearnet = await clearnetPublic('market', item).catch(() => false); - ctx.body = await singleMarketView(await withFavorite(withCount(item, comments), 'market'), filter, comments, { censusList: singleCensus, q, minPrice, maxPrice, sort, returnTo, mapData, zoom, spreads: await spreads.forMessage(item.id).catch(() => null) }) + const orders = await marketModel.listOrders(item.rootId || item.id).catch(() => []); + ctx.body = await singleMarketView(await withFavorite(withCount(item, comments), 'market'), filter, comments, { censusList: singleCensus, q, minPrice, maxPrice, sort, returnTo, mapData, zoom, orders, spreads: await spreads.forMessage(item.id).catch(() => null) }) }) .get('/jobs', async (ctx) => { if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; } @@ -8952,7 +9314,7 @@ router const id = ctx.params.id const viewerId = getViewerId() const job = await jobsModel.getJobById(id, viewerId) - if (job) job.clearnet = await clearnetPublic('jobs', job).catch(() => false); + if (job) Object.assign(job, { clearnet: await clearnetPublic('jobs', job).catch(() => false), mediaLocked: await isRatedMedia(job.id, job) }); ctx.body = await jobsView([job], 'EDIT', { reach: String(ctx.query.visibility || '') }) }) .get('/jobs/:jobId', async (ctx) => { @@ -8982,7 +9344,7 @@ router sendErrorPage(ctx, `Job not found: ${jobId}`, { status: 404 }); return; } - const [comments, mapData] = await Promise.all([getVoteComments(jobId), resolveMapUrl(job.mapUrl)]) + const [comments, mapData] = await Promise.all([commentsFor(jobId), resolveMapUrl(job.mapUrl)]) await enrichMsgSize([job]) let candidates = []; if (job && String(job.author) === String(viewerId)) { @@ -8991,7 +9353,9 @@ router const jobAuthorPrefs2 = await about.visibilityPrefs(job.author).catch(() => null); await enrichItemLifetime(job) if (job) job.clearnet = await clearnetPublic('jobs', job).catch(() => false); - ctx.body = await singleJobsView(await withFavorite(withCount(job, comments), 'jobs'), filter, comments, { ...params, mapData, candidates, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: jobAuthorPrefs2, spreads: await spreads.forMessage(job.id).catch(() => null) }) + const jobOpinions = OPINABLE.jobs(job) ? await opinionsOf([job.rootId, job.id]) : null; + const jobsCensusOne = await censusOf('jobs', () => jobsModel.listJobs('ALL', viewerId, {})).catch(() => []); + ctx.body = await singleJobsView(await withFavorite(withCount(job, comments), 'jobs'), filter, comments, { ...params, censusList: jobsCensusOne, opinions: jobOpinions, mapData, candidates, baseUrl: resolveExternalBaseUrl(ctx), authorPrefs: jobAuthorPrefs2, spreads: await spreads.forMessage(job.id).catch(() => null) }) }) .get('/housing', async (ctx) => { if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } @@ -9046,7 +9410,7 @@ router sendErrorPage(ctx, `Housing not found or invalid id: ${housingId}`, { status: 404 }); return; } if (!item) { sendErrorPage(ctx, `Housing not found: ${housingId}`, { status: 404 }); return; } - const [comments, mapData] = await Promise.all([getVoteComments(housingId), resolveMapUrl(item.mapUrl)]) + const [comments, mapData] = await Promise.all([commentsFor(housingId), resolveMapUrl(item.mapUrl)]) await enrichMsgSize([item]) await enrichItemLifetime(item) item.clearnet = await clearnetPublic('housing', item).catch(() => false); @@ -9238,7 +9602,7 @@ router const product = await shopsModel.getProductById(ctx.params.productId); if (!product) { ctx.redirect('/shops'); return; } const shop = await shopsModel.getShopById(product.shopId); - const comments = await getVoteComments(product.key); + const comments = await commentsFor(product.key); const myPurchases = await shopsModel.listMyPurchases().catch(() => []); const productRootId = product.rootId || product.key; const canRate = myPurchases.some(o => o.productId === productRootId && String(o.status || '').toUpperCase() === 'RECEIVED'); @@ -9336,7 +9700,7 @@ router ctx.type = 'text/html'; ctx.body = require('../views/clearnet_view').renderClearnetMediaView({ kind: 'bookmark', - item: { title: hostOf(item.url) || 'Bookmark', description: [item.url, item.description].filter(Boolean).join('\n\n'), createdAt: item.createdAt || null, author: item.author, tags: item.tags || [] } + item: { title: item.title || hostOf(item.url) || 'Bookmark', description: [item.url, item.description].filter(Boolean).join('\n\n'), createdAt: item.createdAt || null, author: item.author, tags: item.tags || [] } }); }) .get("/c/podcasts/:id", async (ctx) => { @@ -9349,7 +9713,39 @@ router return; } ctx.type = 'text/html'; - ctx.body = require('../views/clearnet_view').renderClearnetPodcastView({ channel }); + ctx.body = require('../views/clearnet_view').renderClearnetPodcastView({ channel, feed: `/c/podcasts/${encodeURIComponent(require('../views/main_views').clearnetSlugFor(channel.title, channel.id))}/rss` }); + }) + .get("/c/podcasts/:id/rss", async (ctx) => { + ctx.params.id = await clearnetIdFor('podcasts', ctx.params.id); + let channel; try { channel = await podcastsModel.getChannelById(ctx.params.id); } catch (_) {} + const p = channel && channel.author ? await about.visibilityPrefs(channel.author).catch(() => null) : null; + if (!channel || !(channel.episodeCount > 0) || !clearnetPublicSync('podcasts', channel, channel.author, p, await clearnetDecisions())) { + ctx.status = 404; ctx.type = 'text/html'; + ctx.body = require('../views/clearnet_view').renderClearnetNotFound(); + return; + } + const { escapeHtml: esc, blobUrl: cnBlobUrl } = require('../views/clearnet_view'); + const { clearnetSlugFor, i18n } = require('../views/main_views'); + const base = resolveExternalBaseUrl(ctx); + const page = `${base}/c/podcasts/${encodeURIComponent(clearnetSlugFor(channel.title, channel.id))}`; + const ssbR = await cooler.open(); + const blobFacts = async (blobId, kind) => { + const size = await new Promise(res => { try { ssbR.blobs.size(blobId, (e, n) => res(e ? 0 : Number(n) || 0)); } catch (_) { res(0); } }); + const head = await new Promise(res => pull(ssbR.blobs.get(blobId), pull.take(1), pull.collect((e, chunks) => res(e || !chunks || !chunks[0] ? null : Buffer.from(chunks[0]))))); + const ft = head ? await FileType.fromBuffer(head).catch(() => null) : null; + return { size, type: (ft && ft.mime) || (kind === 'video' ? 'video/mp4' : 'audio/mpeg') }; + }; + const episodes = (channel.episodes || []).filter(ep => ep && ep.media && ep.media.blobId).slice().sort((a, b) => (Number(b.number) || 0) - (Number(a.number) || 0)); + const items = []; + for (const ep of episodes.slice(0, PODCAST_FEED_MAX)) { + const facts = await blobFacts(ep.media.blobId, ep.media.kind); + items.push(` \n ${esc(`#${Number(ep.number) || 0} ยท ${ep.title || ''}`)}\n ${esc(page)}\n ${esc(ep.id)}\n ${new Date(ep.createdAt || Date.now()).toUTCString()}\n ${esc(ep.description || '')}\n \n ${Number(ep.number) || 0}\n `); + } + const cover = channel.cover && channel.cover.kind === 'image' && channel.cover.blobId ? base + cnBlobUrl(channel.cover.blobId) : ''; + const category = PODCAST_ITUNES_CATEGORY[channel.category] || ''; + ctx.type = 'application/rss+xml'; + ctx.set('Cache-Control', 'public, max-age=600'); + ctx.body = `\n\n \n ${esc(channel.title || i18n.cnUntitled)}\n ${esc(page)}\n \n ${esc(channel.description || '')}\n false\n${cover ? ` \n ${esc(cover)}${esc(channel.title || '')}${esc(page)}\n` : ''}${category ? ` \n` : ''}${items.join('\n')}\n \n`; }) .get("/c/videos/:id", async (ctx) => { ctx.params.id = await clearnetIdFor('videos', ctx.params.id); @@ -9409,7 +9805,8 @@ router return; } ctx.type = 'text/html'; - ctx.body = require('../views/clearnet_view').renderClearnetMediaView({ kind: 'torrent', item: { ...item, details: detailsOf(await blobSizeOf(item.url)).map(v => `โ‡ฉ ${v}`) } }); + const torrentInfo = await torrentInfoOf(item); + ctx.body = require('../views/clearnet_view').renderClearnetMediaView({ kind: 'torrent', item: { ...item, magnet: torrentInfo ? torrentInfo.magnet : '', details: detailsOf(await blobSizeOf(item.url)).map(v => `โ‡ฉ ${v}`) } }); }) .get("/c/blog/:msgKey", async (ctx) => { ctx.params.msgKey = await clearnetIdFor('posts', ctx.params.msgKey); @@ -9481,7 +9878,7 @@ router const members = new Set(Array.isArray(tribe.members) ? tribe.members : []); for (const entry of await getClearnetIndex().catch(() => [])) { if (!members.has(entry.feedId)) continue; - for (const b of (entry.items.posts || [])) items.push({ id: b.id, contentType: 'blog', title: b.title, description: b.snippet, author: entry.feedId, createdAt: b.ts, href: `/c/blog/${encodeURIComponent(clearnetSlugFor(b.title, b.id))}` }); + for (const b of (entry.items.posts || [])) items.push({ id: b.id, contentType: 'blog', title: b.title, description: b.snippet, pdfs: Array.isArray(b.pdfs) ? b.pdfs : [], author: entry.feedId, createdAt: b.ts, href: `/c/blog/${encodeURIComponent(clearnetSlugFor(b.title, b.id))}` }); } items.sort((a, b) => (new Date(b.createdAt).getTime() || 0) - (new Date(a.createdAt).getTime() || 0)); const names = {}; @@ -9511,29 +9908,19 @@ router ctx.type = 'application/xml'; ctx.body = `\n\n${Array.from(new Set(urls)).map(u => ` ${esc(u)}`).join('\n')}\n\n`; }) + .get("/c/rss", async (ctx) => { + const { CLEARNET_MODULES, i18n } = require('../views/main_views'); + await sendClearnetRss(ctx, CLEARNET_MODULES, i18n.cnHubTitle || 'Oasis HUB', ''); + }) .get("/c/rss/:module", async (ctx) => { - const { escapeHtml: esc } = require('../views/clearnet_view'); - const { CLEARNET_MODULES, clearnetSlugFor, cnModuleLabel, i18n } = require('../views/main_views'); + const { CLEARNET_MODULES, cnModuleLabel } = require('../views/main_views'); const wanted = String(ctx.params.module || '').replace(/\.xml$/i, '').toLowerCase(); const mod = CLEARNET_MODULES.find(m => m.key === wanted || (m.modulePath || m.key) === wanted); if (!mod) { ctx.status = 404; ctx.body = require('../views/clearnet_view').renderClearnetNotFound(); return; } - const base = resolveExternalBaseUrl(ctx); - const index = await getClearnetIndex(); - const items = []; - for (const entry of index) { - for (const it of (entry.items[mod.key] || [])) items.push({ ...it, authorName: entry.name || '', feedId: entry.feedId }); - } - items.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0)); - const rssItems = items.slice(0, 50).map(it => { - const link = `${base}/c/${mod.modulePath || mod.key}/${encodeURIComponent(clearnetSlugFor(it.title, it.id))}`; - const date = new Date(Number(it.ts) || Date.now()).toUTCString(); - return ` \n ${esc(it.title || i18n.cnUntitled)}\n ${esc(link)}\n ${esc(String(it.id || link))}\n ${date}\n ${esc(it.authorName || it.feedId || '')}\n ${esc(String(it.snippet || '').slice(0, 500))}\n `; - }).join('\n'); - ctx.type = 'application/rss+xml'; - ctx.body = `\n\n \n Oasis HUB ยท ${esc(cnModuleLabel(mod))}\n ${esc(`${base}/c?type=${mod.key}`)}\n ${esc(`${cnModuleLabel(mod)} ${i18n.cnRssDescription}`)}\n${rssItems}\n \n\n`; + await sendClearnetRss(ctx, [mod], cnModuleLabel(mod), `?type=${mod.key}`); }) .get("/c/inhabitant/:feedId", async (ctx) => { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); if (!ssbRef.isFeedId(feedId)) { ctx.type = 'text/html'; ctx.body = require('../views/clearnet_view').renderClearnetNotFound(); @@ -9593,6 +9980,19 @@ router ctx.type = 'text/html'; ctx.body = await require('../views/school_view').clearnetCourseView(course, lessons); }) + .get("/c/shops/:shopId/product/:productId", async (ctx) => { + const shopId = await clearnetIdFor('shops', ctx.params.shopId); + const shop = await shopsModel.getShopById(shopId).catch(() => null); + const notFound = () => { ctx.type = 'text/html'; ctx.body = require('../views/clearnet_view').renderClearnetNotFound(); }; + if (!shop || String(shop.visibility || '').toUpperCase() === 'CLOSED') return notFound(); + const shopAuthorPrefs = await about.visibilityPrefs(shop.author).catch(() => null); + if (!clearnetPublicSync('shops', shop, shop.author, shopAuthorPrefs, await clearnetDecisions())) return notFound(); + const productId = paramId(ctx.params.productId); + const product = isMsgKey(productId) ? await shopsModel.getProductById(productId).catch(() => null) : null; + if (!product || product.shopId !== (shop.rootId || shop.key) || !(Number(product.stock) > 0 || product.featured)) return notFound(); + ctx.type = 'text/html'; + ctx.body = await require('../views/shops_view').clearnetProductView(shop, product); + }) .get("/c/shops/:shopId", async (ctx) => { ctx.params.shopId = await clearnetIdFor('shops', ctx.params.shopId); const shop = await shopsModel.getShopById(ctx.params.shopId).catch(() => null); @@ -9623,7 +10023,7 @@ router if (!shop) { ctx.redirect('/shops'); return; } if (shop.encrypted && shop.undecryptable && shop.author !== getViewerId()) { ctx.redirect('/invites?filter=others'); return; } const fav = await contentFavorites.getFavoriteSet('shops'); - const [rawProducts, comments, mapData] = await Promise.all([shopsModel.listProducts(shop.rootId || shop.key), getVoteComments(shop.key), resolveMapUrl(shop.mapUrl)]); + const [rawProducts, comments, mapData] = await Promise.all([shopsModel.listProducts(shop.rootId || shop.key), commentsFor(shop.key), resolveMapUrl(shop.mapUrl)]); const favShopProducts = await contentFavorites.getFavoriteSet('shopProducts').catch(() => new Set()); const products = (rawProducts || []).map(p => ({ ...p, isFavorite: favShopProducts.has(String(p.rootId || p.key)) })); const productSpreadMap = await spreads.forMessages(pageOf(ctx, products).map(p => p && p.key)).catch(() => new Map()); @@ -9818,7 +10218,9 @@ router const censusList = await censusOf(`wiki-pages:${page.tribeId || ''}`, () => wikiModel.listPages({ tribeId: page.tribeId || null, filter: 'all' })).catch(() => []); await warmAuthorNames([page, ...page.versions]); if (page) page.clearnet = await clearnetPublic('wiki', page).catch(() => false); - ctx.body = await wikiPageView(page, { version, diff, tribe, subscription, censusList, spread: await spreads.forMessage(page.id).catch(() => null), authorPrefs: await about.visibilityPrefs(page.author).catch(() => null) }); + const comments = COMMENTABLE.wiki(page) ? await commentsFor(page.id) : null; + const opinions = OPINABLE.wiki(page) ? await opinionsOf([page.id, ...(page.versions || []).map(v => v.key)]) : null; + ctx.body = await wikiPageView(page, { version, diff, tribe, subscription, censusList, comments, opinions, spread: await spreads.forMessage(page.id).catch(() => null), authorPrefs: await about.visibilityPrefs(page.author).catch(() => null) }); }) .get("/pads", async (ctx) => { if (!checkMod(ctx, 'padsMod')) { ctx.redirect('/modules'); return; } @@ -10042,7 +10444,7 @@ router const filter = String(ctx.query.filter || "ALL").toUpperCase() const project = await projectsModel.getProjectById(projectId) const zoom = parseInt(ctx.query.zoom) || 2; - const [comments, mapData] = await Promise.all([getVoteComments(projectId), resolveMapUrl(project.mapUrl)]) + const [comments, mapData] = await Promise.all([commentsFor(projectId), resolveMapUrl(project.mapUrl)]) await enrichMsgSize([project]) await enrichItemLifetime(project, { key: project.id || project.key }) try { project.subscription = await subscriptionStateFor(project.rootId || project.id, project.author); } catch (_) {} @@ -10124,7 +10526,9 @@ router if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } try { const bp = await industryModel.getBlueprint(ctx.params.id) - ctx.body = await singleBlueprintView(bp, { spreads: await spreads.forMessage(bp.id).catch(() => null) }) + const comments = COMMENTABLE.industry(bp) ? await commentsFor(bp.rootId || bp.id) : null + const opinions = OPINABLE.industry(bp) ? await opinionsOf([bp.rootId, bp.id]) : null + ctx.body = await singleBlueprintView(bp, { comments, opinions, spreads: await spreads.forMessage(bp.id).catch(() => null) }) } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } }) .get("/industry/blueprints/:id/edit", async (ctx) => { @@ -10132,7 +10536,7 @@ router try { const bp = await industryModel.getBlueprint(ctx.params.id) const fc = await industryModel.getFacilityById(bp.facility) - ctx.body = await blueprintEditView(bp, fc) + ctx.body = await blueprintEditView({ ...bp, mediaLocked: await isRatedMedia(bp.id, bp) }, fc) } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 404 }) } }) .get("/industry/:id", async (ctx) => { @@ -10167,6 +10571,7 @@ router }) .post("/industry/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; try { const image = (ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined) || undefined await industryModel.updateFacility(ctx.params.id, { ...(ctx.request.body || {}), ...(image !== undefined ? { image } : {}) }) @@ -10242,6 +10647,7 @@ router }) .post("/industry/blueprints/:id/update", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; try { const image = (ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined) || undefined await industryModel.updateBlueprint(ctx.params.id, { ...(ctx.request.body || {}), ...(image !== undefined ? { image } : {}) }) @@ -10523,12 +10929,36 @@ router sendErrorPage(ctx, error.message); } }) + .get("/versions/:id", async (ctx) => { + const id = String(ctx.params.id || ''); + if (!isMsgKey(id)) { ctx.redirect('/'); return; } + const ssbV = await cooler.open(); + const get = (k) => new Promise(res => ssbV.get(k, (e, m) => res(e ? null : m))); + const tip = await get(id); + const tc = tip && tip.content; + const readable = (m) => !!m && m.content && typeof m.content === 'object' && !m.content.encryptedPayload && !m.content.tribeId && !m.private && !Array.isArray(m.content.recps); + if (!readable(tip)) { sendErrorPage(ctx, 'not found', { status: 404 }); return; } + if (tc.type === 'wikiPage') { ctx.redirect(`/wiki/${encodeURIComponent(id)}/history`); return; } + const { readTyped } = require('../models/typed_log'); + const { buildValidatedTombstoneSet } = require('../models/tombstone_validator'); + const gone = buildValidatedTombstoneSet(await readTyped(ssbV, ['tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }).catch(() => [])); + if (gone.has(id) || (await dropHidden([{ key: id, value: tip }])).length === 0) { sendErrorPage(ctx, 'not found', { status: 404 }); return; } + const versions = []; + const seen = new Set(); + for (let key = id, cur = tip; readable(cur) && cur.author === tip.author && cur.content.type === tc.type && !seen.has(key) && versions.length < VERSIONS_MAX; ) { + seen.add(key); + versions.push({ key, ts: Number(cur.timestamp) || Date.parse(cur.content.updatedAt || cur.content.createdAt || '') || 0, content: cur.content }); + if (typeof cur.content.replaces !== 'string') break; + key = cur.content.replaces; + cur = await get(key); + } + const backHref = require('../views/activity_view').getViewDetailsAction(tc.type, { id, author: tip.author, content: tc }); + ctx.body = await require('../views/main_views').versionsView({ versions, backHref: backHref === '/activity' ? '/activity' : backHref }); + }) .get("/thread/:message", async (ctx) => { const { message } = ctx.params; - if (checkMod(ctx, 'blogsMod')) { - const target = await blogModel.blogHrefFor(message).catch(() => null); - if (target) { ctx.redirect(target); return; } - } + const target = await commentLocationHref(message); + if (target && (!target.startsWith('/blogs/') || checkMod(ctx, 'blogsMod'))) { ctx.redirect(target); return; } const thread = async (message) => { const all = (await post.fromThread(message)) || []; const hv = await hiddenView(); @@ -10619,7 +11049,7 @@ router }) .get("/wallet/qr/:address", async (ctx) => { if (!checkMod(ctx, 'walletMod')) { ctx.status = 404; ctx.body = ''; return; } - const address = decodeURIComponent(ctx.params.address || ''); + const address = paramId(ctx.params.address); if (!ECO_ADDRESS_RE.test(address)) { ctx.status = 404; ctx.body = ''; return; } try { const QRCode = require('../server/node_modules/qrcode'); @@ -10767,7 +11197,7 @@ router } catch (_) { block = null; } } await enrichItemLifetime(transfer, { author: transfer.from }); - const comments = await getVoteComments(transfer.id); + const comments = await commentsFor(transfer.id); const fav = await contentFavorites.getFavoriteSet('transfers'); await warmAuthorNames([transfer, { author: transfer.from }, { author: transfer.to }], comments); const singleCensus = await censusOf('transfers', () => transfersModel.listAll('all', getViewerId())).catch(() => []); @@ -11294,7 +11724,7 @@ router .post("/comment/:message", koaBody(), async (ctx) => { let decodedMessage; try { - decodedMessage = decodeURIComponent(ctx.params.message); + decodedMessage = paramId(ctx.params.message); } catch { decodedMessage = ctx.params.message; } @@ -11470,6 +11900,7 @@ router ctx.state.inlineError = publishErrorText(err); ctx.body = await forumView([], 'create', { draft: { category, title: stripDangerousTags(title), text: cleanText, isPrivate } }); }; + if (isBlankText(cleanText)) { await keepForum(new Error('Text required')); return; } if (longText.tooLong(cleanText)) { await keepForum(new Error('Text too long')); return; } try { await forumModel.createForum(category, stripDangerousTags(title), cleanText, isPrivate); } catch (err) { await keepForum(err); return; } @@ -11485,6 +11916,7 @@ router ctx.state.inlineError = publishErrorText(err); ctx.body = await forumPageBody(ctx, ctx.params.id, null, { draft: { message: cleanedMsg, parentId: parentId || null } }); }; + if (isBlankText(cleanedMsg)) { await keepReply(new Error('Text required')); return; } if (longText.tooLong(cleanedMsg)) { await keepReply(new Error('Text too long')); return; } const mentions = await extractMentions(cleanedMsg); let posted = null; @@ -11618,7 +12050,8 @@ router ctx.redirect('/trending'); return; } - if (target?.content?.opinions_inhabitants?.includes(voterId)) { + if (target?.content && !target.content.opinions && !require('./opinion_categories').opinable(target.content)) return actionFail(ctx); + if (target?.content?.opinions_inhabitants?.includes(voterId) || (await opinionsModel.tallyOf([contentId])).opinions_inhabitants.includes(voterId)) { return failWith(ctx, 'opinionAlreadyGiven', '/trending'); } await trendingModel.createVote(contentId, category); ctx.redirect('/trending'); @@ -11632,7 +12065,8 @@ router ctx.redirect('/opinions'); return; } - if (target?.content?.opinions_inhabitants?.includes(voterId)) { + if (target?.content && !target.content.opinions && !require('./opinion_categories').opinable(target.content)) return actionFail(ctx); + if (target?.content?.opinions_inhabitants?.includes(voterId) || (await opinionsModel.tallyOf([contentId])).opinions_inhabitants.includes(voterId)) { return failWith(ctx, 'opinionAlreadyGiven', '/opinions'); } await opinionsModel.createVote(contentId, category); ctx.redirect('/opinions'); @@ -11661,7 +12095,7 @@ router catch (err) { if (/too long|too short|required/i.test(String(err && err.message))) { const t = require('../views/main_views').i18n; - ctx.state.inlineError = /too long/i.test(String(err.message)) ? t.publishTooLong : t.publishTooShort; + ctx.state.inlineError = /too long/i.test(String(err.message)) ? t.publishTooLong : /required/i.test(String(err.message)) ? t.publishEmpty : t.publishTooShort; ctx.status = 400; ctx.body = feedCreateView({ text, media: media || '', clearnet: String(ctx.request.body?.clearnet || '') === '1', mastodon: fediverseModel.hasAccount(), mastodonOn: String(ctx.request.body?.mastodon || '') === '1' }); return; @@ -11691,7 +12125,7 @@ router const msg = await new Promise((res) => ssb.get(id, (e, m) => res(e ? null : m))); const c = msg && msg.content; const owner = c && typeof c === 'object' ? (c.author || c.seller || c.organizer || msg.author) : null; - if (!msg || typeof c !== 'object' || c.tribeId || c.encrypted === true || tribeCrypto.isTribeMsg(c) || (msg.author !== ssb.id && owner !== ssb.id)) { failWith(ctx, 'actionFailed'); return; } + if (!msg || typeof c !== 'object' || c.tribeId || c.encrypted === true || cryptoModel.isSealedMsg(c) || (msg.author !== ssb.id && owner !== ssb.id)) { failWith(ctx, 'actionFailed'); return; } if (on && CLEARNET_REACH_OK[kind]) { const getters = { rooms: () => roomsModel.getRoomById(id), maps: () => mapsModel.getMapById(id, getViewerId()), calendars: () => calendarsModel.getCalendarById(id) }; const current = getters[kind] ? await getters[kind]().catch(() => null) : null; @@ -11747,14 +12181,14 @@ router .post("/bookmarks/create", koaBody(), async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body; - const clearnetCreated = await bookmarksModel.createBookmark(stripDangerousTags(b.url), b.tags, stripDangerousTags(b.description), b.lastVisit); + const clearnetCreated = await bookmarksModel.createBookmark(stripDangerousTags(b.url), b.tags, stripDangerousTags(b.description), b.lastVisit, stripDangerousTags(b.title || '')); await applyClearnetChoice(ctx, 'bookmarks', clearnetCreated); ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=all', ['/bookmarks'])); }) .post("/bookmarks/update/:id", koaBody(), async (ctx) => { if (!checkMod(ctx, 'bookmarksMod')) { ctx.redirect('/modules'); return; } const b = ctx.request.body; - await bookmarksModel.updateBookmarkById(ctx.params.id, { url: stripDangerousTags(b.url), tags: b.tags, description: stripDangerousTags(b.description), lastVisit: b.lastVisit }); + await bookmarksModel.updateBookmarkById(ctx.params.id, { url: stripDangerousTags(b.url), title: b.title !== undefined ? stripDangerousTags(b.title) : undefined, tags: b.tags, description: stripDangerousTags(b.description), lastVisit: b.lastVisit }); await applyClearnetChoice(ctx, 'bookmarks', ctx.params.id); ctx.redirect(safeReturnTo(ctx, '/bookmarks?filter=mine', ['/bookmarks'])); }) @@ -11765,14 +12199,14 @@ router .post("/bookmarks/:bookmarkId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'bookmarks', 'bookmarkId')) .post("/images/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } - const blob = await handleBlobUpload(ctx, 'image'), b = ctx.request.body; + const blob = await handleBlobUpload(ctx, 'image', null, 'image'), b = ctx.request.body; const clearnetCreated = await imagesModel.createImage(blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), stripDangerousTags(b.mapUrl || ""), formLicense(b)); await applyClearnetChoice(ctx, 'images', clearnetCreated); ctx.redirect(safeReturnTo(ctx, '/images?filter=all', ['/images'])); }) .post("/images/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'imagesMod')) { ctx.redirect('/modules'); return; } - const b = ctx.request.body, blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : null; + const b = ctx.request.body, blob = ctx.request.files?.image ? await handleBlobUpload(ctx, 'image', null, 'image') : null; await imagesModel.updateImageById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), stripDangerousTags(b.mapUrl || ""), formLicense(b, { keep: true })); await applyClearnetChoice(ctx, 'images', ctx.params.id); ctx.redirect(safeReturnTo(ctx, '/images?filter=mine', ['/images'])); @@ -11799,6 +12233,7 @@ router }) .post("/maps/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'mapsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const target = await mapsModel.getMapById(ctx.params.id, getViewerId()).catch(() => null); if (target && target.tribeId) { const t = await tribesModel.getTribeById(target.tribeId).catch(() => null); @@ -11892,20 +12327,22 @@ router if (fromBlob && !uploadedSize) { const generated = await buildTorrentFromBlob(fromBlob, title || ctx.request.body?.fromName, [resolveExternalBaseUrl(ctx), clearnetPublicBase()]).catch(() => null); if (!generated) { sendErrorPage(ctx, require('../views/main_views').i18n.torrentFromContentMissing, { status: 400, to: '/torrents?filter=create', keep: { fromBlob, name: String(title || '') } }); return; } - await torrentsModel.createTorrent(generated.markdown, stripDangerousTags(tags), stripDangerousTags(title || generated.fileName), stripDangerousTags(description), generated.size, cleanTribeId, { source: fromBlob, sourceName: generated.fileName, sourceMime: generated.mime }); + await torrentsModel.createTorrent(generated.markdown, stripDangerousTags(tags), stripDangerousTags(title || generated.fileName), stripDangerousTags(description), generated.size, cleanTribeId, { source: fromBlob, sourceName: generated.fileName, sourceMime: generated.mime }, { infoHash: generated.infoHash, license: formLicense(ctx.request.body) }); ctx.redirect(cleanTribeId ? `/tribe/${encodeURIComponent(cleanTribeId)}?section=torrents` : '/torrents?filter=mine'); return; } if (cleanTribeId && uploadedSize > 0) { const torrentFile = Array.isArray(uploaded) ? uploaded[0] : uploaded; + const parsed = await readTorrentUpload(torrentFile); const enc = await encryptUploadForTribe(torrentFile, { filename: `${String(title || torrentFile.originalFilename || 'download').replace(/\.torrent$/i, '')}.torrent`, mime: 'application/x-bittorrent' }); - await torrentsModel.createTorrent(enc.markdown, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), enc.size, cleanTribeId, { cipher: enc.cipher }); + await torrentsModel.createTorrent(enc.markdown, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), parsed.length || enc.size, cleanTribeId, { cipher: enc.cipher }, { infoHash: parsed.v2 ? '' : parsed.infoHash, license: formLicense(ctx.request.body) }); ctx.redirect(`/tribe/${encodeURIComponent(cleanTribeId)}?section=torrents`); return; } - const blob = await handleBlobUpload(ctx, 'torrent'); - const fileSize = uploadedSize; - const clearnetCreated = await torrentsModel.createTorrent(blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), fileSize, cleanTribeId); + const parsed = uploadedSize ? await readTorrentUpload(Array.isArray(uploaded) ? uploaded[0] : uploaded) : null; + const blob = await handleBlobUpload(ctx, 'torrent', null, 'torrent'); + const fileSize = (parsed && parsed.length) || uploadedSize; + const clearnetCreated = await torrentsModel.createTorrent(blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), fileSize, cleanTribeId, null, { infoHash: parsed && !parsed.v2 ? parsed.infoHash : '', license: formLicense(ctx.request.body) }); if (!cleanTribeId) await applyClearnetChoice(ctx, 'torrents', clearnetCreated); ctx.redirect(cleanTribeId ? `/tribe/${encodeURIComponent(cleanTribeId)}?section=torrents` : safeReturnTo(ctx, '/torrents?filter=all', ['/torrents'])); }) @@ -11919,13 +12356,19 @@ router const { tags, title, description } = ctx.request.body; const rawT = ctx.request.files?.torrent; const newT = Array.isArray(rawT) ? rawT[0] : rawT; - let blob = null, extra = null; - if (newT && Number(newT.size) > 0 && target && target.tribeId) { - const enc = await encryptUploadForTribe(newT, { filename: `${String(title || target.title || 'download').replace(/\.torrent$/i, '')}.torrent`, mime: 'application/x-bittorrent' }); - blob = enc.markdown; - extra = { cipher: enc.cipher }; - } else if (newT && Number(newT.size) > 0) { - blob = await handleBlobUpload(ctx, 'torrent'); + let blob = null; + const license = formLicense(ctx.request.body, { keep: true }); + const extra = license !== undefined ? { license } : {}; + if (newT && Number(newT.size) > 0) { + const parsed = await readTorrentUpload(newT); + Object.assign(extra, { size: parsed.length, infoHash: parsed.v2 ? '' : parsed.infoHash }); + if (target && target.tribeId) { + const enc = await encryptUploadForTribe(newT, { filename: `${String(title || target.title || 'download').replace(/\.torrent$/i, '')}.torrent`, mime: 'application/x-bittorrent' }); + blob = enc.markdown; + extra.cipher = enc.cipher; + } else { + blob = await handleBlobUpload(ctx, 'torrent', null, 'torrent'); + } } await torrentsModel.updateTorrentById(ctx.params.id, blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), extra); await applyClearnetChoice(ctx, 'torrents', ctx.params.id); @@ -11961,10 +12404,12 @@ router try { const ft = await FileType.fromFile(uploaded.filepath); if (ft && ft.mime) mime = ft.mime; } catch (_) {} const fileName = String(uploaded.originalFilename || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').trim().slice(0, 120); let stored; - try { stored = await nextcloudModel.upload({ filePath: uploaded.filepath, size: Number(uploaded.size), name: fileName }); } + await stripFileMetadata(uploaded.filepath, mime); + const cleanSize = await fs.promises.stat(uploaded.filepath).then(st => st.size).catch(() => Number(uploaded.size)); + try { stored = await nextcloudModel.upload({ filePath: uploaded.filepath, size: cleanSize, name: fileName }); } catch (err) { fediverseFail(ctx, err && err.message ? err.message : 'nextcloudErrUpload', '/files?filter=create'); return; } finally { try { fs.unlinkSync(uploaded.filepath); } catch (_) {} } - const created = await filesModel.createLink(stored.url, stripDangerousTags(tags), stripDangerousTags(title || fileName), stripDangerousTags(description), stored.size, { mime, fileName: stored.name }); + const created = await filesModel.createLink(stored.url, stripDangerousTags(tags), stripDangerousTags(title || fileName), stripDangerousTags(description), stored.size, { mime, fileName: stored.name, license: formLicense(ctx.request.body) }); await applyClearnetChoice(ctx, 'files', created); ctx.redirect(safeReturnTo(ctx, '/files?filter=all', ['/files'])); }) @@ -11994,20 +12439,21 @@ router (mime.startsWith('image/') && modOn('imagesMod') && 'images') || (mime.startsWith('audio/') && modOn('audiosMod') && 'audios') || (mime.startsWith('video/') && modOn('videosMod') && 'videos') || - (mime === 'application/pdf' && modOn('documentsMod') && 'documents') || null); + (mime === 'application/pdf' && modOn('docsMod') && 'documents') || null); if (handOver) { const cleanTitle = stripDangerousTags(title || fileName), cleanTags = stripDangerousTags(tags), cleanDesc = stripDangerousTags(description); - const handed = handOver === 'images' ? await imagesModel.createImage(blob, cleanTags, cleanTitle, cleanDesc, '') - : handOver === 'audios' ? await audiosModel.createAudio(blob, cleanTags, cleanTitle, cleanDesc, '') - : handOver === 'videos' ? await videosModel.createVideo(blob, cleanTags, cleanTitle, cleanDesc, '') - : await documentsModel.createDocument(blob, cleanTags, cleanTitle, cleanDesc); + const license = formLicense(ctx.request.body); + const handed = handOver === 'images' ? await imagesModel.createImage(blob, cleanTags, cleanTitle, cleanDesc, '', license) + : handOver === 'audios' ? await audiosModel.createAudio(blob, cleanTags, cleanTitle, cleanDesc, '', license) + : handOver === 'videos' ? await videosModel.createVideo(blob, cleanTags, cleanTitle, cleanDesc, '', license) + : await documentsModel.createDocument(blob, cleanTags, cleanTitle, cleanDesc, license); await applyClearnetChoice(ctx, handOver, handed); ctx.redirect(`/${handOver}?filter=mine`); return; } const fileTitle = stripDangerousTags(title || fileName), fileTags = stripDangerousTags(tags), fileDesc = stripDangerousTags(description); const linked = await createTorrentForFile(ctx, blob, { title: fileName || fileTitle, tags: fileTags, description: fileDesc, tribeId: cleanTribeId }); - const clearnetCreated = await filesModel.createFile(blob, fileTags, fileTitle, fileDesc, Number(uploaded.size) || 0, cleanTribeId, { mime, fileName, ...(linked || {}) }); + const clearnetCreated = await filesModel.createFile(blob, fileTags, fileTitle, fileDesc, Number(uploaded.size) || 0, cleanTribeId, { mime, fileName, license: formLicense(ctx.request.body), ...(linked || {}) }); if (!cleanTribeId) await applyClearnetChoice(ctx, 'files', clearnetCreated); ctx.redirect(cleanTribeId ? `/tribe/${encodeURIComponent(cleanTribeId)}?section=files` : safeReturnTo(ctx, '/files?filter=all', ['/files'])); }) @@ -12040,6 +12486,8 @@ router const linked = await createTorrentForFile(ctx, blob, { title: fileName || stripDangerousTags(title), tags: stripDangerousTags(tags), description: stripDangerousTags(description), tribeId: target && target.tribeId }); extra = { mime, fileName, size: Number(newFile.size) || 0, ...(linked || {}) }; } + const license = formLicense(ctx.request.body, { keep: true }); + if (license !== undefined) extra = { ...(extra || {}), license }; await filesModel.updateFileById(ctx.params.id, blob, stripDangerousTags(tags), stripDangerousTags(title), stripDangerousTags(description), extra); await applyClearnetChoice(ctx, 'files', ctx.params.id); ctx.redirect(safeReturnTo(ctx, '/files?filter=mine', ['/files'])); @@ -12063,13 +12511,15 @@ router .post("/videos/favorites/remove/:id", koaBody(), async ctx => favAction(ctx, 'videos', 'remove')) .post("/videos/:videoId/comments", koaBodyMiddleware, async ctx => commentAction(ctx, 'videos', 'videoId')) .post("/documents/create", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - const docBlob = await handleBlobUpload(ctx, "document"), b = ctx.request.body; + if (!checkMod(ctx, 'docsMod')) { ctx.redirect('/modules'); return; } + const docBlob = await handleBlobUpload(ctx, "document", null, "document"), b = ctx.request.body; const clearnetCreated = await documentsModel.createDocument(docBlob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), formLicense(b)); await applyClearnetChoice(ctx, 'documents', clearnetCreated); ctx.redirect(safeReturnTo(ctx, "/documents?filter=all", ["/documents"])); }) .post("/documents/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { - const b = ctx.request.body, blob = ctx.request.files?.document ? await handleBlobUpload(ctx, "document") : null; + if (!checkMod(ctx, 'docsMod')) { ctx.redirect('/modules'); return; } + const b = ctx.request.body, blob = ctx.request.files?.document ? await handleBlobUpload(ctx, "document", null, "document") : null; await documentsModel.updateDocumentById(ctx.params.id, blob, b.tags, stripDangerousTags(b.title), stripDangerousTags(b.description), formLicense(b, { keep: true })); await applyClearnetChoice(ctx, 'documents', ctx.params.id); ctx.redirect(safeReturnTo(ctx, "/documents?filter=mine", ["/documents"])); @@ -12448,6 +12898,7 @@ router ctx.redirect(safeReturnTo(ctx, '/tasks?filter=mine', ['/tasks'])); }) .post('/tasks/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body, tags = Array.isArray(b.tags) ? b.tags.filter(Boolean) : (typeof b.tags === 'string' ? b.tags.split(',').map(t => t.trim()).filter(Boolean) : []); const media = await readGalleryUpload(ctx); let current = []; @@ -12636,6 +13087,7 @@ router }) .post('/logistics/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { if (!checkMod(ctx, 'logisticsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body || {}; try { const blobMarkdown = await handleBlobUpload(ctx, 'blob'); @@ -12698,7 +13150,7 @@ router const b = ctx.request.body || {}; try { const cover = await handleBlobUpload(ctx, 'cover'); - const res = await podcastsModel.createChannel({ title: stripDangerousTags(String(b.title || '')), description: stripDangerousTags(String(b.description || '')), category: b.category, cover, tags: b.tags || '' }); + const res = await podcastsModel.createChannel({ title: stripDangerousTags(String(b.title || '')), description: stripDangerousTags(String(b.description || '')), category: b.category, cover, tags: b.tags || '', license: formLicense(ctx.request.body) }); await applyClearnetChoice(ctx, 'podcasts', res); ctx.redirect(`/podcasts/${encodeURIComponent(res.key)}`); } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 400 }); } @@ -12708,7 +13160,7 @@ router const b = ctx.request.body || {}; try { const cover = await handleBlobUpload(ctx, 'cover'); - const res = await podcastsModel.updateChannel(ctx.params.id, { title: stripDangerousTags(String(b.title || '')), description: stripDangerousTags(String(b.description || '')), category: b.category, cover, tags: b.tags || '' }); + const res = await podcastsModel.updateChannel(ctx.params.id, { title: stripDangerousTags(String(b.title || '')), description: stripDangerousTags(String(b.description || '')), category: b.category, cover, tags: b.tags || '', license: formLicense(b, { keep: true }) }); await applyClearnetChoice(ctx, 'podcasts', res); ctx.redirect(`/podcasts/${encodeURIComponent(res.rootId)}`); } catch (e) { sendErrorPage(ctx, e.message || String(e), { status: 403 }); } @@ -12768,6 +13220,7 @@ router }) .post('/campaigns/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { if (!checkMod(ctx, 'campaignsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body || {}; try { let text = stripDangerousTags(String(b.text || '')); @@ -12869,28 +13322,18 @@ router return; } const reportAttachment = await handleBlobUpload(ctx, 'blob'); - await reportsModel.createReport(stripDangerousTags(b.title), stripDangerousTags(b.description) + (reportAttachment || ''), b.category, null, b.tags, b.severity, { - stepsToReproduce: stripDangerousTags(b.stepsToReproduce), expectedBehavior: stripDangerousTags(b.expectedBehavior), actualBehavior: stripDangerousTags(b.actualBehavior), environment: stripDangerousTags(b.environment), reproduceRate: b.reproduceRate, - problemStatement: stripDangerousTags(b.problemStatement), userStory: stripDangerousTags(b.userStory), acceptanceCriteria: stripDangerousTags(b.acceptanceCriteria), - whatHappened: stripDangerousTags(b.whatHappened), reportedUser: b.reportedUser, evidenceLinks: stripDangerousTags(b.evidenceLinks), - contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate) - }, { images: draftImages, video: media.clip || '' }); + await reportsModel.createReport(stripDangerousTags(b.title), stripDangerousTags(b.description) + (reportAttachment || ''), b.category, null, b.tags, b.severity, { images: draftImages, video: media.clip || '' }); ctx.redirect('/reports'); }) .post('/reports/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async ctx => { + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body; const media = await readGalleryUpload(ctx); let current = []; try { current = (await reportsModel.getReportById(ctx.params.id)).images || []; } catch (_) { current = []; } await reportsModel.updateReportById(ctx.params.id, { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), category: b.category, tags: b.tags, severity: b.severity, - ...galleryPatch(media, current), - template: { - stepsToReproduce: stripDangerousTags(b.stepsToReproduce), expectedBehavior: stripDangerousTags(b.expectedBehavior), actualBehavior: stripDangerousTags(b.actualBehavior), environment: stripDangerousTags(b.environment), reproduceRate: b.reproduceRate, - problemStatement: stripDangerousTags(b.problemStatement), userStory: stripDangerousTags(b.userStory), acceptanceCriteria: stripDangerousTags(b.acceptanceCriteria), - whatHappened: stripDangerousTags(b.whatHappened), reportedUser: stripDangerousTags(b.reportedUser), evidenceLinks: stripDangerousTags(b.evidenceLinks), - contentLocation: stripDangerousTags(b.contentLocation), whyInappropriate: stripDangerousTags(b.whyInappropriate) - } + ...galleryPatch(media, current) }); if (media.isMediaAction) { ctx.redirect(`/reports/edit/${encodeURIComponent(ctx.params.id)}`); return; } ctx.redirect('/reports?filter=mine'); @@ -12948,6 +13391,7 @@ router ctx.redirect(safeReturnTo(ctx, '/events?filter=mine', ['/events'])); }) .post('/events/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body, existing = await eventsModel.getEventById(ctx.params.id); const media = await readGalleryUpload(ctx); if (clearnetConflict(b) && !media.isMediaAction) return renderClearnetConflict(ctx, () => eventView([{ ...clearnetDraft(b, existing), id: existing.id, images: existing.images || [] }], 'edit', existing.id, b.returnTo)); @@ -12984,6 +13428,58 @@ router ctx.redirect(safeReturnTo(ctx, `/events/${encodeURIComponent(ctx.params.id)}`, ['/events'])); }) .post('/events/:eventId/comments', koaBodyMiddleware, async ctx => commentAction(ctx, 'events', 'eventId')) + .post('/wiki/opinions/:id/:category', koaBody(), async ctx => { + if (!checkMod(ctx, 'wikiMod')) { ctx.redirect('/modules'); return; } + const page = await wikiModel.getPage(ctx.params.id, {}).catch(() => null); + if (!OPINABLE.wiki(page)) return actionFail(ctx); + return opineOn(ctx, page.id, [page.id, ...(page.versions || []).map(v => v.key)], `/wiki/${encodeURIComponent(page.id)}`); + }) + .post('/maps/opinions/:id/:category', koaBody(), async ctx => { + if (!checkMod(ctx, 'mapsMod')) { ctx.redirect('/modules'); return; } + const map = await mapsModel.getMapById(ctx.params.id, getViewerId()).catch(() => null); + if (!OPINABLE.maps(map)) return actionFail(ctx); + return opineOn(ctx, map.rootId || map.key, [map.rootId, map.key], `/maps/${encodeURIComponent(map.rootId || map.key)}`); + }) + .post('/industry/blueprint/opinions/:id/:category', koaBody(), async ctx => { + if (!checkMod(ctx, 'industryMod')) { ctx.redirect('/modules'); return; } + const bp = await industryModel.getBlueprint(ctx.params.id).catch(() => null); + if (!OPINABLE.industry(bp)) return actionFail(ctx); + return opineOn(ctx, bp.rootId || bp.id, [bp.rootId, bp.id], `/industry/blueprint/${encodeURIComponent(bp.rootId || bp.id)}`); + }) + .post('/jobs/opinions/:id/:category', koaBody(), async ctx => { + if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; } + const job = await jobsModel.getJobById(ctx.params.id, getViewerId()).catch(() => null); + if (!OPINABLE.jobs(job)) return actionFail(ctx); + return opineOn(ctx, job.rootId || job.id, [job.rootId, job.id], `/jobs/${encodeURIComponent(job.rootId || job.id)}`); + }) + .post('/wiki/:id/comments', koaBodyMiddleware, async ctx => { + const page = await wikiModel.getPage(ctx.params.id, {}).catch(() => null); + if (!COMMENTABLE.wiki(page)) return actionFail(ctx); + ctx.params.id = page.id; + return commentAction(ctx, 'wiki', 'id'); + }) + .post('/maps/:mapId/comments', koaBodyMiddleware, async ctx => { + const map = await mapsModel.getMapById(ctx.params.mapId, getViewerId()).catch(() => null); + if (!COMMENTABLE.maps(map)) return actionFail(ctx); + ctx.params.mapId = map.rootId || map.key; + return commentAction(ctx, 'maps', 'mapId'); + }) + .post('/school/course/:id/comments', koaBodyMiddleware, async ctx => { + const course = await schoolModel.getCourseById(ctx.params.id, getViewerId()).catch(() => null); + if (!COMMENTABLE.school(course)) return actionFail(ctx); + ctx.params.id = course.rootId || course.id; + return commentAction(ctx, 'school', 'id'); + }) + .post('/industry/blueprint/:id/comments', koaBodyMiddleware, async ctx => { + const bp = await industryModel.getBlueprint(ctx.params.id).catch(() => null); + if (!COMMENTABLE.industry(bp)) return actionFail(ctx); + ctx.params.id = bp.rootId || bp.id; + return commentAction(ctx, 'industry', 'id'); + }) + .post('/parliament/comments/:id', koaBodyMiddleware, async ctx => { + if (!COMMENTABLE.parliament(await commentsModel.rootTypeOf(ctx.params.id).catch(() => null))) return actionFail(ctx); + return commentAction(ctx, 'parliament', 'id'); + }) .post('/votes/create', koaBody(), async ctx => { const b = ctx.request.body, defaultOptions = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED']; const parsedOptions = b.options ? b.options.split(',').map(o => o.trim()).filter(Boolean) : defaultOptions; @@ -13267,6 +13763,7 @@ router }) .post("/market/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'marketMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; if (clearnetConflict(ctx.request.body)) { const existing = await marketModel.getItemById(ctx.params.id).catch(() => null); if (!existing) { ctx.redirect('/market'); return; } @@ -13311,6 +13808,19 @@ router if (item.item_type === "auction") ctx.throw(400, "Auction items are acquired via bids"); if (String(item.status || "").toUpperCase() === "SOLD") ctx.throw(400, "Item already sold"); if (Number(item.stock || 0) <= 0) ctx.throw(400, "Out of stock"); + const b = ctx.request.body || {}; + const deliveryAddress = stripDangerousTags(String(b.deliveryAddress || "")).trim(); + if (item.includesShipping && !deliveryAddress) { + sendErrorPage(ctx, require('../views/main_views').i18n.shopBuyDeliveryRequired, { status: 400, exact: true }); + return; + } + if (deliveryAddress || String(b.contact || "").trim() || String(b.notes || "").trim()) { + await marketModel.createOrder(ctx.params.id, { + deliveryAddress, + contact: stripDangerousTags(String(b.contact || "")).trim(), + notes: stripDangerousTags(String(b.notes || "")).trim() + }); + } try { await notifyBot("MARKET_SOLD", [item.seller], `${await actorLink(getViewerId())} has bought your item: [${item.title}](/market/${encodeURIComponent(ctx.params.id)}) for: ${item.price} ECO`); } catch (_) {} @@ -13387,6 +13897,7 @@ router }) .post('/housing/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'housingMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body if (clearnetConflict(b)) { let existing = null @@ -13497,6 +14008,7 @@ router }) .post('/jobs/update/:id', koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'jobsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; if (clearnetConflict(ctx.request.body)) { const existing = await jobsModel.getJobById(ctx.params.id, getViewerId()).catch(() => null); if (!existing) { ctx.redirect('/jobs'); return; } @@ -13647,6 +14159,7 @@ router }) .post("/shops/product/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'shopsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const b = ctx.request.body, imageBlob = (ctx.request.files?.image ? await handleBlobUpload(ctx, 'image') : undefined) || undefined; const patch = { title: stripDangerousTags(b.title), description: stripDangerousTags(b.description), price: b.price, stock: b.stock, featured: [].concat(b.featured).includes("1") }; if (imageBlob !== undefined) patch.image = imageBlob; @@ -14107,6 +14620,15 @@ router if (error) { sendErrorPage(ctx, roomErrorMessage(error), { status: 400 }); return; } ctx.redirect(`/rooms/${encodeURIComponent(room.rootId)}`); }) + .post("/rooms/leave/:id", koaBody(), async (ctx) => { + if (config.public || !checkMod(ctx, 'roomsMod')) { ctx.redirect('/modules'); return; } + const room = await roomsModel.getRoomById(ctx.params.id).catch(() => null); + if (!room) { ctx.redirect('/rooms'); return; } + const current = await roomsModel.liveState().catch(() => null); + if (current && current.ref === room.rootId) { await roomsModel.leave().catch(() => {}); await syncRoomBanner(); } + await roomsModel.leaveRoomMembership(room.rootId).catch(() => {}); + ctx.redirect('/rooms?filter=all'); + }) .post("/rooms/leave", koaBody(), async (ctx) => { if (!checkMod(ctx, 'roomsMod')) { ctx.redirect('/modules'); return; } await roomsModel.leave(); @@ -14132,8 +14654,8 @@ router if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; } const b = ctx.request.body || {}; const returnTo = streamReturnTo(ctx); - if (String(b.stop) === '1') { - await roomsModel.stream({ stop: true }).catch(() => null); + if (String(b.stop) === '1' || String(b.screen) === '1') { + await roomsModel.stream(String(b.stop) === '1' ? { stop: true } : { screen: true }).catch(() => null); ctx.redirect(returnTo); return; } @@ -14432,6 +14954,7 @@ router }) .post("/projects/update/:id", koaBody({ multipart: true, formidable: { maxFileSize: maxSize } }), async (ctx) => { if (!checkMod(ctx, 'projectsMod')) { ctx.redirect('/modules'); return; } + if (await guardRatedMedia(ctx, ctx.params.id)) return; const id = await projectsModel.getProjectTipId(ctx.params.id), b = ctx.request.body || {}; const image = (ctx.request.files?.image ? await handleBlobUpload(ctx, "image") : undefined) || undefined; const bounties = b.bountiesInput !== undefined ? String(b.bountiesInput).split("\n").filter(Boolean).map(l => { const [t,a,d] = String(l).split("|"); return { title: String(t||"").trim(), amount: parseFloat(a||0)||0, description: String(d||"").trim(), milestoneIndex: null }; }) : undefined; @@ -14548,7 +15071,7 @@ router const backTo = safeReturnTo(ctx, '/activity', ['/']); if (!checkMod(ctx, 'walletMod')) { ctx.redirect(backTo); return; } try { - const feedId = decodeURIComponent(ctx.params.feedId || ''); + const feedId = paramId(ctx.params.feedId); if (!ssbRef.isFeedId(feedId) || String(feedId) === String(getViewerId())) { ctx.redirect(backTo); return; } const { i18n: i18nD } = require('../views/main_views'); const mine = await bankingModel.getUserAddress(getViewerId()).catch(() => null); @@ -14732,11 +15255,11 @@ router const rec = unf.find(x => x && canonicalKey(x.key) === kcanon) || { host: String(host).trim().toLowerCase(), port: Number(port) || 8008, key: kcanon }; if (!validPeerHost(String(rec.host || '')) || !validPeerPort(Number(rec.port) || 8008)) { failWith(ctx, 'peerHostValidation', '/invites'); return; } if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) { pubs.push({ host: rec.host, port: Number(rec.port) || 8008, key: kcanon }); writeJSON(gossipPath, pubs); } + writeJSON(unfollowedPath, unf.filter(x => !(x && canonicalKey(x.key) === kcanon))); const addr = msAddrFrom(rec.host, rec.port, kcanon); try { ssb.conn.remember(addr, { type: "pub", autoconnect: true, key: kcanon }); } catch {} - try { await new Promise(res => ssb.conn.connect(addr, { type: "pub" }, res)); } catch {} - try { await new Promise((res, rej) => ssb.publish({ type: "contact", contact: kcanon, blocking: false }, e => e ? rej(e) : res())); } catch {} - writeJSON(unfollowedPath, unf.filter(x => !(x && canonicalKey(x.key) === kcanon))); + try { ssb.conn.connect(addr, { type: "pub" }, () => {}); } catch {} + try { await new Promise((res, rej) => ssb.publish({ type: "contact", contact: kcanon, following: true, blocking: false }, e => e ? rej(e) : res())); } catch {} ctx.redirect("/invites"); }) .post("/peers/connect", koaBody(), async (ctx) => { @@ -15014,6 +15537,7 @@ router let ssb = null; try { ssb = await cooler.open(); } catch (_) {} const pubs = readJSON(gossipPath); + const imported = new Set(); let added = 0; for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); @@ -15026,6 +15550,7 @@ router const kcanon = canonicalKey('@' + keyCore + '.ed25519'); if (!ssbRef.isFeed(kcanon) || !validPeerHost(String(host).toLowerCase()) || !validPeerPort(port)) continue; const addr = msAddrFrom(host, port, kcanon); + imported.add(kcanon); if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) { pubs.push({ host, port, key: kcanon }); } @@ -15036,6 +15561,7 @@ router } } writeJSON(gossipPath, pubs); + if (imported.size) writeJSON(unfollowedPath, readJSON(unfollowedPath).filter(x => !(x && imported.has(canonicalKey(x.key))))); ctx.redirect("/peers?filter=connections"); }) .post("/settings/ssb-logstream", koaBody(), async (ctx) => { @@ -15087,6 +15613,12 @@ router saveConfig(cfg); ctx.redirect(next === "ainav" ? "/" : next === "chats" ? "/chats" : next === "feed" ? "/feed" : next === "phone" ? "/phone" : next === "rooms" ? "/rooms" : "/settings"); }) + .post("/settings/connections", koaBody(), async (ctx) => { + const cfg = getConfig(); + cfg.connections = require('../configs/config-manager').connectionsOf(ctx.request.body && ctx.request.body.connections); + saveConfig(cfg); + ctx.redirect("/settings#connections"); + }) .post("/settings/lan-broadcasting", koaBody(), async (ctx) => { const enabled = !!(ctx.request.body && (ctx.request.body.lanBroadcasting === 'on' || ctx.request.body.lanBroadcasting === '1' || ctx.request.body.lanBroadcasting === 'true')); const cfg = getConfig(); @@ -15477,6 +16009,15 @@ const middleware = [ await next(); }, async (ctx, next) => { await require('../models/typed_log').requestScope.run({ capped: false, limit: 0, path: ctx.path, query: ctx.querystring, method: ctx.method }, next); }, + async (ctx, next) => { + const store = ctx.method === 'GET' && !COMMENT_INDEX_SKIP_RE.test(ctx.path) ? require('../models/typed_log').requestScope.getStore() : null; + if (store) { + let timer = null; + try { [store.commentCounts, store.opinionTallies] = await Promise.race([Promise.all([commentIndex(), opinionIndexCache.get()]), new Promise((resolve) => { timer = setTimeout(() => resolve([null, null]), COMMENT_INDEX_WAIT_MS); })]); } catch (_) {} + clearTimeout(timer); + } + await next(); + }, async (ctx, next) => { if (ctx.method !== 'POST') return next(); const forget = () => { @@ -15487,6 +16028,7 @@ const middleware = [ clearnetIndexCache = { ts: 0, data: null, building: null }; clearnetBlobCache = { ts: 0, set: null, forcedAt: clearnetBlobCache.forcedAt }; clearnetTribesCache = { at: 0, data: null }; + forgetEngagementIndexes(); }; forget(); try { await next(); } finally { forget(); await settleLogWrites(); } @@ -15564,6 +16106,10 @@ const middleware = [ sendErrorPage(ctx, i18n.fileTooLargeMessage, { title: i18n.fileTooLargeTitle, status: 413 }); return; } + if (err.name === 'WrongFileTypeError') { + sendErrorPage(ctx, i18n.fileWrongType, { status: 415 }); + return; + } const moduleHome = moduleHomeFor(ctx); if (moduleHome && isMissingContentError(err)) { ctx.redirect(moduleHome); return; } sendErrorPage(ctx, err.message || 'Internal Server Error', { status: err.status || 500 }); @@ -15575,6 +16121,11 @@ const middleware = [ } await next(); }, + async (ctx, next) => { + await next(); + if (ctx.method !== 'GET' || typeof ctx.body !== 'string' || !/<(?:video|audio)\b/.test(ctx.body)) return; + try { ctx.body = await markPendingMedia(ctx.body); } catch (_) {} + }, // la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md) require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about, backupModel }), routes, diff --git a/src/backend/blobHandler.js b/src/backend/blobHandler.js index cb2c3c2..2e4b38d 100644 --- a/src/backend/blobHandler.js +++ b/src/backend/blobHandler.js @@ -230,7 +230,141 @@ const stripPdfMetadata = (buffer) => { } }; +const STRIP_CHUNK = 64 * 1024; +const STRIP_IN_MEMORY_MAX = 256 * 1024 * 1024; +const FFMPEG_STRIP_MS = 120000; +const MP4_LIKE = /mp4|quicktime|m4a|m4v|3gpp/; +const MP4_METADATA_BOXES = new Set(['udta', 'meta']); +const MP4_WALKED_BOXES = new Set(['moov', 'trak']); +const XMP_UUID = Buffer.from('be7acfcb97a942e89c71999491e3afac', 'hex'); +const WAV_METADATA_CHUNKS = new Set(['LIST', 'id3 ', 'ID3 ', 'bext', 'iXML', '_PMX', 'XMP ']); +const FLAC_METADATA_BLOCKS = new Set([4, 6]); +const FLAC_PADDING = 1; +const ID3V1_NO_GENRE = 255; +const FFMPEG_FORMATS = { 'video/webm': 'webm', 'audio/webm': 'webm', 'video/x-matroska': 'matroska', 'audio/x-matroska': 'matroska', 'video/ogg': 'ogg', 'audio/ogg': 'ogg', 'audio/opus': 'ogg', 'video/x-msvideo': 'avi' }; + +const readAt = async (fh, pos, len) => { + const buf = Buffer.alloc(len); + const { bytesRead } = await fh.read(buf, 0, len, pos); + return buf.subarray(0, bytesRead); +}; + +const zeroAt = async (fh, pos, len) => { + const zeros = Buffer.alloc(Math.min(len, STRIP_CHUNK)); + for (let done = 0; done < len; done += zeros.length) await fh.write(zeros, 0, Math.min(zeros.length, len - done), pos + done); +}; + +const stripMp4 = async (fh, start, end, depth = 0) => { + let pos = start; + while (pos + 8 <= end) { + const head = await readAt(fh, pos, 16); + if (head.length < 8) return; + let size = head.readUInt32BE(0); + let header = 8; + const type = head.toString('latin1', 4, 8); + if (size === 1) { if (head.length < 16) return; size = Number(head.readBigUInt64BE(8)); header = 16; } + else if (size === 0) size = end - pos; + if (size < header || pos + size > end) return; + const isXmp = type === 'uuid' && (await readAt(fh, pos + header, XMP_UUID.length)).equals(XMP_UUID); + if (MP4_METADATA_BOXES.has(type) || isXmp) { + await fh.write(Buffer.from('free', 'latin1'), 0, 4, pos + 4); + await zeroAt(fh, pos + header, size - header); + } + else if (MP4_WALKED_BOXES.has(type) && depth < MP4_WALKED_BOXES.size) await stripMp4(fh, pos + header, pos + size, depth + 1); + pos += size; + } +}; + +const stripMp3 = async (fh, size) => { + const head = await readAt(fh, 0, 10); + if (head.length === 10 && head.toString('latin1', 0, 3) === 'ID3') { + const tagSize = ((head[6] & 0x7f) << 21) | ((head[7] & 0x7f) << 14) | ((head[8] & 0x7f) << 7) | (head[9] & 0x7f); + const footer = head[5] & 0x10 ? 10 : 0; + await fh.write(Buffer.from([0]), 0, 1, 5); + await zeroAt(fh, 10, Math.min(tagSize + footer, size - 10)); + } + if (size >= 128 && (await readAt(fh, size - 128, 3)).toString('latin1') === 'TAG') { + await zeroAt(fh, size - 125, 125); + await fh.write(Buffer.from([ID3V1_NO_GENRE]), 0, 1, size - 1); + } +}; + +const stripFlac = async (fh, size) => { + if ((await readAt(fh, 0, 4)).toString('latin1') !== 'fLaC') return; + for (let pos = 4; pos + 4 <= size;) { + const head = await readAt(fh, pos, 4); + if (head.length < 4) return; + const last = head[0] & 0x80; + const len = head.readUIntBE(1, 3); + if (FLAC_METADATA_BLOCKS.has(head[0] & 0x7f)) { + await fh.write(Buffer.from([last | FLAC_PADDING]), 0, 1, pos); + await zeroAt(fh, pos + 4, len); + } + if (last) return; + pos += 4 + len; + } +}; + +const stripWav = async (fh, size) => { + const head = await readAt(fh, 0, 12); + if (head.length < 12 || head.toString('latin1', 0, 4) !== 'RIFF' || head.toString('latin1', 8, 12) !== 'WAVE') return; + for (let pos = 12; pos + 8 <= size;) { + const chunk = await readAt(fh, pos, 8); + if (chunk.length < 8) return; + const len = chunk.readUInt32LE(4); + if (WAV_METADATA_CHUNKS.has(chunk.toString('latin1', 0, 4))) { + await fh.write(Buffer.from('JUNK', 'latin1'), 0, 4, pos); + await zeroAt(fh, pos + 8, Math.min(len, size - pos - 8)); + } + pos += 8 + len + (len & 1); + } +}; + +const stripWithFfmpeg = (filepath, format) => new Promise((resolve) => { + const out = `${filepath}.clean`; + const args = ['-v', 'error', '-y', '-i', filepath, '-map', '0', '-map_metadata', '-1', '-c', 'copy', '-f', format, out]; + require('child_process').execFile(process.env.OASIS_FFMPEG || 'ffmpeg', args, { timeout: FFMPEG_STRIP_MS }, async (err) => { + try { + if (err) await promisesFs.unlink(out).catch(() => {}); + else await promisesFs.rename(out, filepath); + } catch (_) {} + resolve(); + }); +}); + +const stripFileMetadata = async (filepath, mime) => { + const m = String(mime || ''); + try { + const { size } = await promisesFs.stat(filepath); + if ((m.startsWith('image/') && m !== 'image/gif') || m === 'application/pdf') { + if (size > STRIP_IN_MEMORY_MAX) return; + const data = await promisesFs.readFile(filepath); + const clean = m === 'application/pdf' ? stripPdfMetadata(data) : await stripImageMetadata(data); + if (clean !== data) await promisesFs.writeFile(filepath, clean); + return; + } + if (!m.startsWith('audio/') && !m.startsWith('video/')) return; + if (FFMPEG_FORMATS[m]) { await stripWithFfmpeg(filepath, FFMPEG_FORMATS[m]); return; } + const fh = await promisesFs.open(filepath, 'r+'); + try { + if (MP4_LIKE.test(m)) await stripMp4(fh, 0, size); + else if (m === 'audio/mpeg') await stripMp3(fh, size); + else if (m === 'audio/flac' || m === 'audio/x-flac') await stripFlac(fh, size); + else if (/wav|wave/.test(m)) await stripWav(fh, size); + } finally { await fh.close(); } + } catch (_) {} +}; + const MAX_BLOB_SIZE = 75 * 1024 * 1024; +const KIND_MIME = { image: /^image\//, audio: /^audio\//, video: /^video\//, document: /^application\/pdf$/, torrent: /^application\/x-bittorrent$/ }; +const looksLikeTorrent = (data) => data.length > 0 && data[0] === 0x64 && data.subarray(0, 4096).includes(Buffer.from('4:info')); + +class WrongFileTypeError extends Error { + constructor(fileName, mime) { + super(`Wrong file type: ${fileName} (${mime})`); + this.name = 'WrongFileTypeError'; + } +} class FileTooLargeError extends Error { constructor(fileName, fileSize) { @@ -253,7 +387,7 @@ const oggMime = (buffer) => { const sealedRef = (sealed, mime, name) => `/blob/sealed/${encodeURIComponent(sealed.manifestBlobId)}?k=${encodeURIComponent(sealed.key)}&t=${encodeURIComponent(mime)}&n=${encodeURIComponent(name)}`; -const storeUploadedFile = async function (blobUpload, seal = null) { +const storeUploadedFile = async function (blobUpload, seal = null, kind = null) { if (!blobUpload || !blobUpload.filepath) return null; let data = await promisesFs.readFile(blobUpload.filepath); @@ -294,11 +428,16 @@ const storeUploadedFile = async function (blobUpload, seal = null) { if (!blob.mime) { blob.mime = 'application/octet-stream'; } + if (blob.mime === 'application/octet-stream' && looksLikeTorrent(data)) blob.mime = 'application/x-bittorrent'; + if (kind && KIND_MIME[kind] && !KIND_MIME[kind].test(blob.mime)) throw new WrongFileTypeError(blob.name, blob.mime); if (blob.mime.startsWith('image/') && blob.mime !== 'image/gif') { data = await stripImageMetadata(data); } else if (blob.mime === 'application/pdf') { data = stripPdfMetadata(data); + } else if (blob.mime.startsWith('audio/') || blob.mime.startsWith('video/')) { + await stripFileMetadata(blobUpload.filepath, blob.mime); + data = await promisesFs.readFile(blobUpload.filepath); } if (typeof seal === 'function') { @@ -324,11 +463,11 @@ const storeUploadedFile = async function (blobUpload, seal = null) { return `\n[${blob.name}](${blob.id})`; }; -const handleBlobUpload = async function (ctx, fileFieldName, seal = null) { +const handleBlobUpload = async function (ctx, fileFieldName, seal = null, kind = null) { if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null; const entry = ctx.request.files[fileFieldName]; const first = Array.isArray(entry) ? entry[0] : entry; - return storeUploadedFile(first, seal); + return storeUploadedFile(first, seal, kind); }; const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) { @@ -507,5 +646,5 @@ const serveBlob = async function (ctx) { sendBlobBuffer(ctx, buffer); }; -module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, stripImageMetadata }; +module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, WrongFileTypeError, stripImageMetadata, stripFileMetadata }; diff --git a/src/backend/content_favorites.js b/src/backend/content_favorites.js index 1d7540c..01a5152 100644 --- a/src/backend/content_favorites.js +++ b/src/backend/content_favorites.js @@ -94,15 +94,6 @@ const assertKind = (kind) => { return k; }; -const lastArg = (args, n) => args[args.length - n]; - -const kindFromArgs = (args) => { - const k = String(lastArg(args, 1) || "").trim(); - return assertKind(k); -}; - -const idFromArgs = (args) => String(lastArg(args, 1) || "").trim(); - exports.getFavoriteSet = async (kind) => { const k = assertKind(kind); const data = await readAll(); @@ -142,15 +133,4 @@ exports.removeFavorite = async (kind, id) => await writeAll(data); }); -exports.getFavoritesSet = async (...args) => exports.getFavoriteSet(kindFromArgs(args)); -exports.addToFavorites = async (...args) => { - const kind = kindFromArgs(args.slice(0, -1)); - const id = idFromArgs(args); - return exports.addFavorite(kind, id); -}; -exports.removeFromFavorites = async (...args) => { - const kind = kindFromArgs(args.slice(0, -1)); - const id = idFromArgs(args); - return exports.removeFavorite(kind, id); -}; exports.storePath = storePath; diff --git a/src/backend/nameCache.js b/src/backend/nameCache.js index 46c4fdd..7ac0d9f 100644 --- a/src/backend/nameCache.js +++ b/src/backend/nameCache.js @@ -17,4 +17,15 @@ const set = (feedId, name, ts) => { const has = (feedId) => !!feedId && cache.has(String(feedId)) const size = () => cache.size -module.exports = { get, set, has, size } +const images = new Map() +const NULL_IMAGE_RE = /^&0{43}=\.sha256$/ + +const image = (feedId) => (feedId && images.get(String(feedId))) || null + +const setImage = (feedId, img) => { + const link = typeof img === 'string' ? img : (img && typeof img.link === 'string' ? img.link : null) + if (!feedId || !link || !link.startsWith('&') || NULL_IMAGE_RE.test(link)) return + images.set(String(feedId), link) +} + +module.exports = { get, set, has, size, image, setImage } diff --git a/src/backend/opinion_categories.js b/src/backend/opinion_categories.js index 0779724..e047f95 100644 --- a/src/backend/opinion_categories.js +++ b/src/backend/opinion_categories.js @@ -20,6 +20,7 @@ const constructive = [ "rejected", "needsSources", "wrong", + "outdated", "lowQuality", "confusing", "misleading", @@ -46,7 +47,7 @@ const all = [...positive, ...constructive, ...moderation]; all.positive = positive; all.constructive = constructive; all.moderation = moderation; -const karmaNeutral = ["adultOnly", "nsfw", "triggering", "duplicate", "offTopic", "needsSources"]; +const karmaNeutral = ["adultOnly", "nsfw", "triggering", "duplicate", "offTopic", "needsSources", "outdated"]; const karmaModeration = [...moderation.filter(c => !karmaNeutral.includes(c)), "propaganda"]; all.karma = { positive, @@ -79,7 +80,19 @@ all.messageTypes = { podcast: 'podcastOpinion', podcastEpisode: 'podcastOpinion', campaign: 'campaignOpinion', - logisticsRoute: 'logisticsOpinion' + logisticsRoute: 'logisticsOpinion', + wikiPage: 'wikiOpinion', + map: 'mapOpinion', + industryBlueprint: 'blueprintOpinion', + job: 'jobOpinion' }; +all.opinable = (c) => !!c && typeof c === 'object' && !!all.messageTypes[c.type] && !c.tribeId && !c.encrypted && !c.encryptedPayload && !c.contentEncrypted + && (c.type !== 'map' || String(c.mapType || '').toUpperCase() === 'OPEN') + && (c.type !== 'job' || String(c.visibility || 'PUBLIC').toUpperCase() !== 'HIDDEN') + && (c.type !== 'schoolCourse' || String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'); + +all.hasOpinions = (x) => Object.values((x && x.opinions) || {}).some(n => (Number(n) || 0) > 0); +all.MEDIA_LOCKED = 'Cannot replace the file after it has received opinions.'; +all.assertMediaKept = (item, replacing) => { if (replacing && all.hasOpinions(item)) throw new Error(all.MEDIA_LOCKED); }; module.exports = all; diff --git a/src/backend/pdf.js b/src/backend/pdf.js index 10da631..c1ee585 100644 --- a/src/backend/pdf.js +++ b/src/backend/pdf.js @@ -450,14 +450,6 @@ const reportSections = (report) => { out.push({ kind: 'text', text: report.description }); } - const tpl = report.template && typeof report.template === 'object' ? report.template : {}; - const tplEntries = Object.entries(tpl).filter(([, v]) => txt(v).trim()); - if (tplEntries.length) { - out.push({ kind: 'blank' }); - out.push({ kind: 'section', text: 'DETAILS' }); - for (const [k, v] of tplEntries) out.push({ kind: 'kv', label: humanLabel(k), value: v }); - } - const confirmations = asList(report.confirmations); out.push({ kind: 'blank' }); out.push({ kind: 'section', text: 'CONFIRMATIONS' }); diff --git a/src/backend/pdf_view.js b/src/backend/pdf_view.js new file mode 100644 index 0000000..6e896c8 --- /dev/null +++ b/src/backend/pdf_view.js @@ -0,0 +1,176 @@ +const path = require('path'); +const crypto = require('crypto'); +const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); +const { pathToFileURL } = require('url'); + +const WORKER_TAG = 'oasis-pdf-view'; +const MUPDF_URL = pathToFileURL(path.join(__dirname, '..', 'server', 'node_modules', 'mupdf', 'dist', 'mupdf.js')).href; +const PDF_BASE_WIDTH = 1600; +const PDF_FIT_WIDTH = 1200; +const PDF_WEBP_EFFORT = 1; +const PDF_ZOOMS = [50, 75, 100, 125, 150, 200, 300]; +const PDF_ZOOM_DEFAULT = 100; +const PDF_PAGE_MAX_PX = 4096; +const PDF_CALL_MS = 20000; +const PDF_DOCS_BYTES = 128 * 1024 * 1024; +const PDF_ROTATIONS = [0, 90, 180, 270]; +const PDF_FRAME_ID_LEN = 12; +const PDF_PAGES_CACHE_BYTES = 64 * 1024 * 1024; +const PDF_WORKER_HEAP_MB = 256; +const PDF_WEBP_QUALITY = 85; +const PDF_MAGIC = Buffer.from('%PDF-'); +const PDF_MAGIC_WINDOW = 1024; + +const widthOf = (zoom) => (zoom <= PDF_ZOOM_DEFAULT ? PDF_FIT_WIDTH : Math.round(PDF_BASE_WIDTH * zoom / 100)); +const PDF_WIDTHS = new Set(PDF_ZOOMS.map(widthOf)); +const zoomOf = (value) => { const z = Number(value); return PDF_ZOOMS.includes(z) ? z : PDF_ZOOM_DEFAULT; }; +const rotationOf = (value) => { const r = Number(value); return PDF_ROTATIONS.includes(r) ? r : 0; }; +const looksLikePdf = (buf) => Buffer.isBuffer(buf) && buf.subarray(0, PDF_MAGIC_WINDOW).indexOf(PDF_MAGIC) >= 0; + +if (!isMainThread && workerData === WORKER_TAG) { + const docs = new Map(); + const sizes = new Map(); + let openBytes = 0; + let mupdf = null; + const need = (key) => { + const doc = docs.get(key); + if (!doc) throw new Error('NO_DOC'); + docs.delete(key); + docs.set(key, doc); + return doc; + }; + const describe = (doc) => ({ pages: doc.countPages() }); + const ops = { + open({ key, data }) { + const doc = mupdf.Document.openDocument(Buffer.from(data), 'application/pdf'); + if (doc.needsPassword()) { doc.destroy(); throw new Error('LOCKED'); } + docs.set(key, doc); + sizes.set(key, data.byteLength); + openBytes += data.byteLength; + while (openBytes > PDF_DOCS_BYTES && docs.size > 1) { + const [oldKey, oldDoc] = docs.entries().next().value; + docs.delete(oldKey); + openBytes -= sizes.get(oldKey) || 0; + sizes.delete(oldKey); + try { oldDoc.destroy(); } catch (_) {} + } + return describe(doc); + }, + info({ key }) { return describe(need(key)); }, + render({ key, page, width, rotate }) { + const doc = need(key); + const pg = doc.loadPage(page - 1); + try { + const [x0, y0, x1, y1] = pg.getBounds(); + let scale = width / Math.max(1, x1 - x0); + scale = Math.min(scale, PDF_PAGE_MAX_PX / Math.max(1, y1 - y0), PDF_PAGE_MAX_PX / Math.max(1, x1 - x0)); + const pix = pg.toPixmap(mupdf.Matrix.concat(mupdf.Matrix.scale(scale, scale), mupdf.Matrix.rotate(rotate || 0)), mupdf.ColorSpace.DeviceRGB, false, true); + try { return { jpeg: pix.asJPEG(PDF_WEBP_QUALITY, false) }; } finally { pix.destroy(); } + } finally { pg.destroy(); } + } + }; + const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; + const ready = import(MUPDF_URL).then((m) => { + mupdf = m; + mupdf.setLog(debug ? (message) => console.error(`[pdf] ${message}`) : () => {}); + }); + parentPort.on('message', async ({ id, op, args }) => { + try { + await ready; + if (!ops[op]) throw new Error('BAD_OP'); + const { data, ...rest } = args || {}; + if (data && !docs.has(rest.key)) ops.open({ key: rest.key, data }); + const value = op === 'open' ? describe(need(rest.key)) : ops[op](rest); + parentPort.postMessage({ id, value }); + } catch (e) { + parentPort.postMessage({ id, error: String((e && e.message) || e) }); + } + }); +} else { + let worker = null; + let current = null; + const queue = []; + const stop = () => { const w = worker; worker = null; if (w) w.terminate().catch(() => {}); }; + const settle = (outcome) => { + const job = current; + if (!job) return; + current = null; + clearTimeout(job.timer); + if (outcome.error) job.reject(new Error(outcome.error)); else job.resolve(outcome.value); + pump(); + }; + const start = () => { + const w = new Worker(__filename, { workerData: WORKER_TAG, resourceLimits: { maxOldGenerationSizeMb: PDF_WORKER_HEAP_MB } }); + w.on('message', (msg) => { if (worker === w && current && msg.id === current.id) settle(msg); }); + const lost = () => { if (worker === w) { worker = null; settle({ error: 'WORKER' }); } }; + w.on('error', lost); + w.on('exit', lost); + w.unref(); + worker = w; + }; + let seq = 0; + const pump = () => { + if (current || !queue.length) return; + if (!worker) start(); + current = queue.shift(); + current.timer = setTimeout(() => { stop(); settle({ error: 'TIMEOUT' }); }, PDF_CALL_MS); + worker.postMessage({ id: current.id, op: current.op, args: current.args }); + }; + const call = (op, args) => new Promise((resolve, reject) => { + queue.push({ id: ++seq, op, args, resolve, reject, timer: null }); + pump(); + }); + const loading = new Map(); + const loadPdf = (key, load) => { + if (!loading.has(key)) { + loading.set(key, (async () => { + const data = await load(); + if (!data) throw new Error('UNAVAILABLE'); + if (!looksLikePdf(data)) throw new Error('NOT_PDF'); + return data; + })().finally(() => loading.delete(key))); + } + return loading.get(key); + }; + const withDoc = async (key, load, op, args = {}) => { + try { return await call(op, { key, ...args }); } catch (e) { if (e.message !== 'NO_DOC') throw e; } + return call(op, { key, ...args, data: await loadPdf(key, load) }); + }; + const pages = new Map(); + let pagesBytes = 0; + const remember = (k, v) => { + pages.set(k, v); + pagesBytes += v.body.length; + for (const [old, entry] of pages) { + if (pagesBytes <= PDF_PAGES_CACHE_BYTES) break; + pages.delete(old); + pagesBytes -= entry.body.length; + } + }; + const encode = async ({ jpeg }) => ({ body: Buffer.from(jpeg), type: 'image/jpeg' }); + const drawing = new Map(); + const keyOf = (text) => crypto.createHash('sha256').update(String(text)).digest('hex'); + const frameIdOf = (src) => `pdf-${keyOf(src).slice(0, PDF_FRAME_ID_LEN)}`; + module.exports = { + PDF_BASE_WIDTH, PDF_ZOOMS, PDF_ZOOM_DEFAULT, PDF_ROTATIONS, + widthOf, zoomOf, rotationOf, looksLikePdf, keyOf, frameIdOf, + isFrameId: (v) => new RegExp(`^pdf-[0-9a-f]{${PDF_FRAME_ID_LEN}}$`).test(String(v || '')), + isPdfWidth: (w) => PDF_WIDTHS.has(Number(w)), + info: (key, load) => withDoc(key, load, 'info'), + async page(key, load, page, width, rotation) { + const r = rotationOf(rotation); + const cacheKey = `${key}|${page}|${width}|${r}`; + const hit = pages.get(cacheKey); + if (hit) { pages.delete(cacheKey); pages.set(cacheKey, hit); return hit; } + if (!drawing.has(cacheKey)) { + drawing.set(cacheKey, (async () => { + const out = await encode(await withDoc(key, load, 'render', { page: Number(page), width: Number(width), rotate: r }), r); + remember(cacheKey, out); + return out; + })().finally(() => drawing.delete(cacheKey))); + } + return drawing.get(cacheKey); + }, + shutdown: () => { stop(); while (queue.length) queue.shift().reject(new Error('STOPPED')); if (current) settle({ error: 'STOPPED' }); } + }; +} diff --git a/src/backend/renderStyledText.js b/src/backend/renderStyledText.js index 40945d0..50d7f90 100644 --- a/src/backend/renderStyledText.js +++ b/src/backend/renderStyledText.js @@ -1,4 +1,4 @@ -const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea } = require("../server/node_modules/hyperaxe"); +const { a, img, video, audio, span, strong, em, u, s: strike, code, div, textarea, object } = require("../server/node_modules/hyperaxe"); const i18nBase = require("../client/assets/translations/i18n"); const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model"); @@ -202,7 +202,20 @@ function renderStyledText(value, opts = {}) { result.push(audio({ controls: true, class: 'post-audio', src: blobHref(m.blob) })); } else if (m.type === 'blob-pdf') { const i18n = getI18n(); - result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || i18n.pdfFallbackLabel || 'PDF')); + const pdfLabel = m.name || i18n.pdfFallbackLabel || 'PDF'; + const clearnet = String(opts.blobPrefix || '').startsWith('/c/'); + let lang = ''; + try { lang = clearnet ? ((require('../models/typed_log').requestScope.getStore() || {}).cnLang || '') : ''; } catch (_) {} + const fid = require('./pdf_view').frameIdOf(blobHref(m.blob)); + const query = new URLSearchParams({ src: blobHref(m.blob), title: String(m.name || ''), ...(lang ? { lang } : {}), fid }); + const holder = (src, cls) => span({ class: 'pdf-holder' }, + object({ id: fid, data: src, type: 'text/html', class: cls, title: pdfLabel, 'aria-label': pdfLabel }, ''), + a({ href: '#', class: 'pdf-unmax', title: i18n.pdfClose || 'Close' }, 'โœ•')); + result.push(clearnet + ? holder(`/c/pdf/embed?${query}`, 'cn-pdf-frame') + : opts.blobPrefix + ? a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, pdfLabel) + : holder(`/pdf/embed?${query}`, 'pdf-frame')); } else if (m.type === 'blob-file') { result.push(a({ href: blobHref(m.blob), class: 'post-pdf', target: '_blank', rel: 'noopener noreferrer' }, m.name || m.blob.slice(1, 9))); } else if (m.type === 'md-mention') { diff --git a/src/backend/request_guards.js b/src/backend/request_guards.js index bc62e3e..33cf785 100644 --- a/src/backend/request_guards.js +++ b/src/backend/request_guards.js @@ -68,7 +68,7 @@ const publicModeGuard = ({ isPublic, onBlocked }) => async (ctx, next) => { await next(); }; -const buildCsp = (isClearnet, { frames = false } = {}) => (isClearnet +const buildCsp = (isClearnet, { frames = '', framed = false, objects = '' } = {}) => (isClearnet ? [ "default-src 'self'", "script-src 'none'", @@ -76,24 +76,27 @@ const buildCsp = (isClearnet, { frames = false } = {}) => (isClearnet "img-src 'self' data:", "media-src 'self' blob:", "connect-src 'self'", - "frame-src 'none'", + frames ? `frame-src ${frames}` : "frame-src 'none'", "form-action 'self'", - "object-src 'none'", + objects ? `object-src ${objects}` : "object-src 'none'", "base-uri 'none'", - "frame-ancestors 'none'" + framed ? "frame-ancestors 'self'" : "frame-ancestors 'none'" ] : [ "default-src 'self'", - "script-src 'self' http://localhost:3000/js", + "script-src 'none'", "style-src 'self'", "img-src 'self'", "media-src 'self' blob:", - "worker-src 'self' blob:", - frames ? "frame-src 'self'" : "frame-src 'none'", + frames ? `frame-src ${frames}` : "frame-src 'none'", "form-action 'self'", - "object-src 'none'", + objects ? `object-src ${objects}` : "object-src 'none'", "base-uri 'none'", - "frame-ancestors 'none'" + framed ? "frame-ancestors 'self'" : "frame-ancestors 'none'" ]).join('; '); -module.exports = { isMsgKey, pickMsgKeys, isLocalPath, safeReturnTo, refererPath, safeRefererRedirect, isClearnetPath, isTrustedRequest, publicModeGuard, buildCsp }; +const IMAGE_SIZE_MIN = 16; +const IMAGE_SIZE_MAX = 1024; +const isImageSize = (value) => { const n = Number(value); return Number.isInteger(n) && n >= IMAGE_SIZE_MIN && n <= IMAGE_SIZE_MAX; }; + +module.exports = { IMAGE_SIZE_MIN, IMAGE_SIZE_MAX, isImageSize, isMsgKey, pickMsgKeys, isLocalPath, safeReturnTo, refererPath, safeRefererRedirect, isClearnetPath, isTrustedRequest, publicModeGuard, buildCsp }; diff --git a/src/client/assets/styles/highlight.css b/src/client/assets/styles/highlight.css deleted file mode 100644 index 0aa4998..0000000 --- a/src/client/assets/styles/highlight.css +++ /dev/null @@ -1,67 +0,0 @@ -.hljs-comment, -.hljs-quote { - color: var(--base03); -} - -.hljs-variable, -.hljs-template-variable, -.hljs-tag, -.hljs-name, -.hljs-selector-id, -.hljs-selector-class, -.hljs-regexp, -.hljs-deletion { - color: var(--base08); -} - -.hljs-number, -.hljs-built_in, -.hljs-builtin-name, -.hljs-literal, -.hljs-type, -.hljs-params, -.hljs-meta, -.hljs-link { - color: var(--base09); -} - -.hljs-attribute { - color: var(--base0A); -} - -.hljs-string, -.hljs-symbol, -.hljs-bullet, -.hljs-addition { - color: var(--base0B); -} - -.hljs-title, -.hljs-section { - color: var(--base0D); -} - -.hljs-keyword, -.hljs-selector-tag { - color: var(--base0E); -} - -.hljs { - display: block; - overflow-x: auto; - color: var(--base05); - padding: 0.5em; -} - -.hljs-emphasis { - font-style: italic; -} - -.hljs-strong { - font-weight: bold; -} - -.hljs-string, .hljs-link { - background: #FFA500; - user-select: text; -} diff --git a/src/client/assets/styles/mobile.css b/src/client/assets/styles/mobile.css index d2d2cae..9356d24 100644 --- a/src/client/assets/styles/mobile.css +++ b/src/client/assets/styles/mobile.css @@ -672,8 +672,7 @@ a.filter-btn, .main-column .edit-btn, .main-column .join-btn, .main-column .leave-btn, -.main-column .buy-btn, -.main-column .submit-button { +.main-column .buy-btn { max-width: 100% !important; box-sizing: border-box !important; overflow-wrap: anywhere !important; @@ -682,8 +681,7 @@ a.filter-btn, .main-column .btn, .main-column .create-button, .main-column .update-btn, -.main-column .delete-btn, -.main-column .submit-button { +.main-column .delete-btn { white-space: normal !important; } .main-column form { diff --git a/src/client/assets/styles/mobile_ux.css b/src/client/assets/styles/mobile_ux.css index 0f971d8..cf496b2 100644 --- a/src/client/assets/styles/mobile_ux.css +++ b/src/client/assets/styles/mobile_ux.css @@ -695,22 +695,33 @@ body:has(#oasis-menu-toggle:checked) form.oasis-phone-state { display: none !imp width: auto !important; min-width: 0; min-height: 44px !important; margin: 0 !important; padding: 4px 6px !important; font-size: 14px !important; line-height: 1.15 !important; overflow-wrap: anywhere; } -/* las fichas van plegadas tras un boton Filtrar (mobile_ui.activityFilter); por - defecto se ve todo y con un filtro puesto el boton dice cual */ -.main-column details.activity-filter { margin: 0 0 12px !important; } -.main-column details.activity-filter > summary.activity-filter-toggle { - display: flex; align-items: center; justify-content: center; min-height: 48px; list-style: none; cursor: pointer; - border: 1px solid var(--ux-line); border-radius: var(--a26-radius, 12px); +/* las fichas van plegadas tras un boton Filtrar (mobile_ui.activityFilter): casilla, + etiqueta y fichas en la misma caja, que la 1.3.5 mueve a su barra fija (.apk-strip). + Por defecto se ve todo; con un filtro puesto el boton dice cual */ +.main-column div.activity-filter-chips.mobile-filter { + display: block !important; overflow: visible !important; white-space: normal !important; + margin: 0 0 8px !important; padding: 0 !important; width: 100% !important; +} +.main-column .mobile-filter > input.mobile-filter-toggle { position: absolute !important; opacity: 0 !important; width: 1px !important; height: 1px !important; min-height: 0 !important; pointer-events: none; } +.main-column .mobile-filter > label.mobile-filter-btn { + display: flex !important; align-items: center; justify-content: center; min-height: 48px; margin: 0 !important; cursor: pointer; + border: 1px solid var(--ux-line); border-radius: var(--a26-radius, 12px); background: var(--ux-bg); color: var(--ux-fg); font-weight: 700; font-size: 16px; text-transform: uppercase; letter-spacing: 1px; } -.main-column details.activity-filter > summary::-webkit-details-marker { display: none; } -.main-column details.activity-filter > summary::after { content: "\25BE"; margin-left: 8px; font-size: 16px; } -.main-column details.activity-filter[open] > summary { margin-bottom: 10px; } -.main-column details.activity-filter[open] > summary::after { content: "\25B4"; } -.main-column details.activity-filter.is-filtered > summary { background: var(--ux-accent); color: var(--ux-on-accent); border-color: var(--ux-accent); } -.main-column .activity-filter-now { margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0; } -.main-column .activity-filter-now::before { content: "\00B7"; margin-right: 8px; } -.main-column details.activity-filter div.activity-filter-chips:not(.activity-toolbar-row) { margin: 0 !important; } +.main-column .mobile-filter > label.mobile-filter-btn::after { content: "\25BE"; margin-left: 8px; font-size: 16px; } +.main-column .mobile-filter > input:checked + label.mobile-filter-btn::after { content: "\25B4"; } +.main-column .mobile-filter.is-filtered > label.mobile-filter-btn { background: var(--ux-accent); color: var(--ux-on-accent); border-color: var(--ux-accent); } +.main-column .mobile-filter .activity-filter-now { margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0; } +.main-column .mobile-filter .activity-filter-now::before { content: "\00B7"; margin-right: 8px; } +.main-column .mobile-filter > div.mobile-filter-chips { display: none !important; } +.main-column .mobile-filter > input:checked ~ div.mobile-filter-chips { + display: grid !important; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-gap: 8px; margin: 10px 0 0 !important; padding: 0 !important; +} +.main-column .mobile-filter div.mobile-filter-chips a.activity-chip { + display: flex !important; align-items: center; justify-content: center; text-align: center; + width: auto !important; min-width: 0; min-height: 44px !important; margin: 0 !important; padding: 4px 6px !important; + font-size: 14px !important; line-height: 1.15 !important; overflow-wrap: anywhere; white-space: normal !important; +} /* cabecera de cada tarjeta: los botones en una fila arriba a la derecha y la etiqueta (FEEDS, SALAS...) debajo a la izquierda; antes compartian fila y la X caia sola */ @@ -810,3 +821,38 @@ body .oasis-mobile-topbar .omt-logo img, body .oasis-mobile-topbar .omt-avatar i background: var(--ux-accent) !important; color: var(--ux-on-accent) !important; border-color: var(--ux-accent) !important; font-weight: 700 !important; } + +/* ---------- cabecera de modulo de la 1.3.5 (apkModbar en main_views.js) ---------- + el dev la pinta en OasisMobile.css, que con el skin Activista no se carga: sin esto + sus div heredan el relleno generico y la cabecera ocupaba media pantalla. Los avisos + (.apk-banners: EMPEZAR, version nueva) suben por encima con order, sin tocar su HTML */ +.main-column { display: flex !important; flex-direction: column !important; } +.main-column > * { flex: 0 0 auto; min-width: 0; } +.main-column > div.apk-banners { order: -1; margin: 0 0 12px !important; padding: 0 !important; } +.main-column > div.apk-banners > * { margin: 0 0 10px !important; } +.main-column > div.apk-modbar { display: block !important; position: static !important; margin: 0 0 12px !important; padding: 0 !important; background: transparent !important; box-shadow: none !important; } +.main-column div.apk-modbar > div.apk-modbar-row { display: flex !important; align-items: center; justify-content: space-between; margin: 0 0 10px !important; padding: 0 !important; } +.main-column div.apk-modbar .apk-title { margin: 0 !important; padding: 0 !important; color: var(--ux-fg) !important; } +.main-column div.apk-modbar .apk-pin { margin: 0 0 0 10px !important; width: auto !important; flex: 0 0 auto; } +.main-column div.apk-modbar > .apk-strip:not(.mobile-filter) { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; margin: 0 0 10px !important; padding: 0 0 4px !important; } +.main-column div.apk-modbar > .apk-strip:not(.mobile-filter) > * { flex: 0 0 auto !important; width: auto !important; margin: 0 8px 0 0 !important; white-space: nowrap !important; } +.main-column div.apk-modbar > form.filter-box { margin: 0 0 8px !important; } +.main-column div.apk-modbar > div.apk-modbar-foot { margin: 0 !important; padding: 0 !important; } +.main-column div.apk-modbar div.module-stats-line { margin: 0 !important; padding: 0 !important; font-size: 13px; color: var(--ux-muted) !important; } +.main-column div.apk-modbar div.module-stats-line > span { margin-right: 12px; } +/* la seccion que se queda vacia cuando la cabecera se lleva las fichas y el buscador */ +.main-column > section:empty, .main-column div.activity-toolbar-row:empty { display: none !important; } +.main-column > section > div.activity-toolbar-row:only-child:empty { display: none !important; } + +/* cabecera de tarjeta de la 1.3.5: autoria a la izquierda (nombre cortado con ...) y + el menu โ‹ฏ y la flecha juntos a la derecha. Pisa lo de la 1.3.3 (columna) */ +.main-column div.card-header.activity-card-header.card-top { flex-direction: row !important; flex-wrap: nowrap !important; align-items: flex-start !important; } +.main-column div.card-header.card-top > div.card-byline { display: flex !important; flex-wrap: wrap !important; align-items: center; flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; margin: 0 !important; padding: 0 !important; } +.main-column div.card-byline > a.user-link { display: block !important; max-width: calc(100% - 34px) !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; margin: 0 0 0 8px !important; } +.main-column div.card-byline > .card-byline-time, .main-column div.card-byline > div.card-chips-row { flex: 0 0 auto; margin: 4px 8px 0 0 !important; } +.main-column div.card-header.card-top > div.content-actions { order: 2 !important; flex: 0 0 auto !important; width: auto !important; display: flex !important; flex-wrap: nowrap !important; justify-content: flex-end !important; margin: 0 0 0 8px !important; } +.main-column div.content-actions > details.card-menu { width: auto !important; flex: 0 0 auto !important; margin: 0 8px 0 0 !important; } +.main-column details.card-menu > summary.card-menu-toggle { min-width: 44px !important; height: 44px !important; } +/* el menu desplegado con los colores del skin y filas de 48px */ +.main-column details.card-menu > div.card-menu-items { background: var(--ux-bg) !important; border: 1px solid var(--ux-line) !important; color: var(--ux-fg) !important; min-width: 220px; } +.main-column div.card-menu-items > * { min-height: 48px; } diff --git a/src/client/assets/styles/style.css b/src/client/assets/styles/style.css index f3c8297..78bdcb8 100644 --- a/src/client/assets/styles/style.css +++ b/src/client/assets/styles/style.css @@ -35,6 +35,15 @@ input[type="text"], input[type="email"], input[type="password"], textarea { margin-bottom: 15px; } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > br { display: none } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(label, span) { display: block; margin: 0 0 6px 0 } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(h2, h3, p) { margin: 0 0 16px 0 } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(input:not([type="hidden"], [type="checkbox"], [type="radio"], [type="submit"], [type="button"], .logistics-short), select:not(.logistics-price-unit), textarea) { display: block; box-sizing: border-box; width: 100%; max-width: 720px; margin: 0 0 16px 0 } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > input:is([type="checkbox"], [type="radio"]) { display: block; width: auto; margin: 0 0 16px 0 } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(.logistics-short, .logistics-price-unit) { margin: 0 8px 16px 0 } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > :is(div, table):not(.map-form-row, .map-toolbar, .map-form-map-slot) { max-width: 720px; box-sizing: border-box; margin-bottom: 16px } +section > div:is([class$="-form"], [class*="-form "]) > form:not(.inline-form) > button { margin-top: 0 } + /* Body and overall page styles */ html { color: #FFA500; @@ -1602,7 +1611,58 @@ button.create-button:hover { } /* Documents */ +.pdf-holder { + display: block; +} +.pdf-frame { + display: block; + width: 100%; + max-width: 100%; + height: 80vh; + min-height: 520px; + border: 0; + margin-bottom: 1rem; + background: transparent; +} +.pdf-frame:target { + position: fixed; + inset: 0; + z-index: 100000; + width: 100vw; + max-width: none; + height: 100vh; + min-height: 0; + margin: 0; + background: #111; +} +.pdf-unmax { + display: none; +} +.pdf-frame:target + .pdf-unmax { + display: block; + position: fixed; + top: 10px; + right: 18px; + z-index: 100001; + background-color: #2d2d2d; + color: #eee; + padding: 0.4rem 0.8rem; + border-radius: 4px; + font-size: 1rem; + text-decoration: none; +} +.pdf-embed-root, +body.pdf-embed { + height: 100%; +} +body.pdf-embed { + margin: 0; + padding: 0; + overflow: hidden; + background: transparent !important; +} .pdf-viewer-container { + box-sizing: border-box; width: 100%; max-width: 100%; background: #1e1e1e; @@ -1611,42 +1671,116 @@ button.create-button:hover { border-radius: 6px; box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); } - +.pdf-embedded { + display: flex; + flex-direction: column; + height: 100%; + margin-bottom: 0; +} .pdf-canvas-wrap { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; height: 420px; + margin-bottom: 0; + overflow: hidden; +} +.pdf-wrap-scroll { + display: block; overflow: auto; } - -.pdf-viewer-container canvas { +.pdf-embedded .pdf-canvas-wrap { + flex: 1 1 auto; + height: auto; + min-height: 0; +} +.pdf-page-image { display: block; + box-sizing: border-box; + width: auto; + height: auto; margin: 0 auto; border: 1px solid #333; background-color: #fff; } - -.pdf-viewer-container .pdf-controls { - margin-top: 1rem; +.pdf-z50 { + max-width: 50%; + max-height: 50%; } - -.pdf-viewer-container button { +.pdf-z75 { + max-width: 75%; + max-height: 75%; +} +.pdf-z100 { + max-width: 100%; + max-height: 100%; +} +.pdf-z125 { + width: 100%; +} +.pdf-z150 { + width: 150%; +} +.pdf-z200 { + width: 200%; +} +.pdf-z300 { + width: 300%; +} +.pdf-viewer-container .pdf-controls { + flex-shrink: 0; + margin-top: 1rem; + margin-bottom: 0; +} +.pdf-viewer-container .pdf-controls a, +.pdf-viewer-container .pdf-controls span { + display: inline-block; + margin: 0 4px 4px 0; background-color: #2d2d2d; color: #eee; border: none; padding: 0.4rem 0.8rem; font-size: 1rem; + line-height: 1.2; + text-decoration: none; cursor: pointer; border-radius: 4px; transition: background-color 0.2s ease; } - -.pdf-viewer-container button:hover { +.pdf-viewer-container .pdf-controls a:hover { background-color: #444; } - -.pdf-viewer-container span { +.pdf-viewer-container .pdf-controls .pdf-off { + opacity: 0.4; + cursor: default; +} +.pdf-message { + padding: 12px; color: #ccc; - margin: 0 1rem; - font-family: monospace; + text-align: center; +} +.pdf-maximized { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + margin: 0; + padding: 6px; + border-radius: 0; + background: #111; +} +.pdf-maximized .pdf-canvas-wrap { + flex: 1 1 auto; + height: auto; + max-height: none; +} +.pdf-maximized .pdf-controls { + flex-shrink: 0; + margin: 6px 0; } /* CVs */ @@ -2098,6 +2232,51 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; right: 112px; font-size: 1.15em; } +.lightbox-prev, +.lightbox-next { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + color: #fff; + font-size: 2em; + line-height: 1; + text-decoration: none; +} +.lightbox-prev { + left: 15px; +} +.lightbox-next { + right: 15px; +} +.media-download-btn { + display: inline-flex; + margin: 10px 0; +} +.edited-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border: 1px solid rgba(128, 128, 128, 0.5); + border-radius: 10px; + font-size: 0.75em; + font-weight: 700; + letter-spacing: 1px; + text-decoration: none; + white-space: nowrap; +} +.media-lock-note { + font-size: 0.9em; + opacity: 0.85; + margin: 6px 0; +} /* Opinions */ .voting-buttons { @@ -2614,15 +2793,7 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; text-align: center; } -.market-comments { - background: #2f2f2f; - border-radius: 12px; - padding: 12px; -} -.market-comments .votations-comment-card { - background: #3a3a3a; -} .countdown-strong { font-weight: 700; @@ -3169,12 +3340,6 @@ width:100%; max-width:200px; max-height:300px; object-fit:cover; margin:16px 0; color: #ffd740; } -.comment-footer { - font-size: 0.85em; - display: flex; - justify-content: space-between; - color: #ffd740; -} .date-link { color: #ffd740; @@ -3799,8 +3964,6 @@ a.pm-btn:hover { text-decoration: none } .card-field-stacked .card-value{white-space:pre-wrap;word-break:break-word} .card-field-rich{flex-direction:column;align-items:flex-start} .card-field-rich .card-value{white-space:pre-wrap;word-break:break-word;margin-top:4px} -.report-template{margin-top:12px;padding:12px;background:rgba(255,255,255,0.05);border-radius:8px} -.report-template .card-label{white-space:nowrap} @media (max-width: 420px){ .card-field{ flex-direction:column; } @@ -4311,6 +4474,7 @@ a.pm-btn:hover { text-decoration: none } align-items: center; margin: 8px 0; } +.tribe-side-actions.report-confirm-actions { justify-content: center } .tribe-side-actions form { margin: 0; @@ -6927,8 +7091,6 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .jobs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px} .blogs-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(max(300px,calc((100% - 40px)/3)),1fr));gap:20px} .jobs-grid .tribe-card,.tribe-grid .tribe-card{margin-bottom:0} -.tribe-card .card-comments-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px} -.tribe-card .card-comments-summary .inline-form{margin:0} .jobs-info-table{table-layout:auto} .jobs-info-table .tribe-info-label{width:1%;white-space:nowrap} .jobs-info-table .tribe-info-value{white-space:normal;word-break:normal;overflow-wrap:anywhere} @@ -6940,14 +7102,13 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .job-price-line{margin:0;font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace} .job-card-view-btn{margin-top:8px} .job-card-view-btn form{margin:0} -.vote-comments-section,.comments-count,.comment-form-wrapper,.comments-list,.vote-buttons-block,.vote-buttons-row,.voting-buttons,.vote-table{border:none;background:transparent;padding:0;margin:0} +.vote-comments-section,.comment-form-wrapper,.comments-list,.vote-buttons-block,.vote-buttons-row,.voting-buttons,.vote-table{border:none;background:transparent;padding:0;margin:0} .vote-buttons-block{display:flex;flex-direction:column;gap:8px} .vote-buttons-row{display:flex;gap:8px;flex-wrap:wrap} .voting-buttons{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap} .vote-comments-section{margin-top:16px} .comment-form-wrapper{margin-top:12px} .comments-list{margin-top:12px} -.comments-count{margin-bottom:8px} .title-with-chip{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:transparent !important;border:0 !important;padding:0 !important} .title-with-chip h2{margin:0} .pm-exposition-icon{font-size:14px;line-height:1} @@ -6955,7 +7116,7 @@ a.chip-link:hover .pm-exposition-chip{filter:brightness(1.2)} .shop-title-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:6px;padding:0;background:transparent;border-radius:0;box-shadow:none} .shop-title-row h2{margin:0} .card-chips-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 10px 0;padding:0;border:none;background:transparent;border-radius:0;box-shadow:none} -.tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn,.tribe-card-body .card-comments-summary,.tribe-card-body .tribe-card-members{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none} +.tribe-card-body .job-price-line,.tribe-card-body .job-meta-line,.tribe-card-body .job-card-view-btn{padding:0;background:transparent;border-radius:0;box-shadow:none;border:none} .tribe-card-body .job-price-line{margin:4px 0} .tribe-card-body .job-card-view-btn{margin-top:8px;margin-bottom:0} .transfer-block-card{border:none;background:transparent;padding:0;margin:0 0 12px 0} @@ -7316,9 +7477,8 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .industry-blueprint-card{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0} .industry-card-wrap{position:relative;border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:10px;margin:8px 0;background:transparent;box-shadow:none} .industry-card-wrap .industry-blueprint-card{border:none;background:transparent;padding:0;margin:0;box-shadow:none;border-radius:0} -.industry-card-wrap > .card-header{position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1} -.industry-card-wrap > .card-header > span:empty{display:none} -.industry-card-wrap > .industry-blueprint-card{padding:0 96px 0 0} +.industry-card-wrap > .card-header:not(.card-top){position:absolute;top:8px;right:10px;margin:0;padding:0;border:none;background:transparent;box-shadow:none;z-index:1} +.industry-card-wrap > .industry-blueprint-card{padding:0} .industry-actions form,.industry-actions .project-control-form--status{margin:0;padding:0;border:none;background:transparent;display:flex;align-items:center;gap:10px} .industry-actions .project-control-select,.industry-actions .project-control-btn,.industry-actions select,.industry-actions button{margin:0} .industry-actions button,.industry-actions .project-control-select{display:inline-flex;align-items:center;justify-content:center;height:38px;box-sizing:border-box} @@ -7459,7 +7619,6 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol .content-actions .spread-form{margin:0;display:inline-flex} .blog-card-body, -.blog-detail-body, .mention-card-body { border: none !important; background: transparent !important; @@ -7490,14 +7649,6 @@ a.user-link{padding:8px 16px;border-radius:5px;text-align:center;font-weight:bol word-break: break-word; } -.blog-card-meta { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 6px 0; - border: none !important; - background: transparent !important; -} .blog-allow-comments { display: inline-flex; @@ -8865,6 +9016,8 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0 .graphos-lightbox .lightbox-close{position:fixed;top:14px;right:22px;z-index:10002;font-size:28px;text-decoration:none} .live-lightbox .lightbox-image-link{display:flex;align-items:center;justify-content:center;max-width:100%;max-height:100%;padding:0;margin:0;border:0;background:none} .shop-buy-form,.market-item.actions,.market-bid-form,.market-buy-form{border:0;background:transparent;box-shadow:none;padding:0} +.pending-media{display:flex;align-items:center;justify-content:center;min-height:120px;max-width:480px;padding:16px;border:1px dashed #8a8a8a;border-radius:10px;background:rgba(128,128,128,0.15);color:#a0a0a0;text-align:center;box-sizing:border-box;margin:8px 0} +.buy-box{border:1px solid rgba(255,165,0,0.35);border-radius:8px;padding:14px 16px;margin:0 0 16px 0;background:rgba(255,255,255,0.03)} .project-progress-block .confirmations-progress{width:100%} .tags-list .tag-table th:last-child,.tags-list .tag-table td:last-child{width:1%;white-space:nowrap;text-align:right} .stats-block .tag-table,.stats-card .tag-table{width:100%;border-collapse:collapse;table-layout:fixed} @@ -8924,3 +9077,81 @@ label.wallet-create-transfer input[type="checkbox"]{width:auto !important;flex:0 .live-lightbox.live-fullscreen{background:#000;z-index:10050} .live-fullscreen .lightbox-image-link{display:flex;align-items:center;justify-content:center;width:100vw;height:100vh} .live-fullscreen .lightbox-image{width:100vw;height:100vh;max-width:100vw;max-height:100vh;object-fit:contain;border-radius:0} +.graphos-zoom-icon{font-size:1.5em;line-height:1;vertical-align:0.06em} +.card-top{display:flex;flex-wrap:nowrap;align-items:center;justify-content:space-between;gap:8px;margin:0 0 6px !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important} +.card-byline{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;flex:1 1 auto;min-width:0;margin:0 !important;font-size:12px;line-height:16px;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important} +.card-byline-avatar{display:inline-flex;flex:0 0 auto;background:transparent !important;border:0 !important;padding:0 !important} +.card-byline-avatar img{display:block;width:24px;height:24px;border-radius:50%;object-fit:cover;margin:0;border:0} +.card-byline a.user-link{display:inline;background:transparent !important;border:0 !important;padding:0 !important;margin:0;border-radius:0;color:#FFD700 !important;font-size:12px;font-weight:700;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:100%;box-shadow:none !important} +.card-byline a.user-link:hover{text-decoration:underline;background:transparent !important} +.card-byline-time{opacity:.65;white-space:nowrap} +.card-byline-time::before{content:"\2022";margin-right:6px} +.card-byline .card-chips-row{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0 !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important} +.card-byline .pm-exposition-chip{font-size:11px;padding:1px 8px} +.card-byline :is(.emergency-severity,.edited-chip,.eco-tax-chip){font-size:11px;padding:1px 8px;margin:0;line-height:16px} +.card-byline :is(.edited-badge,.votations-comment-date){font-size:12px;opacity:.65;margin:0} +.card-top>.content-actions{display:flex;flex:0 0 auto;flex-wrap:nowrap;align-items:center;gap:4px;margin:0 0 0 auto !important;background:transparent !important;border:0 !important;padding:0 !important;box-shadow:none !important;overflow:visible !important} +.card-top .btn-singleview.btn-content{min-width:32px;height:32px;padding:0 8px;font-size:.95em} +.card-menu{position:relative} +.card-menu>.card-menu-toggle.btn-singleview{list-style:none;min-width:32px;height:32px;padding:0 6px;font-size:1.3em;border-radius:50%} +.card-menu[open]>.card-menu-toggle::before{content:"";position:fixed;inset:0;z-index:29;cursor:default} +.card-menu[open]{z-index:40} +.card-top .spread-form{display:inline-flex;align-items:center;margin:0;padding:0} +.card-top .spread-form .btn-singleview{box-sizing:border-box;height:32px;min-width:32px;margin:0;padding:0 12px;gap:6px;border-radius:16px;font-size:.95em;line-height:1} +.card-top .spread-form .btn-singleview .btn-count{line-height:1} +.card-top>.content-actions>*{margin-top:0;margin-bottom:0} +.card-menu-toggle::-webkit-details-marker{display:none} +.card-menu-toggle::marker{content:""} +.card-menu-items{position:absolute;right:0;top:calc(100% + 4px);z-index:30;display:flex;flex-direction:column;gap:0;min-width:200px;margin:0 !important;padding:6px 0 !important;background:#1e1f23;border:1px solid #333;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,.45);overflow:visible !important} +.card-menu-items .content-action-form,.card-menu-items .spread-form{display:block;margin:0;background:transparent !important;border:0 !important;padding:0 !important} +.card-menu-items .btn-singleview{width:100%;justify-content:flex-start;gap:12px;height:40px;margin:0;padding:0 16px;border-radius:0;font-size:1em;background:transparent !important} +.card-menu-items .btn-singleview:hover{background:rgba(128,128,128,.2) !important} +.card-menu-items .btn-singleview:has(>.btn-icon:only-child){width:100%;min-width:0;height:40px;padding:0 16px;flex-shrink:0} +.card-menu-items .btn-singleview .btn-icon{flex:0 0 18px;width:18px;height:18px} +.card-menu-items .btn-singleview::after{content:attr(title);font-size:13px;white-space:nowrap} +div:has(.card-menu[open]){overflow:visible !important} +div:has(>.card-top):not(.tribe-side){padding:8px 16px 12px !important;border-radius:16px !important} +.podcast-episode-row .podcast-episode-main{padding:0 !important;border-radius:0 !important} +:is(.campaign-update,.logistics-booking,.logistics-rating):has(>.card-top){border-radius:0 !important} +:where(:is(div,li):has(>.card-top):not(.tribe-side)) div{margin:0 0 8px;padding:0;border:0;border-radius:0;background-color:transparent;box-shadow:none} +:where(:is(div,li):has(>.card-top):not(.tribe-side)) :is(.card-section,.card-body){margin:0 !important;padding:0 !important;border:0 !important;border-radius:0 !important;background:transparent !important;box-shadow:none !important} +:is(div,li):has(>.card-top):not(.tribe-side){font-family:'Inter','Roboto','Segoe UI','Helvetica Neue',Arial,sans-serif;font-size:14px;line-height:20px;font-weight:400} +:where(:is(div,li):has(>.card-top):not(.tribe-side)) :is(p,.blog-card-text,.feed-text,.post-text){font-size:14px;line-height:20px;font-weight:400} +:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title){font-size:18px;font-weight:600;line-height:24px;letter-spacing:0;margin:4px 0 6px} +:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title) a{text-decoration:none} +:where(:is(div,li):has(>.card-top)) :is(h2,.tribe-card-title) a:hover{text-decoration:underline} +.card-engage{display:flex !important;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:8px;margin:8px 0 0 !important} +.card-engage>*{flex:0 0 auto;margin:0 !important} +.card-engage>details[open],.card-engage>details[open]~details,.card-engage>.feed-votes-summary{flex:1 0 100%} +.card-engage>details:not([open]){width:auto} +.card-meta-row{gap:8px} +.card-engage .opinions-summary,.card-engage .comments-summary,.card-meta-row .opinions-summary,.card-meta-row .comments-summary-link,.card-meta-row .comments-summary,.share-summary-link{display:inline-flex !important;align-items:center;gap:6px;width:auto !important;max-width:none;box-sizing:border-box;height:32px;padding:0 12px !important;border-radius:5px !important;font-size:12px;font-weight:600;line-height:30px;text-decoration:none} +.share-summary-link{border:1px solid rgba(128,128,128,.5);background:rgba(128,128,128,.12);color:inherit} +.share-summary-link:hover{background:rgba(128,128,128,.25);text-decoration:none} +.card-meta-row>.feed-votes-summary{flex:1 0 100%;order:-1;margin:0} +.card-share{display:inline-flex;flex:0 0 auto;background:transparent !important;border:0 !important;padding:0 !important} +.share-summary-icon{margin:0} +.tribe-section-nav{gap:6px;padding:10px 12px !important;margin:0 0 12px !important;overflow:visible} +.tribe-section-group{display:contents} +.tribe-section-group>form:last-child{margin-right:12px} +.tribe-section-nav .filter-btn{margin:0} +.wallet-error-actions{display:flex;flex-wrap:wrap;gap:10px;margin:12px 0 4px;padding:0;border:0;background:none;box-shadow:none;overflow:visible} +.wallet-error-actions .update-download-link{display:inline-block;padding:10px 18px;background:#FFA500;border:0;border-radius:6px;color:#000;font-size:14px;font-weight:bold;text-decoration:none} +.wallet-error-actions .update-download-link:hover{background:#FFD700;color:#000;text-decoration:none} +.tribe-card:has(>.card-top)>.tribe-card-body{padding-bottom:0} +.tribe-card:has(>.card-top)>.tribe-card-body>:last-child{margin-bottom:0} +.bookmark-title .card-title-link:not([href^="/tribe/"]){color:#ff6600} +.blockexplorer-search{margin:12px 0 16px;padding:10px 12px} +.blockexplorer-search-form :is(.blockexplorer-search-row,.blockexplorer-search-pair,.blockexplorer-search-dates,.blockexplorer-search-actions,.filter-box__controls){margin:0;padding:0;border:0;background:transparent;box-shadow:none;overflow:visible} +.blockexplorer-search-form .blockexplorer-search-row{flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px} +.blockexplorer-search-form .blockexplorer-search-pair,.blockexplorer-search-form .blockexplorer-search-dates{gap:8px} +.blockexplorer-search-form .blockexplorer-search-input{min-width:180px;margin:0} +.tribe-main>.media-download-btn{align-self:flex-start} +.tribe-side-actions .update-btn.danger-btn{border-color:#d9534f;color:#d9534f;background:transparent} +.tribe-side-actions .update-btn.danger-btn:hover{background:#d9534f;color:#000} +.detail-share{display:flex;justify-content:flex-start;margin:4px 0 !important;padding:0 !important;border:0 !important;background:transparent !important;box-shadow:none !important;overflow:visible} +.detail-share .share-summary-link{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border:1px solid rgba(128,128,128,.5);border-radius:5px;background:rgba(128,128,128,.12);font-size:12px;font-weight:600;line-height:30px;text-decoration:none;color:inherit} +.detail-share .share-summary-link:hover{background:rgba(128,128,128,.25);text-decoration:none} + +.link-lightbox > .lightbox-close::before { content: ""; position: fixed; inset: 0; z-index: 0; cursor: default; } +.link-lightbox > .link-box { position: relative; z-index: 1; } diff --git a/src/client/assets/themes/Clear-SNH.css b/src/client/assets/themes/Clear-SNH.css index c64f10b..23099a3 100644 --- a/src/client/assets/themes/Clear-SNH.css +++ b/src/client/assets/themes/Clear-SNH.css @@ -320,6 +320,8 @@ a.user-link:focus, button.user-link:focus { .btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #FF8F00 !important; color: #FFFFFF !important; } .btn-singleview.btn-content { background: #FF6F00 !important; color: #F0F0F0 !important; border: 1px solid #FF6F00 !important; } .btn-singleview.btn-content:hover { background: #FF8F00 !important; color: #FFFFFF !important; border-color: #FF8F00 !important; } +.card-menu-items { background: #F0F0F0 !important; border-color: #D0D0D0 !important; } +.card-byline a.user-link { color: #FF6F00 !important; } .btn-back { background: #FF6F00 !important; color: #FFFFFF !important; } .btn-back:hover { background: #FF8F00 !important; color: #FFFFFF !important; } .block-info-table td, .pm-info-table td { border-color: #E0E0E0 !important; } @@ -409,3 +411,7 @@ button.tribe-action-btn { background: transparent !important; border: 1px solid .oasis-pager-info { color: #888888; } .oasis-pager-btn { background: #FFFFFF; color: #555555 !important; border: 1px solid #E0E0E0; text-decoration: none !important; } .oasis-pager-btn:hover, .oasis-pager-btn.active { border-color: #2C2C2C; color: #2C2C2C !important; text-decoration: none !important; } + +.pdf-viewer-full { + background-color: #F9F9F9; +} diff --git a/src/client/assets/themes/Dark-SNH.css b/src/client/assets/themes/Dark-SNH.css index 6fe0855..591404f 100644 --- a/src/client/assets/themes/Dark-SNH.css +++ b/src/client/assets/themes/Dark-SNH.css @@ -98,6 +98,11 @@ div { border: 1px solid #333; } +:where(:is(div, li):has(> .card-top):not(.tribe-side)) div { + background-color: transparent; + border: 0; +} + div .header-content { width: 100%; } @@ -320,6 +325,7 @@ a.user-link:focus, button.user-link:focus { .btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #ffb733 !important; color: #000000 !important; } .btn-singleview.btn-content { background: #ffa300 !important; color: #1e1f23 !important; border: 1px solid #ffa300 !important; } .btn-singleview.btn-content:hover { background: #FFD700 !important; color: #1e1f23 !important; border-color: #FFD700 !important; } +.card-menu-items { background: #1e1f23 !important; border-color: #333333 !important; } .btn-back { background: #21232b !important; color: #ffa300 !important; } .btn-back:hover { background: #292b36 !important; color: #FFD700 !important; } .block-info-table td, .pm-info-table td { border-color: #444 !important; } diff --git a/src/client/assets/themes/Matrix-SNH.css b/src/client/assets/themes/Matrix-SNH.css index 1daec99..1a0f649 100644 --- a/src/client/assets/themes/Matrix-SNH.css +++ b/src/client/assets/themes/Matrix-SNH.css @@ -322,6 +322,8 @@ a.user-link:focus, button.user-link:focus { .btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #66FF66 !important; color: #000000 !important; } .btn-singleview.btn-content { background: #00FF00 !important; color: #000000 !important; border: 1px solid #00FF00 !important; } .btn-singleview.btn-content:hover { background: #000000 !important; color: #00FF00 !important; border-color: #00FF00 !important; } +.card-menu-items { background: #000000 !important; border-color: #00FF00 !important; } +.card-byline a.user-link { color: #00FF00 !important; } .btn-back { background: #000000 !important; color: #00FF00 !important; border: 1px solid #00FF00 !important; } .btn-back:hover { background: #00FF00 !important; color: #000000 !important; } .block-info-table td, .pm-info-table td { border-color: #00FF00 !important; } diff --git a/src/client/assets/themes/OasisMobile.css b/src/client/assets/themes/OasisMobile.css index 889157e..2073e8d 100644 --- a/src/client/assets/themes/OasisMobile.css +++ b/src/client/assets/themes/OasisMobile.css @@ -1220,16 +1220,12 @@ audio, video { max-width: 100% !important; } .module-table td:first-child a { white-space: normal !important; word-break: keep-all !important; overflow-wrap: normal !important; hyphens: none !important; } .module-table td, .module-table th { padding: 8px 6px !important; vertical-align: middle !important; } .module-table td:nth-child(2) p { font-size: .85em !important; margin: 0 !important; } -.pdf-canvas-wrap { height: auto !important; max-height: 70vh !important; overflow: auto !important; } -.pdf-viewer-container { padding: 8px !important; } +.pdf-frame { height: 75vh !important; min-height: 420px !important; } +.pdf-frame:target { height: 100vh !important; } +.pdf-viewer-container { padding: 8px !important; margin-bottom: 8px !important; } +.pdf-embedded { margin-bottom: 0 !important; } .pdf-viewer-container .pdf-controls { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; justify-content: center !important; align-items: center !important; margin-top: 8px !important; } -.pdf-viewer-container button { font-size: 1em !important; padding: 7px 10px !important; min-width: 42px !important; } -.pdf-viewer-container span { margin: 0 4px !important; } -.pdf-viewer-container { margin-bottom: 8px !important; } -.pdf-maximized { position: fixed !important; inset: 0 !important; z-index: 9999 !important; margin: 0 !important; border-radius: 0 !important; background: #111 !important; display: flex !important; flex-direction: column !important; padding: 6px !important; overflow: hidden !important; } -.pdf-maximized .pdf-canvas-wrap { flex: 1 1 auto !important; max-height: none !important; height: auto !important; overflow: auto !important; } -.pdf-maximized .pdf-controls { flex-shrink: 0 !important; margin: 6px 0 !important; } -.pdf-maximized-body { overflow: hidden !important; } +.pdf-viewer-container .pdf-controls a, .pdf-viewer-container .pdf-controls span { font-size: 1em !important; padding: 7px 10px !important; min-width: 42px !important; margin: 0 !important; text-align: center !important; box-sizing: border-box !important; text-decoration: none !important; } .main-content .feed-workspace { display: flex !important; flex-direction: column !important; gap: 10px !important; } .feed-workspace .feed-side { display: block !important; position: static !important; } @@ -1290,10 +1286,8 @@ audio, video { max-width: 100% !important; } .feed-card .refeed-column { flex-wrap: wrap !important; } .feed-card .refeed-column .muted { flex-basis: 100% !important; text-align: center !important; margin: 4px 0 0 0 !important; } .feed-card .card-engage summary, .feed-card .comments-link { min-height: 40px !important; padding: 8px 10px !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; justify-content: center !important; font-size: .85em !important; } -.comments-collapse .comments-inline-form { display: flex !important; flex-direction: column !important; gap: 6px !important; padding: 8px !important; } .comments-collapse textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; } .comments-collapse .filter-btn { width: auto !important; align-self: center !important; padding: 6px 18px !important; } -.comments-collapse .comments-view-all { display: block !important; text-align: center !important; padding: 6px !important; font-size: .8em !important; } .comments-collapse .comment-form { display: flex !important; flex-direction: column !important; gap: 6px !important; width: 100% !important; } .comments-collapse .comment-textarea { width: 100% !important; box-sizing: border-box !important; margin: 0 !important; } .comments-collapse .comment-file-upload { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } @@ -3411,7 +3405,7 @@ body > .header { :where(.main-column) section { margin-bottom: 10px; } -.main-column :is(form, section, div) > br { +.main-column :is(form, section, div):has(> :is(input, select, textarea, button, label)) > br { display: none !important; } .main-column .card-field > a.card-value.user-link { @@ -3604,6 +3598,10 @@ body > .header { width: 100% !important; text-align: center !important; } +.main-column .room-live-panel .room-live-head-main > .card-chips-row { + justify-content: center !important; + margin-bottom: 1em !important; +} .main-column .tribe-main:has(> .room-live-panel) { display: flex !important; flex-direction: column !important; @@ -4040,3 +4038,127 @@ body > .header { width: 100vw !important; height: 100vh !important; } +.main-column .tribe-side { flex-flow: row wrap !important; justify-content: center !important; align-items: center !important; column-gap: 8px !important; } +.main-column .tribe-side > * { flex: 0 0 100% !important; min-width: 0 !important; } +.main-column .tribe-side > .tribe-side-actions:not(.tribe-side-actions-bottom) { flex-wrap: nowrap !important; justify-content: center !important; gap: 8px !important; } +.main-column .tribe-side > .tribe-side-actions:not(.tribe-side-actions-bottom) button { font-size: 13px !important; padding: 6px 10px !important; white-space: nowrap !important; } +.main-column .tribe-side > .tribe-side-actions-bottom { order: 1 !important; flex: 0 0 auto !important; margin: 0 !important; } +.main-column .tribe-side > .tribe-side-subtribes { order: 2 !important; flex: 0 0 auto !important; margin: 0 !important; } +.main-column .tribe-side > .tribe-side-subtribes ~ .tribe-side-tags { order: 3 !important; } +.main-column .tribe-side > .tribe-side-subtribes { overflow: visible !important; } +.main-column .tribe-side > :is(.tribe-side-subtribes, .tribe-side-actions-bottom) :is(form, button) { margin-top: 0 !important; margin-bottom: 0 !important; } +.main-column .tribe-side > .tribe-members-count { order: 4 !important; text-align: center !important; } +.main-column .tribe-open-invite-panel { text-align: center !important; } +.main-column .tribe-open-invite-panel .invite-qr-card { margin-left: auto !important; margin-right: auto !important; } +.main-column .tribe-open-invite-panel > form { display: flex !important; justify-content: center !important; } +.main-column .tribe-open-invite-panel .tribe-open-invite { text-align: center !important; } +.main-column .tribe-open-invite-panel .tribe-open-invite-code { margin-bottom: 6px !important; } +.main-column .tribe-open-invite-panel .invite-qr-card { margin-top: 0 !important; margin-bottom: 6px !important; } +.main-column .tribe-open-invite-panel > form button { margin-top: 0 !important; } +.main-column .pm-card .pm-info-table tr { flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; } +.main-column .pm-card .pm-info-table td.card-label { flex: 0 0 auto !important; width: auto !important; } +.main-column .pm-card .pm-info-table td.card-value { flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; } +.main-column .pm-card .pm-info-table td.card-value > a.user-link { display: block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } +.main-column .pm-card .pm-info-table, .main-column .pm-card .pm-info-table > tbody { display: flex !important; flex-direction: column !important; } +.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell) { order: -1 !important; } +.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell) { flex-wrap: wrap !important; align-items: baseline !important; row-gap: 6px !important; } +.main-column .pm-card .pm-info-table tr:has(> .pm-subject-cell)::after { content: "" !important; order: -1 !important; flex: 0 0 100% !important; height: 0 !important; } +.main-column .pm-card .pm-subject-cell { display: contents !important; } +.main-column .pm-card .pm-subject-cell > .pm-exposition-chip { order: -2 !important; margin: 0 !important; } +.main-column .pm-card .pm-subject-cell > span:not(.pm-exposition-chip) { flex: 1 1 0 !important; min-width: 0 !important; } + +.main-column .card-header.card-top { display: flex !important; flex-wrap: nowrap !important; align-items: flex-start !important; justify-content: space-between !important; gap: 8px !important; width: auto !important; min-height: 0 !important; margin: 0 0 6px !important; padding: 0 !important; } +.main-column .card-header.card-top > .card-byline { display: flex !important; flex: 1 1 auto !important; flex-wrap: wrap !important; align-items: center !important; gap: 4px 6px !important; min-width: 0 !important; min-height: 32px !important; width: auto !important; margin: 0 !important; padding: 0 !important; } +.main-column .card-header.card-top .card-byline-avatar { display: inline-flex !important; flex: 0 0 auto !important; align-items: center !important; width: 24px !important; height: 32px !important; min-width: 0 !important; padding: 0 !important; margin: 0 !important; } +.main-column .card-header.card-top .card-byline-avatar img { width: 24px !important; height: 24px !important; min-height: 0 !important; max-height: 24px !important; border-radius: 50% !important; object-fit: cover !important; margin: 0 !important; } +.main-column .card-header.card-top .card-byline a.user-link { display: block !important; flex: 0 1 auto !important; width: auto !important; height: auto !important; min-width: min(72px, 100%) !important; max-width: calc(100% - 150px) !important; padding: 0 !important; margin: 0 !important; font-size: 12px !important; line-height: 16px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } +.main-column .card-header.card-top > .content-actions { display: flex !important; flex: 0 0 auto !important; flex-wrap: nowrap !important; align-items: center !important; gap: 4px !important; width: auto !important; margin: 0 0 0 auto !important; padding: 0 !important; } +.main-column .card-header.card-top > .content-actions > .card-menu { display: block !important; width: auto !important; margin: 0 !important; } +.main-column .card-header.card-top > .content-actions .btn-singleview:not(.card-menu-items *) { width: auto !important; min-width: 32px !important; height: 32px !important; margin: 0 !important; } +.main-column .card-header.card-top .card-menu > .card-menu-toggle.btn-singleview { background: #2a2a2a !important; border-radius: 50% !important; } +.main-column .card-header.card-top .card-menu > .card-menu-items { position: absolute !important; top: calc(100% + 4px) !important; right: 0 !important; left: auto !important; z-index: 700 !important; display: flex !important; flex-direction: column !important; flex-wrap: nowrap !important; gap: 0 !important; width: max-content !important; min-width: 200px !important; max-width: calc(100vw - 32px) !important; height: auto !important; margin: 0 !important; padding: 6px 0 !important; background: #1e1f23 !important; border: 1px solid #333 !important; border-radius: 8px !important; box-shadow: 0 6px 18px rgba(0,0,0,.45) !important; } +.main-column .card-header.card-top .card-menu-items :is(.content-action-form, .spread-form) { display: block !important; width: 100% !important; margin: 0 !important; padding: 0 !important; background: transparent !important; border: 0 !important; } +.main-column .card-header.card-top .card-menu-items .btn-singleview { display: flex !important; align-items: center !important; justify-content: flex-start !important; gap: 12px !important; width: 100% !important; min-width: 0 !important; height: 40px !important; margin: 0 !important; padding: 0 16px !important; border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; } +.main-column .card-header.card-top .card-menu-items .btn-singleview::after { content: attr(title); font-size: 13px; white-space: nowrap; } +.lightbox:not(.graphos-lightbox, .bank-eco-chart-lightbox, .live-lightbox) > .lightbox-close { right: 16px !important; } +.lightbox:not(.graphos-lightbox, .bank-eco-chart-lightbox, .live-lightbox) > .lightbox-download { right: 68px !important; } + +/* APK: barra de modulo compacta y fija */ +body:has(.apk-modbar), body:has(.apk-modbar) .main-content { overflow-x: clip !important; overflow-y: visible !important; } +.main-column > .apk-modbar { position: sticky !important; top: 60px !important; z-index: 800 !important; background: #121212 !important; display: flex !important; flex-direction: column !important; gap: 6px !important; width: auto !important; max-width: none !important; margin: -20px calc(50% - 50vw) 6px !important; padding: 12px calc(50vw - 50%) 6px !important; box-shadow: none !important; } +.main-column > .apk-modbar > .apk-modbar-row { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; min-width: 0 !important; width: 100% !important; } +.main-column > .apk-modbar > .apk-modbar-row > .apk-title { flex: 0 1 auto !important; min-width: 0 !important; margin: 0 !important; padding: 0 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; color: #ffa300 !important; line-height: 1.1 !important; } +.main-column > .apk-modbar > .apk-modbar-row > .tags-header { flex: 0 1 auto !important; margin: 0 !important; padding: 0 !important; border: 0 !important; background: transparent !important; } +.main-column > .apk-modbar > .apk-modbar-row > .tags-header > p { display: none !important; } +.main-column > .apk-modbar > .apk-modbar-row > a { flex: 0 0 auto !important; display: inline-flex !important; align-items: center !important; margin: 0 !important; } +.main-column > .apk-modbar > .apk-modbar-row > a > .pm-exposition-chip { margin: 0 !important; } +.main-column > .apk-modbar > .apk-modbar-foot { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 8px !important; min-width: 0 !important; width: 100% !important; } +.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > .apk-pin { flex: 0 0 auto !important; margin: 0 0 0 auto !important; } +.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > form.apk-pin { display: flex !important; padding: 0 !important; } +.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) :is(.apk-pin.create-button, .apk-pin .create-button) { width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 2px 10px !important; font-size: 11.25px !important; font-weight: 700 !important; line-height: 16.875px !important; border-radius: 12px !important; border: 1px solid #ffcc00 !important; background: #ffcc00 !important; color: #111 !important; white-space: nowrap !important; } +.main-column > .apk-modbar > .apk-strip { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; overflow-x: auto !important; scrollbar-width: none; gap: 6px !important; width: auto !important; max-width: none !important; margin: 0 calc((100% - 100vw) / 2) !important; padding: 0 calc((100vw - 100%) / 2) !important; border: 0 !important; background: transparent !important; overscroll-behavior: none !important; } +@supports (animation-timeline: scroll()) { +.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::before, .main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::after { position: sticky; z-index: 3; flex: 0 0 auto; align-self: stretch; width: 22px; padding-bottom: 3px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; color: #FFD700; font-size: 22px; font-weight: 700; line-height: 1; pointer-events: none; opacity: 0; animation-duration: auto; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: scroll(nearest inline); } +.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::before { content: "โ€น"; order: -1; left: calc((100% - 100vw) / 2); margin-right: -28px; justify-content: center; background: #121212; animation-name: apk-strip-left; } +.main-column > .apk-modbar > :is(.apk-strip, .apk-xrow)::after { content: "โ€บ"; right: calc((100% - 100vw) / 2); margin-left: -28px; justify-content: center; background: #121212; animation-name: apk-strip-right; } +@keyframes apk-strip-right { 0%, 92% { opacity: 1; } 100% { opacity: 0; } } +@keyframes apk-strip-left { 0% { opacity: 0; } 8%, 100% { opacity: 1; } } +} +.main-column > .apk-modbar > .apk-strip::-webkit-scrollbar { display: none; } +.main-column > .apk-modbar > .apk-strip :is(form, div) { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 6px !important; margin: 0 !important; padding: 0 !important; flex: 0 0 auto !important; width: auto !important; max-width: none !important; } +.main-column > .apk-modbar > .apk-strip :is(a, button, label) { flex: 0 0 auto !important; white-space: nowrap !important; margin: 0 !important; } +.main-column > .apk-modbar > .filter-box { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; width: 100% !important; } +.main-column > .apk-modbar > .filter-box .filter-box__input { flex: 1 1 auto !important; min-width: 0 !important; width: auto !important; height: 30px !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; font-size: 14px !important; } +.main-column > .apk-modbar > .filter-box .filter-box__controls { flex: 0 0 auto !important; width: auto !important; min-width: 0 !important; margin: 0 !important; display: flex !important; flex-direction: row !important; gap: 4px !important; } +.main-column > .apk-modbar > .filter-box .filter-box__select { height: 30px !important; padding-top: 0 !important; padding-bottom: 0 !important; width: auto !important; max-width: 110px !important; margin: 0 !important; font-size: 13px !important; } +.main-column > .apk-modbar > .filter-box .filter-box__button { font-size: 0 !important; width: 36px !important; min-width: 36px !important; height: 30px !important; padding: 0 !important; margin: 0 !important; } +.main-column > .apk-modbar > .filter-box .filter-box__button::after { content: "โŒ•"; font-size: 22px; line-height: 1; color: #FFD700; font-weight: 700; } +.main-column > .apk-modbar > :is(.apk-modbar-row, .apk-modbar-foot) > .apk-pin.create-button { margin: 0 0 0 auto !important; } +.main-column > .apk-modbar > .filter-box:has(.apk-more-fields) { flex-wrap: wrap !important; row-gap: 6px !important; } +.main-column > .apk-modbar .apk-more-toggle { display: none !important; } +.main-column > .apk-modbar .apk-more-btn { flex: 0 0 auto !important; display: flex !important; align-items: center !important; justify-content: center !important; width: 36px !important; height: 30px !important; margin: 0 !important; padding: 0 !important; font-size: 18px !important; line-height: 1 !important; color: #FFD700 !important; background: #2a2a2a !important; border: 1px solid #444 !important; border-radius: 8px !important; cursor: pointer !important; } +.main-column > .apk-modbar .apk-more-btn::after { content: "โŠ•"; } +.main-column > .apk-modbar .filter-box:has(.apk-more-toggle:checked) .apk-more-btn::after { content: "โŠ–"; } +.main-column > .apk-modbar .apk-more-fields { display: none !important; order: 99 !important; flex: 0 0 100% !important; flex-wrap: wrap !important; gap: 6px !important; margin: 0 !important; padding: 0 !important; border: 0 !important; border-radius: 0 !important; overflow: visible !important; background: transparent !important; } +.main-column > .apk-modbar .apk-more-toggle:checked ~ .apk-more-fields { display: flex !important; } +.main-column > .apk-modbar .apk-more-fields > :is(input, select) { background: rgba(255, 255, 255, 0.12) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; border-radius: 4px !important; color: #FFD700 !important; font-size: 14px !important; padding: 0 12px !important; } +.main-column > .apk-modbar .apk-more-fields > select:not(#apk-z) { -webkit-appearance: none !important; appearance: none !important; box-sizing: border-box !important; height: 32px !important; padding: 0 30px 0 12px !important; border-radius: 4px !important; text-align: left !important; background-color: rgba(255, 255, 255, 0.12) !important; background-image: linear-gradient(45deg, transparent 50%, #FFD700 50%), linear-gradient(135deg, #FFD700 50%, transparent 50%) !important; background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55% !important; background-size: 5px 5px, 5px 5px !important; background-repeat: no-repeat !important; } +.main-column > .apk-modbar .apk-more-fields > input:not(#apk-z) { box-sizing: border-box !important; height: 32px !important; } +.main-column > .apk-modbar .apk-more-toggle:checked ~ .apk-more-fields:has(> .apk-more-field) { display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 6px 8px !important; } +.main-column > .apk-modbar .apk-more-fields > .apk-more-field { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 2px !important; min-width: 0 !important; height: auto !important; background: transparent !important; border: 0 !important; border-radius: 0 !important; overflow: visible !important; padding: 0 !important; } +.main-column > .apk-modbar .apk-more-field > .apk-more-label { flex: 0 0 auto !important; font-size: 11px !important; line-height: 1.2 !important; color: #FFD700 !important; text-align: left !important; opacity: 0.85 !important; } +.main-column > .apk-modbar .apk-more-field > :is(input, select):not(#apk-z) { flex: 0 0 auto !important; min-width: 0 !important; width: 100% !important; box-sizing: border-box !important; height: 32px !important; margin: 0 !important; padding: 0 8px !important; font-size: 13px !important; color: #FFD700 !important; background-color: rgba(255, 255, 255, 0.12) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; border-radius: 4px !important; } +.main-column > .apk-modbar .apk-more-field > select:not(#apk-z) { -webkit-appearance: none !important; appearance: none !important; padding: 0 30px 0 12px !important; background-image: linear-gradient(45deg, transparent 50%, #FFD700 50%), linear-gradient(135deg, #FFD700 50%, transparent 50%) !important; background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55% !important; background-size: 5px 5px, 5px 5px !important; background-repeat: no-repeat !important; } +.main-column > .apk-modbar .apk-more-fields input:is([type="datetime-local"], [type="date"], [type="time"])::-webkit-calendar-picker-indicator { display: none !important; -webkit-appearance: none !important; } +.main-column > .apk-modbar .apk-more-field > input::placeholder { color: rgba(255, 215, 0, 0.75) !important; opacity: 1 !important; } +.main-column > .apk-modbar .apk-more-fields > input::placeholder { color: rgba(255, 215, 0, 0.75) !important; opacity: 1 !important; } +.main-column > .apk-modbar .apk-more-toggle:checked ~ .filter-box__controls .apk-more-btn, .main-column > .apk-modbar .filter-box:has(.apk-more-toggle:checked) .apk-more-btn { border-color: #FFD700 !important; } +.main-column > .apk-modbar .apk-more-fields > * { flex: 1 1 100% !important; min-width: 0 !important; width: auto !important; max-width: none !important; height: 30px !important; margin: 0 !important; font-size: 14px !important; } +body .main-column > .apk-modbar > .filter-box input[type="hidden"] { display: none !important; margin: 0 !important; } +.main-column > .apk-modbar > .filter-box :is(input.filter-box__input, select, .filter-box__controls, .apk-more-fields > *):not(#apk-z) { margin: 0 !important; } +.main-column > .apk-modbar > .apk-xrow { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; scrollbar-width: none; overscroll-behavior: none !important; align-items: center !important; gap: 4px !important; width: auto !important; max-width: none !important; margin: 0 calc((100% - 100vw) / 2) !important; padding: 0 calc((100vw - 100%) / 2) !important; border: 0 !important; background: transparent !important; } +.main-column > .apk-modbar > .apk-xrow::-webkit-scrollbar { display: none; } +.main-column > .apk-modbar > .apk-xrow > * { flex: 0 0 auto !important; margin: 0 !important; } +.main-column > .apk-modbar > .apk-xrow.inbox-exposition > .inbox-filters-label { display: none !important; } +.main-column > .apk-modbar > .apk-xrow .inbox-bulk-delete { display: none !important; } +.main-column > .apk-banners { display: flex !important; flex-direction: column !important; gap: 6px !important; margin: 0 0 18px !important; } +.main-column > .apk-banners:has(+ .apk-stats) { margin-bottom: 8px !important; } +.main-column > .apk-banners > * { margin: 0 !important; } +.main-column > .apk-banners .welcome-banner-start { background: #FFA300 !important; border-color: #FFA300 !important; color: #111 !important; text-transform: uppercase !important; white-space: nowrap !important; width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 5px 14px !important; font-size: 13px !important; font-weight: 700 !important; line-height: 1.2 !important; border-radius: 14px !important; } +.main-column > .apk-modbar > .apk-modbar-foot > .module-stats-line.apk-stats { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; padding: 0 !important; display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 4px 8px !important; font-size: .72em !important; white-space: nowrap !important; text-transform: uppercase !important; background: transparent !important; border: 0 !important; } +.main-column .activity-toolbar-row:not(:has(*)) { display: none !important; } +.main-column .inbox-exposition { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; scrollbar-width: none; align-items: center !important; gap: 4px !important; margin: 0 0 10px !important; padding: 0 !important; border: 0 !important; background: transparent !important; } +.main-column .inbox-exposition > .inbox-filters-label { flex: 0 0 auto !important; margin: 0 !important; font-size: 11.25px !important; } +.main-column .inbox-exposition > form { flex: 0 0 auto !important; } +.main-column .inbox-exposition::-webkit-scrollbar { display: none; } +.main-column .inbox-exposition > form { display: inline-flex !important; margin: 0 !important; padding: 0 !important; width: auto !important; } +.main-column .inbox-exposition .btn { flex: 0 0 auto !important; width: auto !important; height: auto !important; min-height: 0 !important; margin: 0 !important; padding: 2px 7px !important; font-size: 11.25px !important; font-weight: 400 !important; line-height: 16.875px !important; border-radius: 12px !important; border: 1px solid #555 !important; background: transparent !important; color: #FFD700 !important; white-space: nowrap !important; text-transform: uppercase !important; } +.main-column .inbox-exposition .btn.delete-btn { color: #e74c3c !important; border-color: #e74c3c !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-description { order: 1 !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > :is(.tribe-side-actions:not(.tribe-side-actions-bottom), .tribe-open-invite-panel) { order: 3 !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-tags { order: 2 !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-members-count { order: 4 !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-actions-bottom { order: 5 !important; } +.main-column .tribe-side:has(> h2.tribe-members-count):not(#apk-z) > .tribe-side-subtribes { order: 6 !important; } +.main-column .tribe-details:has(> .tribe-side):not(#apk-z) { background: transparent !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; } +.main-column .tribe-details:has(> .tribe-side):not(#apk-z) > .tribe-side { background: #1e1e1e !important; border-radius: 14px !important; padding: 12px !important; margin: 0 0 12px !important; } diff --git a/src/client/assets/themes/Purple-SNH.css b/src/client/assets/themes/Purple-SNH.css index 0f50ff7..9e8cfbc 100644 --- a/src/client/assets/themes/Purple-SNH.css +++ b/src/client/assets/themes/Purple-SNH.css @@ -357,6 +357,8 @@ a.user-link:focus, button.user-link:focus { .btn-spread-on:hover, .content-actions .btn-pin-on:hover { background: #CE8CE9 !important; color: #2A0E42 !important; } .btn-singleview.btn-content { background: #B86ADE !important; color: #4B1A72 !important; border: 1px solid #B86ADE !important; } .btn-singleview.btn-content:hover { background: #FFD600 !important; color: #4B1A72 !important; border-color: #FFD600 !important; } +.card-menu-items { background: #4B1A72 !important; border-color: #5A1A85 !important; } +.card-byline a.user-link { color: #FFD600 !important; } .btn-back { background: #A34AD8 !important; color: #FFFFFF !important; } .btn-back:hover { background: #751E9F !important; color: #FFFFFF !important; } .block-info-table td, .pm-info-table td { border-color: #B86ADE !important; } diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js index 1b32cf2..acc09cd 100644 --- a/src/client/assets/translations/i18n_fork.js +++ b/src/client/assets/translations/i18n_fork.js @@ -81,7 +81,9 @@ module.exports = { wfHint_mobile: "The basics for a phone", workflow_acampada: "Camp", workflow_activista: "Activist", - wfHint_activista: "Direct action: call out, coordinate, phone and alert", + wfHint_activista: "For a collective: everything in Camp plus campaigns, alerts, polls, calendar and tasks", + workflow_minimal: "Minimal", + wfHint_minimal: "Direct action: call out, coordinate, phone and alert", wfNameMobile_activists: "Movement", wfModuleCount: "{n} modules", activityFilter: "Filter", @@ -222,7 +224,9 @@ module.exports = { wfHint_mobile: "Lo bรกsico para el mรณvil", workflow_acampada: "Acampada", workflow_activista: "Activista", - wfHint_activista: "Acciรณn directa: convocar, coordinarse, llamar y avisar", + wfHint_activista: "Para un colectivo: lo de Acampada mรกs campaรฑas, alertas, encuestas, calendario y tareas", + workflow_minimal: "Mรญnimo", + wfHint_minimal: "Acciรณn directa: convocar, coordinarse, llamar y avisar", wfNameMobile_activists: "Movimiento", wfModuleCount: "{n} mรณdulos", activityFilter: "Filtrar", @@ -369,7 +373,9 @@ module.exports = { wfHint_mobile: "L'essentiel pour le tรฉlรฉphone", workflow_acampada: "Campement", workflow_activista: "Activiste", - wfHint_activista: "Action directe : appeler, se coordonner, tรฉlรฉphoner et alerter", + wfHint_activista: "Pour un collectif : tout Campement plus campagnes, alertes, sondages, calendrier et tรขches", + workflow_minimal: "Minimal", + wfHint_minimal: "Action directe : appeler, se coordonner, tรฉlรฉphoner et alerter", wfNameMobile_activists: "Mouvement", wfModuleCount: "{n} modules", activityFilter: "Filtrer", @@ -510,7 +516,9 @@ module.exports = { wfHint_mobile: "Mugikorrerako oinarrizkoa", workflow_acampada: "Kanpaldia", workflow_activista: "Aktibista", - wfHint_activista: "Ekintza zuzena: deitu, koordinatu, telefonoz hitz egin eta abisatu", + wfHint_activista: "Kolektibo baterako: Kanpaldiko guztia gehi kanpainak, alertak, inkestak, egutegia eta zereginak", + workflow_minimal: "Gutxienekoa", + wfHint_minimal: "Ekintza zuzena: deitu, koordinatu, telefonoz hitz egin eta abisatu", wfNameMobile_activists: "Mugimendua", wfModuleCount: "{n} modulu", activityFilter: "Iragazi", @@ -651,7 +659,9 @@ module.exports = { wfHint_mobile: "Das Wichtigste fรผrs Handy", workflow_acampada: "Camp", workflow_activista: "Aktivist*in", - wfHint_activista: "Direkte Aktion: aufrufen, koordinieren, telefonieren und warnen", + wfHint_activista: "Fรผr ein Kollektiv: alles aus Camp plus Kampagnen, Warnungen, Umfragen, Kalender und Aufgaben", + workflow_minimal: "Minimal", + wfHint_minimal: "Direkte Aktion: aufrufen, koordinieren, telefonieren und warnen", wfNameMobile_activists: "Bewegung", wfModuleCount: "{n} Module", activityFilter: "Filtern", @@ -792,7 +802,9 @@ module.exports = { wfHint_mobile: "L'essenziale per il telefono", workflow_acampada: "Accampata", workflow_activista: "Attivista", - wfHint_activista: "Azione diretta: convocare, coordinarsi, chiamare e avvisare", + wfHint_activista: "Per un collettivo: tutto di Accampamento piรน campagne, allerte, sondaggi, calendario e compiti", + workflow_minimal: "Minimo", + wfHint_minimal: "Azione diretta: convocare, coordinarsi, chiamare e avvisare", wfNameMobile_activists: "Movimento", wfModuleCount: "{n} moduli", activityFilter: "Filtra", @@ -933,7 +945,9 @@ module.exports = { wfHint_mobile: "O bรกsico para o telemรณvel", workflow_acampada: "Acampada", workflow_activista: "Ativista", - wfHint_activista: "Aรงรฃo direta: convocar, coordenar, ligar e avisar", + wfHint_activista: "Para um coletivo: tudo do Acampamento mais campanhas, alertas, sondagens, calendรกrio e tarefas", + workflow_minimal: "Mรญnimo", + wfHint_minimal: "Aรงรฃo direta: convocar, coordenar, ligar e avisar", wfNameMobile_activists: "Movimento", wfModuleCount: "{n} mรณdulos", activityFilter: "Filtrar", @@ -1074,7 +1088,9 @@ module.exports = { wfHint_mobile: "ๆ‰‹ๆœบๅŸบๆœฌๅŠŸ่ƒฝ", workflow_acampada: "่ฅๅœฐ", workflow_activista: "ๆดปๅŠจ่€…", - wfHint_activista: "็›ดๆŽฅ่กŒๅŠจ๏ผšๅฌ้›†ใ€ๅ่ฐƒใ€้€š่ฏๅ’Œ้ข„่ญฆ", + wfHint_activista: "้€‚ๅˆๅ›ขไฝ“๏ผš่ฅๅœฐ็š„ๅ…จ้ƒจๅŠŸ่ƒฝ๏ผŒๅŠ ไธŠ่ฟๅŠจใ€้ข„่ญฆใ€ๆŠ•็ฅจใ€ๆ—ฅๅކๅ’ŒไปปๅŠก", + workflow_minimal: "ๆž็ฎ€", + wfHint_minimal: "็›ดๆŽฅ่กŒๅŠจ๏ผšๅฌ้›†ใ€ๅ่ฐƒใ€้€š่ฏๅ’Œ้ข„่ญฆ", wfNameMobile_activists: "่ฟๅŠจ", wfModuleCount: "{n} ไธชๆจกๅ—", activityFilter: "็ญ›้€‰", @@ -1215,7 +1231,9 @@ module.exports = { wfHint_mobile: "ุงู„ุฃุณุงุณูŠุงุช ู„ู„ู‡ุงุชู", workflow_acampada: "ู…ุฎูŠู…", workflow_activista: "ู†ุงุดุท", - wfHint_activista: "ุนู…ู„ ู…ุจุงุดุฑ: ุฏุนูˆุฉ ูˆุชู†ุณูŠู‚ ูˆุงุชุตุงู„ ูˆุชู†ุจูŠู‡", + wfHint_activista: "ู„ู…ุฌู…ูˆุนุฉ: ูƒู„ ู…ุง ููŠ ุงู„ู…ุฎูŠู… ู…ุน ุงู„ุญู…ู„ุงุช ูˆุงู„ุชู†ุจูŠู‡ุงุช ูˆุงู„ุงุณุชุทู„ุงุนุงุช ูˆุงู„ุชู‚ูˆูŠู… ูˆุงู„ู…ู‡ุงู…", + workflow_minimal: "ุงู„ุฃุฏู†ู‰", + wfHint_minimal: "ุนู…ู„ ู…ุจุงุดุฑ: ุฏุนูˆุฉ ูˆุชู†ุณูŠู‚ ูˆุงุชุตุงู„ ูˆุชู†ุจูŠู‡", wfNameMobile_activists: "ุงู„ุญุฑูƒุฉ", wfModuleCount: "{n} ูˆุญุฏุงุช", activityFilter: "ุชุตููŠุฉ", @@ -1356,7 +1374,9 @@ module.exports = { wfHint_mobile: "เคซเคผเฅ‹เคจ เค•เฅ‡ เคฒเคฟเค เคฌเฅเคจเคฟเคฏเคพเคฆเฅ€", workflow_acampada: "เคถเคฟเคตเคฟเคฐ", workflow_activista: "เค•เคพเคฐเฅเคฏเค•เคฐเฅเคคเคพ", - wfHint_activista: "เคธเฅ€เคงเฅ€ เค•เคพเคฐเฅเคฐเคตเคพเคˆ: เคฌเฅเคฒเคพเคจเคพ, เคคเคพเคฒเคฎเฅ‡เคฒ, เคซเคผเฅ‹เคจ เค”เคฐ เคšเฅ‡เคคเคพเคตเคจเฅ€", + wfHint_activista: "เคเค• เคธเคฎเฅ‚เคน เค•เฅ‡ เคฒเคฟเค: เคถเคฟเคตเคฟเคฐ เค•เคพ เคธเคฌ เค•เฅเค›, เคธเคพเคฅ เคฎเฅ‡เค‚ เค…เคญเคฟเคฏเคพเคจ, เคšเฅ‡เคคเคพเคตเคจเฅ€, เคธเคฐเฅเคตเฅ‡, เค•เฅˆเคฒเฅ‡เค‚เคกเคฐ เค”เคฐ เค•เคพเคฎ", + workflow_minimal: "เคจเฅเคฏเฅ‚เคจเคคเคฎ", + wfHint_minimal: "เคธเฅ€เคงเฅ€ เค•เคพเคฐเฅเคฐเคตเคพเคˆ: เคฌเฅเคฒเคพเคจเคพ, เคคเคพเคฒเคฎเฅ‡เคฒ, เคซเคผเฅ‹เคจ เค”เคฐ เคšเฅ‡เคคเคพเคตเคจเฅ€", wfNameMobile_activists: "เค†เค‚เคฆเฅ‹เคฒเคจ", wfModuleCount: "{n} เคฎเฅ‰เคกเฅเคฏเฅ‚เคฒ", activityFilter: "เคซเคผเคฟเคฒเฅเคŸเคฐ", @@ -1497,7 +1517,9 @@ module.exports = { wfHint_mobile: "ะžัะฝะพะฒะฝะพะต ะดะปั ั‚ะตะปะตั„ะพะฝะฐ", workflow_acampada: "ะ›ะฐะณะตั€ัŒ", workflow_activista: "ะะบั‚ะธะฒะธัั‚", - wfHint_activista: "ะŸั€ัะผะพะต ะดะตะนัั‚ะฒะธะต: ัะพะทะฒะฐั‚ัŒ, ะบะพะพั€ะดะธะฝะธั€ะพะฒะฐั‚ัŒ, ะทะฒะพะฝะธั‚ัŒ ะธ ะฟั€ะตะดัƒะฟั€ะตะถะดะฐั‚ัŒ", + wfHint_activista: "ะ”ะปั ะบะพะปะปะตะบั‚ะธะฒะฐ: ะฒัั‘ ะธะท ะ›ะฐะณะตั€ั ะฟะปัŽั ะบะฐะผะฟะฐะฝะธะธ, ั‚ั€ะตะฒะพะณะธ, ะพะฟั€ะพัั‹, ะบะฐะปะตะฝะดะฐั€ัŒ ะธ ะทะฐะดะฐั‡ะธ", + workflow_minimal: "ะœะธะฝะธะผัƒะผ", + wfHint_minimal: "ะŸั€ัะผะพะต ะดะตะนัั‚ะฒะธะต: ัะพะทะฒะฐั‚ัŒ, ะบะพะพั€ะดะธะฝะธั€ะพะฒะฐั‚ัŒ, ะทะฒะพะฝะธั‚ัŒ ะธ ะฟั€ะตะดัƒะฟั€ะตะถะดะฐั‚ัŒ", wfNameMobile_activists: "ะ”ะฒะธะถะตะฝะธะต", wfModuleCount: "{n} ะผะพะดัƒะปะตะน", activityFilter: "ะคะธะปัŒั‚ั€", diff --git a/src/client/assets/translations/oasis_ar.js b/src/client/assets/translations/oasis_ar.js index c706560..e05261a 100644 --- a/src/client/assets/translations/oasis_ar.js +++ b/src/client/assets/translations/oasis_ar.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† ุดุจูƒุชูŠ", graphosShowing: "ุนุฑุถ", graphosYou: "ุฃู†ุช", - graphosTotalNodes: "ุฅุฌู…ุงู„ูŠ ุงู„ุนู‚ุฏ", + graphosTotalNodes: "ุงู„ุฅุฌู…ุงู„ูŠ", + onlinePlural: "ู…ุชุตู„ูˆู†", mentions: "ุงู„ุฅุดุงุฑุงุช", mentionsDescription: [ strong("ูƒู„ ุฑุณุงู„ุฉ ุชุฐูƒุฑูƒ ุจู€ @"), @@ -420,6 +421,7 @@ module.exports = { menuMedia: "ุงู„ู…ูƒุชุจุฉ", menuTools: "ุงู„ุฃุฏูˆุงุช", comment: "ุชุนู„ูŠู‚", + typeComment: "ุชุนู„ูŠู‚", subtopic: "ู…ูˆุถูˆุน ูุฑุนูŠ", json: "JSON", unfollow: "ุฅู„ุบุงุก ุงู„ุฏุนู…", @@ -449,9 +451,6 @@ module.exports = { publishCustomDescription: [ "ุชุฐูƒู‘ุฑ: ุจุณุจุจ ุชู‚ู†ูŠุฉ ุงู„ุจู„ูˆูƒุชุดูŠู†ุŒ ุจู…ุฌุฑุฏ ู†ุดุฑ ุงู„ู…ู†ุดูˆุฑ ู„ุง ูŠู…ูƒู† ุชุนุฏูŠู„ู‡ ุฃูˆ ุญุฐูู‡.", ], - commentWarning: [ - "ุชุฐูƒู‘ุฑ: ุจุณุจุจ ุชู‚ู†ูŠุฉ ุงู„ุจู„ูˆูƒุชุดูŠู†ุŒ ุจู…ุฌุฑุฏ ู†ุดุฑ ุงู„ู…ู†ุดูˆุฑ ู„ุง ูŠู…ูƒู† ุชุนุฏูŠู„ู‡ ุฃูˆ ุญุฐูู‡.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -730,10 +729,6 @@ module.exports = { votesQuestionLabel:"ุงู„ุณุคุงู„", voteStatusOpen:"ู…ูุชูˆุญ", voteStatusClosed:"ู…ุบู„ู‚", - commentDescription: ({ parentUrl }) => [ - " ุนู„ู‘ู‚ ุนู„ู‰ ", - a({ href: parentUrl }, " ุงู„ู…ุญุงุฏุซุฉ"), - ], commentTitle: ({ authorName }) => [`ุชุนู„ูŠู‚ ุนู„ู‰ ู…ู†ุดูˆุฑ @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " ุฃู†ุดุฃ ู…ูˆุถูˆุนู‹ุง ูุฑุนูŠู‹ุง ู…ู† ", @@ -868,6 +863,8 @@ module.exports = { bankRulesEligibility5: "ูŠุฏูุน ุงู„ู€ PUB ููŠ ุฏูˆุฑุชู‡ ุงู„ุชุงู„ูŠุฉ ูˆูŠู†ุดุฑ ุชุญูˆูŠู„ ุฏุฎู„ ุฃุณุงุณูŠ ูŠู…ูƒู†ูƒ ุชุฃูƒูŠุฏู‡.", bankRulesEligibility6: "ุฅุฐุง ุบุงุจ ุนู†ูˆุงู†ูƒ ุฃูˆ ูƒุงู†ุช ุงู„ุชุบุฐูŠุฉ ุญุฏูŠุซุฉ ุฌุฏุงู‹ ุฃูˆ ุฑูุถุช ุงู„ุดู‡ุฑ ุฃูˆ ู„ู… ูŠูƒู† ู„ุฏู‰ ุงู„ู€ PUB ุฃู…ูˆุงู„ุŒ ุชูุชุฎุทู‰ ุงู„ู…ุทุงู„ุจุฉ ูˆุชูุณุฌูŽู‘ู„ ุนู„ู‰ ุงู„ู€ PUB.", copyLinkTitle: "ู†ุณุฎ ุงู„ุฑุงุจุท", + cardMenuTitle: "ุฅุฌุฑุงุกุงุช ุฃุฎุฑู‰", + shareContent: "ู…ุดุงุฑูƒุฉ", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "ุฑูุถ ุงู„ุฏุฎู„ ุงู„ุฃุณุงุณูŠ", @@ -901,7 +898,7 @@ module.exports = { walletNotConfigured: "ู…ุญูุธุฉ ECOin ุงู„ุฎุงุตุฉ ุจูƒ ุบูŠุฑ ู…ูู‡ูŠุฃุฉ ุจุนุฏ.", walletSettingsDescription: "ุฏู…ุฌ Oasis ู…ุน ู…ุญูุธุฉ ECOin ุงู„ุฎุงุตุฉ ุจูƒ.", walletSettingsMobileDescription: "ุงุฑุจุท Oasis ุจุชุทุจูŠู‚ ECOin Wallet ุนู„ู‰ ู‡ุฐุง ุงู„ุฌู‡ุงุฒ.", - walletDownloadEcoin: "ู†ุฒู‘ู„ ECOin!", + walletDownloadEcoin: "ู†ุฒู‘ู„ ECOin", walletRpcUrl: "ุนู†ูˆุงู† RPC", walletStatusMessages: { invalid_amount: "ู…ุจู„ุบ ุบูŠุฑ ุตุงู„ุญ", @@ -954,6 +951,7 @@ module.exports = { bookmarkUpdateButton: "ุชุญุฏูŠุซ", bookmarkAddFavoriteButton: "ุฅุถุงูุฉ ู„ู„ู…ูุถู„ุฉ", bookmarkRemoveFavoriteButton: "ุฅุฒุงู„ุฉ ู…ู† ุงู„ู…ูุถู„ุฉ", + bookmarkTitleLabel: "ุงู„ุนู†ูˆุงู†", bookmarkUrlLabel: "ุงู„ุฑุงุจุท", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "ุงู„ูˆุตู", @@ -1098,9 +1096,9 @@ module.exports = { allCVs: "ุงู„ุณูŠุฑ ุงู„ุฐุงุชูŠุฉ", discoverPeople: "ุงูƒุชุดู ุงู„ุณูƒุงู† ููŠ ุดุจูƒุชูƒ.", searchInhabitantsPlaceholder: "ุงุจุญุซ ููŠ ุงู„ุณูƒุงู†...", - filterLocation: "ุชุตููŠุฉ ุงู„ุณูƒุงู† ุญุณุจ ุงู„ู…ูˆู‚ุน โ€ฆ", - filterLanguage: "ุชุตููŠุฉ ุงู„ุณูƒุงู† ุญุณุจ ุงู„ู„ุบุฉ โ€ฆ", - filterSkills: "ุชุตููŠุฉ ุงู„ุณูƒุงู† ุญุณุจ ุงู„ู…ู‡ุงุฑุงุช โ€ฆ", + filterLocation: "ุงุจุญุซ ุญุณุจ ุงู„ู…ูˆู‚ุนโ€ฆ", + filterLanguage: "ุงุจุญุซ ุญุณุจ ุงู„ู„ุบุฉโ€ฆ", + filterSkills: "ุงุจุญุซ ุญุณุจ ุงู„ู…ู‡ุงุฑุงุชโ€ฆ", cvSkillsCount: "ุงู„ู…ู‡ุงุฑุงุช", applyFilters: "ุชุทุจูŠู‚ ุงู„ูู„ุงุชุฑ", locationLabel: "ุงู„ู…ูˆู‚ุน", @@ -1116,6 +1114,7 @@ module.exports = { topactivitySectionTitle: "ุงู„ุฃูƒุซุฑ ู†ุดุงุทู‹ุง", "TOP INACTIVITYButton": "ุงู„ุฃู‚ู„ ู†ุดุงุทุงู‹", topinactivitySectionTitle: "ุงู„ุณูƒุงู† ุงู„ุฃู‚ู„ ู†ุดุงุทุงู‹", + contactsSectionTitle: "ุฌู‡ุงุช ุงู„ุงุชุตุงู„", blockedSectionTitle: "ุงู„ู…ุญุธูˆุฑูˆู†", gallerySectionTitle: "ุงู„ู…ุนุฑุถ", topSectionTitle: "ุงู„ุฃุนู„ู‰", @@ -1569,9 +1568,7 @@ module.exports = { voteUnknownDate: "ุชุงุฑูŠุฎ ุบูŠุฑ ู…ุนุฑูˆู", voteDetailSectionTitle: 'ุชูุงุตูŠู„ ุงู„ุชุตูˆูŠุช', voteCommentsLabel: 'ุงู„ุชุนู„ูŠู‚ุงุช', - voteCommentsForumButton: 'ูุชุญ ุงู„ู†ู‚ุงุด', - voteCommentsSectionTitle: 'ูุชุญ ุงู„ู†ู‚ุงุด', - voteNoCommentsYet: 'ู„ุง ุชูˆุฌุฏ ุชุนู„ูŠู‚ุงุช ุจุนุฏ. ูƒู† ุฃูˆู„ ู…ู† ูŠุฑุฏ.', + voteNoCommentsYet: 'ู„ุง ุชูˆุฌุฏ ุชุนู„ูŠู‚ุงุช ุจุนุฏ.', commentsGoToLast: "ุงุฐู‡ุจ ุฅู„ู‰ ุขุฎุฑ ุชุนู„ูŠู‚", voteNewCommentPlaceholder: 'ุงูƒุชุจ ุชุนู„ูŠู‚ูƒ ู‡ู†ุงโ€ฆ', voteNewCommentButton: 'ู†ุดุฑ ุงู„ุชุนู„ูŠู‚', @@ -1698,7 +1695,6 @@ module.exports = { pollVoteButton: "ุตูˆู‘ุช", pollChangeVote: "ุชุบูŠูŠุฑ ุงู„ุตูˆุช", pollVoters: "ุงู„ู…ุตูˆู‘ุชูˆู†", - pollComments: "ุงู„ุชุนู„ูŠู‚ุงุช", pollStatusLabel: "ุงู„ุญุงู„ุฉ", pollStatusOpen: "ู…ูุชูˆุญ", pollStatusClosed: "ู…ุบู„ู‚", @@ -1720,9 +1716,22 @@ module.exports = { blogAllowComments: "ุงู„ุณู…ุงุญ ุจุงู„ุชุนู„ูŠู‚ุงุช", blogCommentsClosed: "ุฃุบู„ู‚ ุงู„ูƒุงุชุจ ุงู„ุชุนู„ูŠู‚ุงุช ุนู„ู‰ ู‡ุฐู‡ ุงู„ู…ุฏูˆู†ุฉ.", blogNoItems: "ู„ู… ูŠุชู… ุงู„ุนุซูˆุฑ ุนู„ู‰ ู…ุฏูˆู†ุงุช.", - viewBlog: "ุนุฑุถ ุงู„ู…ุฏูˆู†ุฉ", forumTitle: "ุงู„ู…ู†ุชุฏูŠุงุช", forumCategoryLabel: "ุงู„ูุฆุฉ", + forumCatGENERAL: "ุนุงู…", + forumCatPOLITICS: "ุณูŠุงุณุฉ", + forumCatTECH: "ุชู‚ู†ูŠุฉ", + forumCatSCIENCE: "ุนู„ูˆู…", + forumCatMUSIC: "ู…ูˆุณูŠู‚ู‰", + forumCatART: "ูู†", + forumCatGAMING: "ุฃู„ุนุงุจ", + forumCatBOOKS: "ูƒุชุจ", + forumCatFILMS: "ุฃูู„ุงู…", + forumCatPHILOSOPHY: "ูู„ุณูุฉ", + forumCatSOCIETY: "ู…ุฌุชู…ุน", + forumCatPRIVACY: "ุฎุตูˆุตูŠุฉ", + forumCatCYBERWARFARE: "ุญุฑุจ ุณูŠุจุฑุงู†ูŠุฉ", + forumCatSURVIVALISM: "ุงู„ุจู‚ุงุก", forumTitleLabel: "ุงู„ุนู†ูˆุงู†", forumTitlePlaceholder: "ุนู†ูˆุงู† ุงู„ู…ู†ุชุฏู‰...", forumCreateButton: "ุฅู†ุดุงุก ู…ู†ุชุฏู‰", @@ -1991,43 +2000,6 @@ module.exports = { reportsUnderReviewSectionTitle: "ุงู„ุชู‚ุงุฑูŠุฑ ู‚ูŠุฏ ุงู„ู…ุฑุงุฌุนุฉ", reportsResolvedSectionTitle: "ุงู„ุชู‚ุงุฑูŠุฑ ุงู„ู…ุญู„ูˆู„ุฉ", reportsInvalidSectionTitle: "ุงู„ุชู‚ุงุฑูŠุฑ ุบูŠุฑ ุงู„ุตุงู„ุญุฉ", - reportsBugTemplateTitle: 'ุชูุงุตูŠู„ ุฅุนุงุฏุฉ ุงู„ุฅู†ุชุงุฌ (ุงู„ุฃุฎุทุงุก)', - reportsFeatureTemplateTitle: 'ุงู„ุชูุงุตูŠู„ (ุงู„ู…ูŠุฒุงุช)', - reportsAbuseTemplateTitle: 'ุงู„ุชูุงุตูŠู„ (ุงู„ุฅุณุงุกุฉ)', - reportsContentTemplateTitle: 'ุงู„ุชูุงุตูŠู„ (ู…ุดุงูƒู„ ุงู„ู…ุญุชูˆู‰)', - reportsStepsToReproduceLabel: 'ุฎุทูˆุงุช ุฅุนุงุฏุฉ ุงู„ุฅู†ุชุงุฌ', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'ุงู„ู†ุชูŠุฌุฉ ุงู„ู…ุชูˆู‚ุนุฉ', - reportsExpectedBehaviorPlaceholder: 'ู…ุงุฐุง ูŠุฌุจ ุฃู† ูŠุญุฏุซุŸ', - reportsActualBehaviorLabel: 'ุงู„ู†ุชูŠุฌุฉ ุงู„ูุนู„ูŠุฉ', - reportsActualBehaviorPlaceholder: 'ู…ุงุฐุง ูŠุญุฏุซ ูุนู„ุงู‹ุŸ', - reportsEnvironmentLabel: 'ุงู„ุจูŠุฆุฉ', - reportsEnvironmentPlaceholder: 'ุงู„ุฅุตุฏุงุฑุŒ ุงู„ุฌู‡ุงุฒุŒ ู†ุธุงู… ุงู„ุชุดุบูŠู„ุŒ ุงู„ู…ุชุตูุญุŒ ุงู„ุฅุนุฏุงุฏุงุชุŒ ุงู„ุณุฌู„ุงุชุŒ ุฅู„ุฎ.', - reportsReproduceRateLabel: 'ู…ุนุฏู„ ุฅุนุงุฏุฉ ุงู„ุฅู†ุชุงุฌ', - reportsReproduceRateAlways: 'ุฏุงุฆู…ู‹ุง', - reportsReproduceRateOften: 'ุบุงู„ุจู‹ุง', - reportsReproduceRateSometimes: 'ุฃุญูŠุงู†ู‹ุง', - reportsReproduceRateRarely: 'ู†ุงุฏุฑู‹ุง', - reportsReproduceRateUnable: 'ุบูŠุฑ ู‚ุงุจู„ ู„ุฅุนุงุฏุฉ ุงู„ุฅู†ุชุงุฌ', - reportsReproduceRateUnknown: 'ุบูŠุฑ ู…ุญุฏุฏ', - reportsProblemStatementLabel: 'ุงู„ู…ุดูƒู„ุฉ / ุงู„ุญุงุฌุฉ', - reportsProblemStatementPlaceholder: 'ู…ุง ุงู„ู…ุดูƒู„ุฉ ุงู„ุชูŠ ุชุญู„ู‡ุง ู‡ุฐู‡ ุงู„ู…ูŠุฒุฉุŸ', - reportsUserStoryLabel: 'ู‚ุตุฉ ุงู„ุณุงูƒู†', - reportsUserStoryPlaceholder: 'ุจุตูุชูŠ <ุณุงูƒู†>ุŒ ุฃุฑูŠุฏ <ุฅุฌุฑุงุก>ุŒ ู„ูƒูŠ <ูุงุฆุฏุฉ>.', - reportsAcceptanceCriteriaLabel: 'ู…ุนุงูŠูŠุฑ ุงู„ู‚ุจูˆู„', - reportsAcceptanceCriteriaPlaceholder: '- ุจูุฑุถ...\n- ุนู†ุฏู…ุง...\n- ุนู†ุฏู‡ุง...', - reportsWhatHappenedLabel: 'ู…ุงุฐุง ุญุฏุซุŸ', - reportsWhatHappenedPlaceholder: 'ุตู ุงู„ุญุงุฏุซุฉ ู…ุน ุงู„ุณูŠุงู‚ ูˆุงู„ุชูˆุงุฑูŠุฎ ุงู„ุชู‚ุฑูŠุจูŠุฉ.', - reportsReportedUserLabel: 'ุงู„ุณุงูƒู† / ุงู„ูƒูŠุงู† ุงู„ู…ูุจู„ูŽู‘ุบ ุนู†ู‡', - reportsReportedUserPlaceholder: '@ุณุงูƒู†ุŒ ุชุบุฐูŠุฉุŒ ู…ุนุฑู‘ูุŒ ุฑุงุจุทุŒ ุฅู„ุฎ.', - reportsEvidenceLinksLabel: 'ุงู„ุฃุฏู„ุฉ / ุงู„ุฑูˆุงุจุท', - reportsEvidenceLinksPlaceholder: 'ุงู„ุตู‚ ุงู„ุฑูˆุงุจุทุŒ ู…ุนุฑู‘ูุงุช ุงู„ุฑุณุงุฆู„ุŒ ู„ู‚ุทุงุช ุงู„ุดุงุดุฉ (ุฅู† ูˆูุฌุฏุช)ุŒ ุฅู„ุฎ.', - reportsContentLocationLabel: 'ู…ูƒุงู† ุงู„ู…ุญุชูˆู‰', - reportsContentLocationPlaceholder: 'ุฑุงุจุทุŒ ู…ุนุฑู‘ูุŒ ู‚ู†ุงุฉุŒ ู…ุญุงุฏุซุฉุŒ ู…ุคู„ูุŒ ุฅู„ุฎ.', - reportsWhyInappropriateLabel: "ู„ู…ุงุฐุง ู‡ูˆ ุบูŠุฑ ู…ู†ุงุณุจ", - reportsWhyInappropriatePlaceholder: 'ุงุดุฑุญ ุงู„ุณุจุจ ูˆุงู„ุชุฃุซูŠุฑ.', - reportsRequestedActionLabel: 'ุงู„ุฅุฌุฑุงุก ุงู„ู…ุทู„ูˆุจ', - reportsRequestedActionPlaceholder: 'ุฅุฒุงู„ุฉุŒ ุฅุฎูุงุกุŒ ูˆุณู…ุŒ ุชุญุฐูŠุฑุŒ ุฅู„ุฎ.', tribesTitle: "ุงู„ู‚ุจุงุฆู„", tribeFilterAll: "ุงู„ูƒู„", tribeFilterRecent: "ุงู„ุฃุญุฏุซ", @@ -2271,6 +2243,7 @@ module.exports = { voteWellResearched: "ุจุญุซ ุฌูŠุฏ", voteNeedsSources: "ูŠุญุชุงุฌ ู…ุตุงุฏุฑ", voteWrong: "ุฎุงุทุฆ", + voteOutdated: "ู‚ุฏูŠู…", voteLowQuality: "ุฌูˆุฏุฉ ู…ู†ุฎูุถุฉ", voteLove: "ุญุจ", voteClear: "ูˆุงุถุญ", @@ -2515,7 +2488,6 @@ module.exports = { shopNoProducts: "ู„ุง ุชูˆุฌุฏ ู…ู†ุชุฌุงุช ุจุนุฏ.", shopOutOfStock: "ู†ูุฏ ุงู„ู…ุฎุฒูˆู†", shopBuy: "ุดุฑุงุก", - shopBuyEncryptedNote: "ุจูŠุงู†ุงุช ุงู„ุชูˆุตูŠู„ ุงู„ุฎุงุตุฉ ุจูƒ ู…ุดูู‘ุฑุฉ ูˆู„ุง ูŠุฑุงู‡ุง ุณูˆู‰ ุตุงุญุจ ุงู„ู…ุชุฌุฑ.", shopBuyDeliveryAddress: "ุนู†ูˆุงู† ุงู„ุชูˆุตูŠู„", shopBuyContact: "ุฌู‡ุฉ ุงู„ุงุชุตุงู„", shopBuyContactPlaceholder: "ุงู„ุจุฑูŠุฏุŒ ุงู„ู‡ุงุชูุŒ ุฅู„ุฎ.", @@ -3032,6 +3004,16 @@ module.exports = { industryDescriptionLabel: "ุงู„ูˆุตู", industryDescriptionPlaceholder: "ู…ุงุฐุง ูŠู†ุชุฌ ู‡ุฐุง ุงู„ู…ุตู†ุนุŸ", industrySector: "ุงู„ู‚ุทุงุน", + industrySector_software: "ุจุฑู…ุฌูŠุงุช", + industrySector_hardware: "ุนุชุงุฏ", + industrySector_agriculture: "ุฒุฑุงุนุฉ", + industrySector_textile: "ู†ุณูŠุฌ", + industrySector_energy: "ุทุงู‚ุฉ", + industrySector_food: "ุบุฐุงุก", + industrySector_construction: "ุจู†ุงุก", + industrySector_media: "ุฅุนู„ุงู…", + industrySector_services: "ุฎุฏู…ุงุช", + industrySector_other: "ุฃุฎุฑู‰", industryMembershipPolicy: "ุณูŠุงุณุฉ ุงู„ุนุถูˆูŠุฉ", industryQuorum: "ุงู„ู†ุตุงุจ", industryMajority: "ุงู„ุฃุบู„ุจูŠุฉ", @@ -3233,6 +3215,18 @@ module.exports = { modulesFavoritesDescription: "ูˆุญุฏุฉ ู„ุฅุฏุงุฑุฉ ุงู„ู…ุญุชูˆู‰ ุงู„ู…ูุถู„ ู„ุฏูŠูƒ.", fileTooLargeTitle: "ุงู„ู…ู„ู ูƒุจูŠุฑ ุฌุฏู‹ุง", fileTooLargeMessage: "ูŠุชุฌุงูˆุฒ ุงู„ู…ู„ู ุงู„ุญุฌู… ุงู„ุฃู‚ุตู‰ ุงู„ู…ุณู…ูˆุญ ุจู‡ (50 ู…ูŠุบุงุจุงูŠุช). ูŠุฑุฌู‰ ุงุฎุชูŠุงุฑ ู…ู„ู ุฃุตุบุฑ.", + fileWrongType: "ู‡ุฐุง ุงู„ู…ู„ู ู„ูŠุณ ู…ู† ุงู„ู†ูˆุน ุงู„ู…ู†ุงุณุจ ู„ู‡ุฐุง ุงู„ู‚ุณู….", + mediaLockedAfterOpinions: "ู„ุฏูŠู‡ ุขุฑุงุก ุจุงู„ูุนู„: ูŠู…ูƒู†ูƒ ุชุนุฏูŠู„ ุงู„ุจุงู‚ูŠุŒ ู„ูƒู† ู„ุง ูŠู…ูƒู†ูƒ ุชุบูŠูŠุฑ ู…ุง ุชู… ุชู‚ูŠูŠู…ู‡.", + pdfPrevPage: "ุงู„ุตูุญุฉ ุงู„ุณุงุจู‚ุฉ", + pdfNextPage: "ุงู„ุตูุญุฉ ุงู„ุชุงู„ูŠุฉ", + pdfPage: "ุตูุญุฉ", + pdfZoomIn: "ุชูƒุจูŠุฑ", + pdfZoomOut: "ุชุตุบูŠุฑ", + pdfFullscreen: "ู…ู„ุก ุงู„ุดุงุดุฉ", + pdfClose: "ุฅุบู„ุงู‚", + pdfRotate: "ุชุฏูˆูŠุฑ", + contentNotAvailableYet: "ู‡ุฐุง ุงู„ู…ุญุชูˆู‰ ุบูŠุฑ ู…ุชุงุญ ุจุนุฏ.", + pdfRetry: "ุฅุนุงุฏุฉ ุงู„ู…ุญุงูˆู„ุฉ", goBack: "ุฑุฌูˆุน", errorDismiss: "ุญุณู†ู‹ุง", errorPageTitle: "ุญุฏุซ ุฎุทุฃ", @@ -3275,9 +3269,7 @@ module.exports = { uploadMedia: "ุฑูุน ูˆุณุงุฆุท (ุงู„ุญุฏ ุงู„ุฃู‚ุตู‰: 50 ู…ูŠุบุงุจุงูŠุช)", feedOpenDiscussion: "ูุชุญ ุงู„ู†ู‚ุงุด", feedDetailTitle: "ุงู„ุชุบุฐูŠุฉ", - feedPostComment: "ู†ุดุฑ ุชุนู„ูŠู‚", courtsRespondentInvalid: "ูŠุฌุจ ุฃู† ูŠูƒูˆู† ู…ุนุฑู‘ู SSB ุตุงู„ุญู‹ุง (@...ed25519)", - noComments: "ู„ุง ุชูˆุฌุฏ ุชุนู„ูŠู‚ุงุช ุจุนุฏ", mapsLabel: "ุฎุฑุงุฆุท", mapTitle: "ุฎุฑุงุฆุท", mapDescription: "ุฃุฏุฑ ุงู„ุฎุฑุงุฆุท ุบูŠุฑ ุงู„ู…ุชุตู„ุฉ ูˆุดุงุฑูƒู‡ุง ููŠ ุดุจูƒุชูƒ.", @@ -3581,6 +3573,8 @@ module.exports = { podcastTagsPlaceholder: "ุฃุฏุฎู„ ุงู„ูˆุณูˆู… ู…ูุตูˆู„ุฉ ุจููˆุงุตู„", podcastFilterAll: "ุงู„ูƒู„", podcastFilterMine: "ู‚ู†ูˆุงุชูŠ", + podcastFilterFavorites: "ุงู„ู…ูุถู„ุฉ", + podcastEpisodeNumberLabel: "ุฑู‚ู… ุงู„ุญู„ู‚ุฉ", podcastFilterRecent: "ุงู„ุฃุญุฏุซ", podcastFilterTop: "ุงู„ุฃูุถู„", podcastFilterViewers: "ุงู„ู…ุณุชู…ุนูˆู†", @@ -3799,7 +3793,6 @@ module.exports = { wikiBackToPage: "ุงู„ุนูˆุฏุฉ ุฅู„ู‰ ุงู„ุตูุญุฉ", wikiLinkedChip: "ู…ุฑุชุจุทุฉ", wikiRestoredSummary: "ุฅุตุฏุงุฑ ู…ุณุชุนุงุฏ", - wikiExportPdf: "ุฅู†ุดุงุก ุงู„ุตูุญุฉ", wikiPreview: "ู…ุนุงูŠู†ุฉ", tribeSectionWiki: "ูˆูŠูƒูŠ", tribeWikiCreate: "ุฅู†ุดุงุก ูˆูŠูƒูŠ", @@ -3867,6 +3860,9 @@ module.exports = { roomRecStop: "ุฅูŠู‚ุงู ุงู„ุชุณุฌูŠู„", roomStreamFile: "ุจุซู‘", roomStreamStop: "ุฅูŠู‚ุงู", + roomScreen: "ุงู„ุดุงุดุฉ", + roomScreenSharing: "ู…ุดุงุฑูƒุฉ ุงู„ุดุงุดุฉ", + roomScreenWarning: "ุณูŠุฑู‰ ูƒู„ ู…ู† ููŠ ุงู„ุบุฑูุฉ ุดุงุดุชูƒ ุจุงู„ูƒุงู…ู„ ุญุชู‰ ุชุชูˆู‚ู.", roomStreaming: "ุฌุงุฑู ุงู„ุจุซ", liveFullscreen: "ู…ู„ุก ุงู„ุดุงุดุฉ", roomStreamPickTitle: "ุงุฎุชุฑ ููŠุฏูŠูˆ", @@ -3924,6 +3920,7 @@ module.exports = { inviteAlreadyMember: "ุฃู†ุช ู…ู†ุถู… ุจุงู„ูุนู„. ูŠู…ูƒู†ูƒ ูุชุญู‡ ู…ู† ูˆุญุฏุชู‡.", actionFailed: "ุชุนุฐู‘ุฑ ุชู†ููŠุฐ ุฐู„ูƒ. ุญุงูˆู„ ู…ุฑุฉ ุฃุฎุฑู‰.", publishTooShort: "ู…ู†ุดูˆุฑูƒ ู‚ุตูŠุฑ ุฌุฏู‹ุง.", + publishEmpty: "ุงูƒุชุจ ุดูŠุฆู‹ุง ุฃูˆ ุฃุฑูู‚ ู…ู„ูู‹ุง.", walletSendFailed: "ุชุนุฐู‘ุฑ ุฅุฑุณุงู„ ุงู„ุฏูุนุฉ: {reason}", pixeliaCoordsError: "ู‡ุฐู‡ ุงู„ุฅุญุฏุงุซูŠุงุช ุฎุงุฑุฌ ุงู„ุดุจูƒุฉ.", courtsErrTitle: "ุงู„ุนู†ูˆุงู† ู…ุทู„ูˆุจ.", @@ -4435,6 +4432,9 @@ module.exports = { torrentTagsLabel: "ุงู„ูˆุณูˆู…", torrentTagsPlaceholder: "ุฃุฏุฎู„ ุงู„ูˆุณูˆู… ู…ูุตูˆู„ุฉ ุจููˆุงุตู„", torrentSizeLabel: "ุงู„ุญุฌู…", + torrentInfoHash: "ุจุตู…ุฉ ุงู„ู…ุนู„ูˆู…ุงุช", + torrentFilesLabel: "ุงู„ู…ู„ูุงุช", + torrentMagnet: "MAGNET", torrentFromContent: "ุฅู†ุดุงุก ุชูˆุฑู†ุช", torrentFromContentMissing: "ุงู„ู…ู„ู ุบูŠุฑ ู…ุชุงุญ ุจุนุฏ ุนู„ู‰ ู‡ุฐู‡ ุงู„ุนู‚ุฏุฉ. ุญุงูˆู„ูŠ ู„ุงุญู‚ู‹ุง.", torrentDownloads: "ุงู„ุชู†ุฒูŠู„ุงุช", @@ -4520,9 +4520,6 @@ module.exports = { inhabitantsPendingAccept: "ู‚ุจูˆู„", inhabitantsPendingReject: "ุฑูุถ", tribeSectionGovernance: "ุงู„ุญูˆูƒู…ุฉ", - tribePadInviteRequired: "ู„ุง ูŠู…ูƒู†ูƒ ุงู„ูˆุตูˆู„. ุงุทู„ุจ ุฏุนูˆุฉ.", - tribeChatInviteRequired: "ู„ุง ูŠู…ูƒู†ูƒ ุงู„ูˆุตูˆู„ ู„ู„ุฏุฑุฏุดุฉ. ุงุทู„ุจ ุฏุนูˆุฉ.", - tribeGovernanceDesc: "ุงู„ุญูˆูƒู…ุฉ ุงู„ุฏุงุฎู„ูŠุฉ ู„ู‡ุฐู‡ ุงู„ู‚ุจูŠู„ุฉ.", tribeGovernanceNoGov: "ู„ุง ุชูˆุฌุฏ ุญูƒูˆู…ุฉ ู†ุดุทุฉ", tribeGovernanceNoGovDesc: "ู„ู… ุชู†ุชุฎุจ ู‡ุฐู‡ ุงู„ู‚ุจูŠู„ุฉ ุญูƒูˆู…ุฉ ุจุนุฏ.", tribeGovCardTitle: "ุงู„ุญูƒูˆู…ุฉ ุงู„ุญุงู„ูŠุฉ", @@ -4579,7 +4576,6 @@ module.exports = { logsFilterMonth: "ุงู„ุดู‡ุฑ ุงู„ู…ุงุถูŠ", logsFilterWeek: "ุงู„ุฃุณุจูˆุน ุงู„ู…ุงุถูŠ", logsFilterToday: "ุงู„ูŠูˆู…", - logsComments: "ุงู„ุชุนู„ูŠู‚ุงุช", logsAuthorEntries: "ุงู„ู…ุฏุฎู„ุงุช", logsCreate: "ุฅู†ุดุงุก ุฅุฏุฎุงู„", logsExport: "ุชุตุฏูŠุฑ ุงู„ุณุฌู„", @@ -4735,6 +4731,8 @@ module.exports = { larpWillTestTitle: "ุงุฎุชุจุงุฑ WILL", settingsLanDesc: "ุงู„ุฅุนู„ุงู† ุจุดูƒู„ ุฏูˆุฑูŠ ุนู† ู‡ุฐุง ุงู„ู†ุธูŠุฑ ู„ู…ุซูŠู„ุงุช Oasis ุงู„ุฃุฎุฑู‰ ุนู„ู‰ ู†ูุณ ุงู„ุดุจูƒุฉ ุงู„ู…ุญู„ูŠุฉ.", settingsLanEnable: "ุชูุนูŠู„ ุจุซ ุงู„ุดุจูƒุฉ ุงู„ู…ุญู„ูŠุฉ", + settingsConnectionsTitle: "ุงู„ุงุชุตุงู„ุงุช", + settingsConnectionsDesc: "ุนุฏุฏ ุงู„ู†ุธุฑุงุก ุงู„ุฐูŠู† ุชุจู‚ูŠู‡ู… ู‡ุฐู‡ ุงู„ุนู‚ุฏุฉ ู…ุชุตู„ูŠู† ููŠ ุขู† ูˆุงุญุฏ.", settingsLanTitle: "ุจุซ ุงู„ุดุจูƒุฉ ุงู„ู…ุญู„ูŠุฉ", settingsReplicationTitle: "ุงู„ู†ุณุฎ", settingsReplicationDesc: "ุงุถุจุท ุนุฏุฏ ุงู„ู‚ูุฒุงุช ุงู„ุชูŠ ูŠุชุจุนู‡ุง ู†ุธูŠุฑูƒ ุงู†ุทู„ุงู‚ู‹ุง ู…ู† ุฎู„ุงุตุชูƒ ุนู†ุฏ ู†ุณุฎ ุงู„ู…ุญุชูˆู‰.", diff --git a/src/client/assets/translations/oasis_ca.js b/src/client/assets/translations/oasis_ca.js index bc12bde..ea53194 100644 --- a/src/client/assets/translations/oasis_ca.js +++ b/src/client/assets/translations/oasis_ca.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† La meva xarxa", graphosShowing: "Mostrant", graphosYou: "Tu", - graphosTotalNodes: "Nodes totals", + graphosTotalNodes: "Totals", + onlinePlural: "Connectats", mentions: "Mencions", mentionsDescription: [ strong("Tots els missatges que et @mencionen"), @@ -412,6 +413,7 @@ module.exports = { menuMedia: "Biblioteca", menuTools: "Eines", comment: "Comentar", + typeComment: "Comentari", subtopic: "Subtema", json: "JSON", unfollow: "Deixar de donar suport", @@ -441,9 +443,6 @@ module.exports = { publishCustomDescription: [ "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bรฉ!", ], - commentWarning: [ - "RECORDA: A causa de la tecnologia blockchain, un cop has publicat alguna cosa, no es pot editar ni esborrar. Pensa-t'ho bรฉ!", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -718,10 +717,6 @@ module.exports = { votesQuestionLabel:"Pregunta", voteStatusOpen:"OBERT", voteStatusClosed:"TANCAT", - commentDescription: ({ parentUrl }) => [ - " ha comentat en ", - a({ href: parentUrl }, " fil"), - ], commentTitle: ({ authorName }) => [`Ha comentat al post de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " ha creat un subtema des de ", @@ -856,6 +851,8 @@ module.exports = { bankRulesEligibility5: "El PUB paga en el seu segรผent cicle i publica un transfer RBU que pots confirmar.", bankRulesEligibility6: "Si falta la teva adreรงa, el feed รฉs massa jove, has rebutjat el mes o el PUB no tรฉ fons, la reclamaciรณ se salta i queda registrada al PUB.", copyLinkTitle: "Copiar l'enllaรง", + cardMenuTitle: "Mรฉs accions", + shareContent: "Compartir", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Rebutjar RBU", @@ -889,7 +886,7 @@ module.exports = { walletNotConfigured: "La teva cartera d'ECOin encara no estร  configurada.", walletSettingsDescription: "Integrar Oasis amb el teu moneder d'ECOin.", walletSettingsMobileDescription: "Vincula Oasis amb l'app ECOin Wallet d'aquest dispositiu.", - walletDownloadEcoin: "Baixar ECOin!", + walletDownloadEcoin: "Baixar ECOin", walletRpcUrl: "URL del RPC", walletStatusMessages: { invalid_amount: "Quantitat no vร lida", @@ -942,6 +939,7 @@ module.exports = { bookmarkUpdateButton: "Actualitzar", bookmarkAddFavoriteButton: "Afegir a preferits", bookmarkRemoveFavoriteButton: "Treure de preferits", + bookmarkTitleLabel: "Tรญtol", bookmarkUrlLabel: "Enllaรง", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descripciรณ", @@ -1086,9 +1084,9 @@ module.exports = { allCVs: "CV", discoverPeople: "Descobreix habitants a la teva xarxa.", searchInhabitantsPlaceholder: "Cercar a Habitants...", - filterLocation: "FILTRAR habitants PER UBICACIร“ โ€ฆ", - filterLanguage: "FILTRAR habitants PER IDIOMA โ€ฆ", - filterSkills: "FILTRAR habitants PER HABILITATS โ€ฆ", + filterLocation: "Cercar per ubicaciรณโ€ฆ", + filterLanguage: "Cercar per idiomaโ€ฆ", + filterSkills: "Cercar per habilitatsโ€ฆ", cvSkillsCount: "Habilitats", applyFilters: "Aplicar els filtres", locationLabel: "Ubicaciรณ", @@ -1104,6 +1102,7 @@ module.exports = { topactivitySectionTitle: "Mรฉs activitat", "TOP INACTIVITYButton": "Mร‰S INACTIUS", topinactivitySectionTitle: "Habitants menys actius", + contactsSectionTitle: "Contactes", blockedSectionTitle: "Bloquejats", gallerySectionTitle: "GALERIA", topSectionTitle: "TOP", @@ -1558,9 +1557,7 @@ module.exports = { voteUnknownDate: "Data desconeguda", voteDetailSectionTitle: "Detalls de la votaciรณ", voteCommentsLabel: "Comentaris", - voteCommentsForumButton: "Obrir la conversa", - voteCommentsSectionTitle: "Conversa oberta", - voteNoCommentsYet: "Encara no hi ha comentaris. Sigues la primera persona a opinar.", + voteNoCommentsYet: "Encara no hi ha comentaris.", commentsGoToLast: "Anar al darrer comentari", voteNewCommentPlaceholder: "Escriu aquรญ el teu comentariโ€ฆ", voteNewCommentButton: "Publicar el comentari", @@ -1687,7 +1684,6 @@ module.exports = { pollVoteButton: "Votar", pollChangeVote: "Canviar el vot", pollVoters: "Votants", - pollComments: "Comentaris", pollStatusLabel: "Estat", pollStatusOpen: "OBERTA", pollStatusClosed: "TANCADA", @@ -1709,9 +1705,22 @@ module.exports = { blogAllowComments: "Permetre comentaris", blogCommentsClosed: "L'autor ha tancat els comentaris d'aquest blog.", blogNoItems: "No s'han trobat blogs.", - viewBlog: "Veure el blog", forumTitle: "Fรฒrums", forumCategoryLabel: "Categoria", + forumCatGENERAL: "GENERAL", + forumCatPOLITICS: "POLรTICA", + forumCatTECH: "TECNOLOGIA", + forumCatSCIENCE: "CIรˆNCIA", + forumCatMUSIC: "MรšSICA", + forumCatART: "ART", + forumCatGAMING: "JOCS", + forumCatBOOKS: "LLIBRES", + forumCatFILMS: "PELยทLรCULES", + forumCatPHILOSOPHY: "FILOSOFIA", + forumCatSOCIETY: "SOCIETAT", + forumCatPRIVACY: "PRIVADESA", + forumCatCYBERWARFARE: "CIBERGUERRA", + forumCatSURVIVALISM: "SUPERVIVรˆNCIA", forumTitleLabel: "Tรญtol", forumTitlePlaceholder: "Tรญtol del fรฒrum...", forumCreateButton: "Crear un fรฒrum", @@ -1770,8 +1779,6 @@ module.exports = { feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Obrir el debat", - feedPostComment: "Publicar el comentari", - noComments: "Encara no hi ha comentaris", createFeedTitle: "Crear un feed", createFeedButton: "Enviar el feed!", feedPlaceholder: "Quรจ estร  passant? (mร xim 280 carร cters)", @@ -1982,42 +1989,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Informes en revisiรณ", reportsResolvedSectionTitle: "Informes resolts", reportsInvalidSectionTitle: "Informes no vร lids", - reportsBugTemplateTitle: "Dades per reproduir (errors)", - reportsFeatureTemplateTitle: "Detalls (funcions)", - reportsAbuseTemplateTitle: "Detalls (abรบs)", - reportsContentTemplateTitle: "Detalls (problemes de contingut)", - reportsStepsToReproduceLabel: "Passos per reproduir-ho", - reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...", - reportsExpectedBehaviorLabel: "Resultat esperat", - reportsExpectedBehaviorPlaceholder: "Quรจ hauria de passar?", - reportsActualBehaviorLabel: "Resultat real", - reportsActualBehaviorPlaceholder: "Quรจ passa realment?", - reportsEnvironmentLabel: "Entorn", - reportsEnvironmentPlaceholder: "Versiรณ, dispositiu, SO, navegador, configuraciรณ, registres, etc.", - reportsReproduceRateLabel: "Freqรผรจncia", - reportsReproduceRateAlways: "Sempre", - reportsReproduceRateOften: "Sovint", - reportsReproduceRateSometimes: "De vegades", - reportsReproduceRateRarely: "Rarament", - reportsReproduceRateUnable: "No es pot reproduir", - reportsReproduceRateUnknown: "No especificat", - reportsProblemStatementLabel: "Problema / necessitat", - reportsProblemStatementPlaceholder: "Quin problema resol aquesta funciรณ?", - reportsUserStoryLabel: "Histรฒria d'habitant", - reportsUserStoryPlaceholder: "Com a , vull , per .", - reportsAcceptanceCriteriaLabel: "Criteris d'acceptaciรณ", - reportsAcceptanceCriteriaPlaceholder: "- Donat...\n- Quan...\n- Aleshores...", - reportsWhatHappenedLabel: "Quรจ ha passat?", - reportsWhatHappenedPlaceholder: "Descriu l'incident amb context i dates aproximades.", - reportsReportedUserLabel: "Habitant / entitat denunciada", - reportsReportedUserPlaceholder: "@habitant, feed, ID, enllaรง, etc.", - reportsEvidenceLinksLabel: "Proves / enllaรงos", - reportsEvidenceLinksPlaceholder: "Enganxa enllaรงos, IDs de missatges, captures (si escau), etc.", - reportsContentLocationLabel: "On รฉs el contingut", - reportsContentLocationPlaceholder: "Enllaรง, ID, canal, fil, autor, etc.", - reportsWhyInappropriatePlaceholder: "Explica el motiu i l'impacte.", - reportsRequestedActionLabel: "Acciรณ solยทlicitada", - reportsRequestedActionPlaceholder: "Eliminar, amagar, marcar, advertir, etc.", tribesTitle: "Tribus", tribeFilterAll: "TOTES", tribeFilterRecent: "RECENTS", @@ -2261,6 +2232,7 @@ module.exports = { voteWellResearched: "Ben documentat", voteNeedsSources: "Necessita fonts", voteWrong: "Incorrecte", + voteOutdated: "Desactualitzat", voteLowQuality: "Baixa qualitat", voteLove: "Amor", voteClear: "Clar", @@ -2505,7 +2477,6 @@ module.exports = { shopNoProducts: "Encara no hi ha productes.", shopOutOfStock: "Exhaurit", shopBuy: "Comprar", - shopBuyEncryptedNote: "Les teves dades de lliurament s'envien xifrades; nomรฉs el propietari de la botiga les pot veure.", shopBuyDeliveryAddress: "Adreรงa de lliurament", shopBuyContact: "Contacte", shopBuyContactPlaceholder: "correu, telรจfon, etc.", @@ -3022,6 +2993,16 @@ module.exports = { industryDescriptionLabel: "Descripciรณ", industryDescriptionPlaceholder: "Quรจ produeix aquesta fร brica?", industrySector: "Sector", + industrySector_software: "Programari", + industrySector_hardware: "Maquinari", + industrySector_agriculture: "Agricultura", + industrySector_textile: "Tรจxtil", + industrySector_energy: "Energia", + industrySector_food: "Alimentaciรณ", + industrySector_construction: "Construcciรณ", + industrySector_media: "Mitjans", + industrySector_services: "Serveis", + industrySector_other: "Altres", industryMembershipPolicy: "Polรญtica de pertinenรงa", industryQuorum: "Quรฒrum", industryMajority: "Majoria", @@ -3223,6 +3204,18 @@ module.exports = { modulesFavoritesDescription: "Mรฒdul per gestionar el teu contingut preferit.", fileTooLargeTitle: "Fitxer massa gran", fileTooLargeMessage: "El fitxer supera la mida mร xima permesa (75 MB). Si us plau, selecciona un fitxer mรฉs petit.", + fileWrongType: "Aquest fitxer no รฉs del tipus adequat per a aquesta secciรณ.", + mediaLockedAfterOpinions: "Ja tรฉ opinions: pots editar la resta, perรฒ no canviar el que s'ha valorat.", + pdfPrevPage: "Pร gina anterior", + pdfNextPage: "Pร gina segรผent", + pdfPage: "Pร gina", + pdfZoomIn: "Apropa", + pdfZoomOut: "Allunya", + pdfFullscreen: "Pantalla completa", + pdfClose: "Tanca", + pdfRotate: "Gira", + contentNotAvailableYet: "Aquest contingut encara no estร  disponible.", + pdfRetry: "Torna-ho a provar", goBack: "Tornar", errorDismiss: "D'acord", errorPageTitle: "S'ha produรฏt un error", @@ -3267,7 +3260,6 @@ module.exports = { invitesFollow: "Seguir", invitesUnfollowedInvites: "No federades", invitesNoUnfollowed: "No hi ha pubs no federats.", - reportsWhyInappropriateLabel: "Per quรจ รฉs inadequat?", blockchainContentDeleted: "Aquest contingut s'ha eliminat", visitContent: "Visitar el contingut", favoriteAdd: "Fixar a preferits", @@ -3579,6 +3571,8 @@ module.exports = { podcastTagsPlaceholder: "Introdueix etiquetes separades per comes", podcastFilterAll: "TOTS", podcastFilterMine: "MEUS", + podcastFilterFavorites: "PREFERITS", + podcastEpisodeNumberLabel: "Nรบmero d'episodi", podcastFilterRecent: "RECENTS", podcastFilterTop: "TOP", podcastFilterViewers: "OIENTS", @@ -3797,7 +3791,6 @@ module.exports = { wikiBackToPage: "Tornar a la pร gina", wikiLinkedChip: "Enllaรงada", wikiRestoredSummary: "Versiรณ restaurada", - wikiExportPdf: "Generar la pร gina", wikiPreview: "Previsualitzaciรณ", tribeSectionWiki: "WIKI", tribeWikiCreate: "Crear una wiki", @@ -3865,6 +3858,9 @@ module.exports = { roomRecStop: "Atura la gravaciรณ", roomStreamFile: "Emet", roomStreamStop: "Atura", + roomScreen: "Pantalla", + roomScreenSharing: "Compartint pantalla", + roomScreenWarning: "Tothom a la sala veurร  la teva pantalla sencera fins que aturis.", roomStreaming: "Emetent", liveFullscreen: "Pantalla completa", roomStreamPickTitle: "Tria un vรญdeo", @@ -3922,6 +3918,7 @@ module.exports = { inviteAlreadyMember: "Ja en formes part. Pots obrir-ho des del seu mรฒdul.", actionFailed: "No s'ha pogut fer. Torna-ho a provar.", publishTooShort: "La teva publicaciรณ รฉs massa curta.", + publishEmpty: "Escriu alguna cosa o adjunta un fitxer.", walletSendFailed: "No s'ha pogut enviar el pagament: {reason}", pixeliaCoordsError: "Aquestes coordenades sรณn fora de la quadrรญcula.", courtsErrTitle: "Cal un tรญtol.", @@ -4434,6 +4431,9 @@ module.exports = { torrentTagsLabel: "Etiquetes", torrentTagsPlaceholder: "Introdueix etiquetes separades per comes", torrentSizeLabel: "Mida", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Fitxers", + torrentMagnet: "MAGNET", torrentFromContent: "Generar torrent", torrentFromContentMissing: "El fitxer encara no estร  disponible en aquest node. Torna-ho a provar d'aquรญ a una estona.", torrentDownloads: "Baixades", @@ -4519,9 +4519,6 @@ module.exports = { inhabitantsPendingAccept: "Acceptar", inhabitantsPendingReject: "Rebutjar", tribeSectionGovernance: "GOVERN", - tribePadInviteRequired: "No tens accรฉs al pad. Demana una invitaciรณ per accedir al contingut.", - tribeChatInviteRequired: "No tens accรฉs al xat. Demana una invitaciรณ per accedir al contingut.", - tribeGovernanceDesc: "Govern intern d'aquesta tribu.", tribeGovernanceNoGov: "Sense govern actiu", tribeGovernanceNoGovDesc: "Aquesta tribu encara no tรฉ govern. Proposa candidatures per iniciar el procรฉs.", tribeGovCardTitle: "Govern actual", @@ -4578,7 +4575,6 @@ module.exports = { logsFilterMonth: "DARRER MES", logsFilterWeek: "DARRERA SETMANA", logsFilterToday: "AVUI", - logsComments: "Comentaris", logsAuthorEntries: "Entrades", logsCreate: "Crear una entrada", logsExport: "Exportar la bitร cola", @@ -4657,6 +4653,8 @@ module.exports = { larpWillTestTitle: "Test WILL", settingsLanDesc: "Anuncia periรฒdicament aquest node a altres instร ncies d'Oasis de la mateixa xarxa local.", settingsLanEnable: "Activar la difusiรณ LAN", + settingsConnectionsTitle: "Connexions", + settingsConnectionsDesc: "Quants peers mantรฉ connectats aquest node alhora.", settingsLanTitle: "Difusiรณ LAN", settingsReplicationTitle: "Replicaciรณ", settingsReplicationDesc: "Configura el nombre de salts que el teu node segueix des del teu propi feed en replicar contingut.", diff --git a/src/client/assets/translations/oasis_de.js b/src/client/assets/translations/oasis_de.js index 7132912..69d1d68 100644 --- a/src/client/assets/translations/oasis_de.js +++ b/src/client/assets/translations/oasis_de.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† Mein Netzwerk", graphosShowing: "Zeige", graphosYou: "Du", - graphosTotalNodes: "Knoten insgesamt", + graphosTotalNodes: "Gesamt", + onlinePlural: "Online", mentions: "Erwรคhnungen", mentionsDescription: [ strong("Jede Nachricht, die dich @erwรคhnt"), @@ -418,6 +419,7 @@ module.exports = { menuMedia: "Bibliothek", menuTools: "Werkzeuge", comment: "Kommentar", + typeComment: "Kommentar", subtopic: "Unterthema", json: "JSON", unfollow: "Nicht mehr unterstรผtzen", @@ -447,9 +449,6 @@ module.exports = { publishCustomDescription: [ "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein verรถffentlichter Beitrag nicht bearbeitet oder gelรถscht werden.", ], - commentWarning: [ - "ACHTUNG: Aufgrund der Blockchain-Technologie kann ein verรถffentlichter Beitrag nicht bearbeitet oder gelรถscht werden.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -725,10 +724,6 @@ module.exports = { votesQuestionLabel: "Frage", voteStatusOpen: "OFFEN", voteStatusClosed: "GESCHLOSSEN", - commentDescription: ({ parentUrl }) => [ - " hat kommentiert in ", - a({ href: parentUrl }, " Thread"), - ], commentTitle: ({ authorName }) => [`Kommentar zu @${authorName}s Beitrag`], subtopicDescription: ({ parentUrl }) => [ " hat ein Unterthema erstellt aus ", @@ -863,6 +858,8 @@ module.exports = { bankRulesEligibility5: "Das PUB zahlt im nรคchsten Zyklus und verรถffentlicht einen UBI-Transfer, den du bestรคtigen kannst.", bankRulesEligibility6: "Fehlt die Adresse, ist der Feed zu jung, wurde der Monat abgelehnt oder hat das PUB keine Mittel, wird der Anspruch รผbersprungen und auf dem PUB protokolliert.", copyLinkTitle: "Link kopieren", + cardMenuTitle: "Weitere Aktionen", + shareContent: "Teilen", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "UBI ablehnen", @@ -896,7 +893,7 @@ module.exports = { walletNotConfigured: "Deine ECOin-Wallet ist noch nicht eingerichtet.", walletSettingsDescription: "Integriere Oasis mit deiner ECOin-Wallet.", walletSettingsMobileDescription: "Verbinde Oasis mit der ECOin-Wallet-App auf diesem Gerรคt.", - walletDownloadEcoin: "ECOin herunterladen!", + walletDownloadEcoin: "ECOin herunterladen", walletRpcUrl: "RPC-URL", walletStatusMessages: { invalid_amount: "Ungรผltiger Betrag", @@ -948,6 +945,7 @@ module.exports = { bookmarkUpdateButton: "Aktualisieren", bookmarkAddFavoriteButton: "Favorit hinzufรผgen", bookmarkRemoveFavoriteButton: "Favorit entfernen", + bookmarkTitleLabel: "Titel", bookmarkUrlLabel: "Link", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Beschreibung", @@ -1092,9 +1090,9 @@ module.exports = { allCVs: "Lebenslรคufe", discoverPeople: "Entdecke Bewohner in deinem Netzwerk.", searchInhabitantsPlaceholder: "In Bewohner suchen...", - filterLocation: "BEWOHNER NACH ORT FILTERN โ€ฆ", - filterLanguage: "BEWOHNER NACH SPRACHE FILTERN โ€ฆ", - filterSkills: "BEWOHNER NACH Fร„HIGKEITEN FILTERN โ€ฆ", + filterLocation: "Nach Ort suchenโ€ฆ", + filterLanguage: "Nach Sprache suchenโ€ฆ", + filterSkills: "Nach Fรคhigkeiten suchenโ€ฆ", cvSkillsCount: "Fรคhigkeiten", applyFilters: "Filter anwenden", locationLabel: "Ort", @@ -1110,6 +1108,7 @@ module.exports = { topactivitySectionTitle: "Top Aktivitรคt", "TOP INACTIVITYButton": "AM INAKTIVSTEN", topinactivitySectionTitle: "Am wenigsten aktive Bewohner", + contactsSectionTitle: "Kontakte", blockedSectionTitle: "Blockiert", gallerySectionTitle: "GALERIE", topSectionTitle: "TOP", @@ -1563,9 +1562,7 @@ module.exports = { voteUnknownDate: "Unbekanntes Datum", voteDetailSectionTitle: 'Abstimmungsdetails', voteCommentsLabel: 'Kommentare', - voteCommentsForumButton: 'Diskussion erรถffnen', - voteCommentsSectionTitle: 'Offene Diskussion', - voteNoCommentsYet: 'Noch keine Kommentare. Sei der Erste, der antwortet.', + voteNoCommentsYet: 'Noch keine Kommentare.', commentsGoToLast: "Zum letzten Kommentar", voteNewCommentPlaceholder: 'Schreibe deinen Kommentar hierโ€ฆ', voteNewCommentButton: 'Kommentar verรถffentlichen', @@ -1692,7 +1689,6 @@ module.exports = { pollVoteButton: "Abstimmen", pollChangeVote: "Stimme ร„ndern", pollVoters: "Abstimmende", - pollComments: "Kommentare", pollStatusLabel: "Status", pollStatusOpen: "OFFEN", pollStatusClosed: "GESCHLOSSEN", @@ -1714,9 +1710,22 @@ module.exports = { blogAllowComments: "Kommentare erlauben", blogCommentsClosed: "Der Autor hat die Kommentare fรผr diesen Blog geschlossen.", blogNoItems: "Keine Blogs gefunden.", - viewBlog: "Blog Ansehen", forumTitle: "Foren", forumCategoryLabel: "Kategorie", + forumCatGENERAL: "ALLGEMEIN", + forumCatPOLITICS: "POLITIK", + forumCatTECH: "TECHNIK", + forumCatSCIENCE: "WISSENSCHAFT", + forumCatMUSIC: "MUSIK", + forumCatART: "KUNST", + forumCatGAMING: "SPIELE", + forumCatBOOKS: "BรœCHER", + forumCatFILMS: "FILME", + forumCatPHILOSOPHY: "PHILOSOPHIE", + forumCatSOCIETY: "GESELLSCHAFT", + forumCatPRIVACY: "PRIVATSPHร„RE", + forumCatCYBERWARFARE: "CYBERKRIEG", + forumCatSURVIVALISM: "SURVIVALISMUS", forumTitleLabel: "Titel", forumTitlePlaceholder: "Forentitel...", forumCreateButton: "Forum erstellen", @@ -1988,43 +1997,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Berichte in Prรผfung", reportsResolvedSectionTitle: "Gelรถste Berichte", reportsInvalidSectionTitle: "Ungรผltige Berichte", - reportsBugTemplateTitle: 'Reproduktionsdetails (Fehler)', - reportsFeatureTemplateTitle: 'Details (Funktionen)', - reportsAbuseTemplateTitle: 'Details (Missbrauch)', - reportsContentTemplateTitle: 'Details (Inhaltsprobleme)', - reportsStepsToReproduceLabel: 'Reproduktionsschritte', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Erwartetes Ergebnis', - reportsExpectedBehaviorPlaceholder: 'Was sollte passieren?', - reportsActualBehaviorLabel: 'Tatsรคchliches Ergebnis', - reportsActualBehaviorPlaceholder: 'Was passiert tatsรคchlich?', - reportsEnvironmentLabel: 'Umgebung', - reportsEnvironmentPlaceholder: 'Version, Gerรคt, Betriebssystem, Browser, Einstellungen, Protokolle, etc.', - reportsReproduceRateLabel: 'Reproduktionsrate', - reportsReproduceRateAlways: 'Immer', - reportsReproduceRateOften: 'Hรคufig', - reportsReproduceRateSometimes: 'Manchmal', - reportsReproduceRateRarely: 'Selten', - reportsReproduceRateUnable: 'Nicht reproduzierbar', - reportsReproduceRateUnknown: 'Nicht angegeben', - reportsProblemStatementLabel: 'Problem / Bedarf', - reportsProblemStatementPlaceholder: 'Welches Problem lรถst diese Funktion?', - reportsUserStoryLabel: 'Bewohner-Geschichte', - reportsUserStoryPlaceholder: 'Als mรถchte ich , damit .', - reportsAcceptanceCriteriaLabel: 'Akzeptanzkriterien', - reportsAcceptanceCriteriaPlaceholder: '- Gegeben...\n- Wenn...\n- Dann...', - reportsWhatHappenedLabel: 'Was ist passiert?', - reportsWhatHappenedPlaceholder: 'Beschreibe den Vorfall mit Kontext und ungefรคhrem Datum.', - reportsReportedUserLabel: 'Gemeldeter Bewohner / Entitรคt', - reportsReportedUserPlaceholder: '@Bewohner, Feed, ID, Link, etc.', - reportsEvidenceLinksLabel: 'Beweise / Links', - reportsEvidenceLinksPlaceholder: 'Links, Nachrichten-IDs, Screenshots (falls zutreffend), etc.', - reportsContentLocationLabel: 'Wo sich der Inhalt befindet', - reportsContentLocationPlaceholder: 'Link, ID, Kanal, Thread, Autor, etc.', - reportsWhyInappropriateLabel: "Warum es unangemessen ist", - reportsWhyInappropriatePlaceholder: 'Grund und Auswirkung erklรคren.', - reportsRequestedActionLabel: 'Gewรผnschte MaรŸnahme', - reportsRequestedActionPlaceholder: 'Entfernen, verbergen, markieren, warnen, etc.', tribesTitle: "Stรคmme", tribeFilterAll: "ALLE", tribeFilterRecent: "NEUESTE", @@ -2268,6 +2240,7 @@ module.exports = { voteWellResearched: "Gut recherchiert", voteNeedsSources: "Quellen nรถtig", voteWrong: "Falsch", + voteOutdated: "Veraltet", voteLowQuality: "Niedrige Qualitรคt", voteLove: "Liebe", voteClear: "Leeren", @@ -2512,7 +2485,6 @@ module.exports = { shopNoProducts: "Noch keine Produkte.", shopOutOfStock: "Ausverkauft", shopBuy: "Kaufen", - shopBuyEncryptedNote: "Deine Lieferdaten werden verschlรผsselt รผbertragen; nur der Shop-Inhaber kann sie lesen.", shopBuyDeliveryAddress: "Lieferadresse", shopBuyContact: "Kontakt", shopBuyContactPlaceholder: "E-Mail, Telefon, usw.", @@ -3029,6 +3001,16 @@ module.exports = { industryDescriptionLabel: "Beschreibung", industryDescriptionPlaceholder: "Was produziert diese Fabrik?", industrySector: "Sektor", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Landwirtschaft", + industrySector_textile: "Textil", + industrySector_energy: "Energie", + industrySector_food: "Lebensmittel", + industrySector_construction: "Bauwesen", + industrySector_media: "Medien", + industrySector_services: "Dienstleistungen", + industrySector_other: "Sonstiges", industryMembershipPolicy: "Mitgliedschaftsrichtlinie", industryQuorum: "Quorum", industryMajority: "Mehrheit", @@ -3230,6 +3212,18 @@ module.exports = { modulesFavoritesDescription: "Modul zum Verwalten deiner favorisierten Inhalte.", fileTooLargeTitle: "Datei zu groรŸ", fileTooLargeMessage: "Die Datei รผberschreitet die maximal erlaubte GrรถรŸe (75 MB). Bitte wรคhle eine kleinere Datei.", + fileWrongType: "Diese Datei hat nicht den passenden Typ fรผr diesen Bereich.", + mediaLockedAfterOpinions: "Es hat bereits Meinungen: Du kannst den Rest bearbeiten, aber nicht das, was bewertet wurde.", + pdfPrevPage: "Vorherige Seite", + pdfNextPage: "Nรคchste Seite", + pdfPage: "Seite", + pdfZoomIn: "VergrรถรŸern", + pdfZoomOut: "Verkleinern", + pdfFullscreen: "Vollbild", + pdfClose: "SchlieรŸen", + pdfRotate: "Drehen", + contentNotAvailableYet: "Dieser Inhalt ist noch nicht verfรผgbar.", + pdfRetry: "Erneut versuchen", goBack: "Zurรผck", errorDismiss: "OK", errorPageTitle: "Etwas ist schiefgelaufen", @@ -3274,8 +3268,6 @@ module.exports = { courtsRespondentInvalid: "Ungรผltiger Beklagter", feedDetailTitle: "Feed-Details", feedOpenDiscussion: "Diskussion erรถffnen", - feedPostComment: "Kommentar verรถffentlichen", - noComments: "Noch keine Kommentare.", mapsLabel: "Karten", mapTitle: "Karten", mapDescription: "Verwalten und teilen Sie Offline-Karten in Ihrem Netzwerk.", @@ -3579,6 +3571,8 @@ module.exports = { podcastTagsPlaceholder: "Tags durch Kommas getrennt eingeben", podcastFilterAll: "ALLE", podcastFilterMine: "MEINE", + podcastFilterFavorites: "FAVORITEN", + podcastEpisodeNumberLabel: "Folgennummer", podcastFilterRecent: "NEU", podcastFilterTop: "TOP", podcastFilterViewers: "Hร–RER", @@ -3797,7 +3791,6 @@ module.exports = { wikiBackToPage: "Zurรผck zur Seite", wikiLinkedChip: "Verknรผpft", wikiRestoredSummary: "Wiederhergestellte Version", - wikiExportPdf: "Seite erzeugen", wikiPreview: "Vorschau", tribeSectionWiki: "WIKI", tribeWikiCreate: "Wiki erstellen", @@ -3865,6 +3858,9 @@ module.exports = { roomRecStop: "Aufnahme beenden", roomStreamFile: "Streamen", roomStreamStop: "Stoppen", + roomScreen: "Bildschirm", + roomScreenSharing: "Bildschirm wird geteilt", + roomScreenWarning: "Alle im Raum sehen deinen ganzen Bildschirm, bis du stoppst.", roomStreaming: "Wird gestreamt", liveFullscreen: "Vollbild", roomStreamPickTitle: "Video auswรคhlen", @@ -3922,6 +3918,7 @@ module.exports = { inviteAlreadyMember: "Du bist schon dabei. Du kannst es รผber sein Modul รถffnen.", actionFailed: "Das hat nicht geklappt. Bitte versuche es erneut.", publishTooShort: "Dein Beitrag ist zu kurz.", + publishEmpty: "Schreibe etwas oder hรคnge eine Datei an.", walletSendFailed: "Die Zahlung konnte nicht gesendet werden: {reason}", pixeliaCoordsError: "Diese Koordinaten liegen auรŸerhalb des Rasters.", courtsErrTitle: "Ein Titel ist erforderlich.", @@ -4430,6 +4427,9 @@ module.exports = { torrentTagsLabel: "Tags", torrentTagsPlaceholder: "Tags durch Kommas getrennt eingeben", torrentSizeLabel: "GrรถรŸe", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Dateien", + torrentMagnet: "MAGNET", torrentFromContent: "Torrent erzeugen", torrentFromContentMissing: "Die Datei ist auf diesem Knoten noch nicht verfรผgbar. Versuche es spรคter erneut.", torrentDownloads: "Downloads", @@ -4515,9 +4515,6 @@ module.exports = { inhabitantsPendingAccept: "Akzeptieren", inhabitantsPendingReject: "Ablehnen", tribeSectionGovernance: "GOVERNANCE", - tribePadInviteRequired: "Kein Zugriff auf das Pad. Bitte um Einladung.", - tribeChatInviteRequired: "Kein Zugriff auf den Chat. Bitte um Einladung.", - tribeGovernanceDesc: "Interne Governance dieses Stammes.", tribeGovernanceNoGov: "Keine aktive Regierung", tribeGovernanceNoGovDesc: "Dieser Stamm hat noch keine Regierung gewรคhlt.", tribeGovCardTitle: "Aktuelle Regierung", @@ -4574,7 +4571,6 @@ module.exports = { logsFilterMonth: "LETZTER MONAT", logsFilterWeek: "LETZTE WOCHE", logsFilterToday: "HEUTE", - logsComments: "Kommentare", logsAuthorEntries: "Eintrรคge", logsCreate: "Eintrag erstellen", logsExport: "Logbuch exportieren", @@ -4652,6 +4648,8 @@ module.exports = { larpWillTestTitle: "WILL-Test", settingsLanDesc: "Diesen Peer periodisch an andere Oasis-Instanzen im selben lokalen Netzwerk ankรผndigen.", settingsLanEnable: "LAN-Broadcasting aktivieren", + settingsConnectionsTitle: "Verbindungen", + settingsConnectionsDesc: "Wie viele Peers dieser Knoten gleichzeitig verbunden hรคlt.", settingsLanTitle: "LAN-Broadcasting", settingsReplicationTitle: "Replikation", settingsReplicationDesc: "Konfiguriere die Anzahl der Hops, die dein Peer von deinem eigenen Feed aus beim Replizieren von Inhalten folgt.", diff --git a/src/client/assets/translations/oasis_en.js b/src/client/assets/translations/oasis_en.js index 6361b35..811fea0 100644 --- a/src/client/assets/translations/oasis_en.js +++ b/src/client/assets/translations/oasis_en.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† My network", graphosShowing: "Showing", graphosYou: "You", - graphosTotalNodes: "Total nodes", + graphosTotalNodes: "Total", + onlinePlural: "Online", mentions: "Mentions", mentionsDescription: [ strong("Every message that @mention you"), @@ -430,6 +431,7 @@ module.exports = { menuMedia: "Library", menuTools: "Tools", comment: "Comment", + typeComment: "Comment", subtopic: "Subtopic", json: "JSON", unfollow: "Unsupport", @@ -459,9 +461,6 @@ module.exports = { publishCustomDescription: [ "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", ], - commentWarning: [ - "REMEMBER: Due to blockchain technology, once a post is published it cannot be edited or deleted.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -513,6 +512,8 @@ module.exports = { settingsBlobCacheCleanNow: "Clean now", settingsBlobCacheCleaned: "{n} files removed, {size} freed.", settingsLanEnable: "Enable LAN broadcasting", + settingsConnectionsTitle: "Connections", + settingsConnectionsDesc: "How many peers this node keeps connected at the same time.", saveSettings: "Save settings", exportDescription: "Set password (min 32 characters long) to encrypt your key", exportDataTitle: "Instant Backup", @@ -754,10 +755,6 @@ module.exports = { votesQuestionLabel:"Question", voteStatusOpen:"OPEN", voteStatusClosed:"CLOSED", - commentDescription: ({ parentUrl }) => [ - " commented on ", - a({ href: parentUrl }, " thread"), - ], commentTitle: ({ authorName }) => [`Comment on @${authorName}'s post`], subtopicDescription: ({ parentUrl }) => [ " created a subtopic from ", @@ -895,6 +892,8 @@ module.exports = { bankRulesEligibility5: "The PUB pays on its next cycle and publishes a UBI transfer that you can confirm.", bankRulesEligibility6: "If your address is missing, the feed is too young, you refused the month or the PUB has no funds, the claim is skipped and logged on the PUB.", copyLinkTitle: "Copy link", + cardMenuTitle: "More actions", + shareContent: "Share", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Refuse UBI", @@ -928,7 +927,7 @@ module.exports = { walletNotConfigured: "Your ECOin wallet is not configured yet.", walletSettingsDescription: "Integrate Oasis with your ECOin wallet.", walletSettingsMobileDescription: "Link Oasis with the ECOin Wallet app on this device.", - walletDownloadEcoin: "Download ECOin!", + walletDownloadEcoin: "Download ECOin", walletRpcUrl: "RPC URL", walletStatusMessages: { invalid_amount: "Invalid amount", @@ -981,6 +980,7 @@ module.exports = { bookmarkUpdateButton: "Update", bookmarkAddFavoriteButton: "Add favorite", bookmarkRemoveFavoriteButton: "Remove favorite", + bookmarkTitleLabel: "Title", bookmarkUrlLabel: "Link", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Description", @@ -1212,9 +1212,9 @@ module.exports = { allCVs: "CVs", discoverPeople: "Discover inhabitants in your network.", searchInhabitantsPlaceholder: "Search in Inhabitants...", - filterLocation: "FILTER inhabitants BY LOCATION โ€ฆ", - filterLanguage: "FILTER inhabitants BY LANGUAGE โ€ฆ", - filterSkills: "FILTER inhabitants BY SKILLS โ€ฆ", + filterLocation: "Search by locationโ€ฆ", + filterLanguage: "Search by languageโ€ฆ", + filterSkills: "Search by skillsโ€ฆ", cvSkillsCount: "Skills", applyFilters: "Apply Filters", locationLabel: "Location", @@ -1230,6 +1230,7 @@ module.exports = { topactivitySectionTitle: "Top Activity", "TOP INACTIVITYButton": "TOP INACTIVITY", topinactivitySectionTitle: "Least active inhabitants", + contactsSectionTitle: "Contacts", blockedSectionTitle: "Blocked", gallerySectionTitle: "GALLERY", topSectionTitle: "TOP", @@ -1684,9 +1685,7 @@ module.exports = { voteUnknownDate: "Unknown Date", voteDetailSectionTitle: 'Vote details', voteCommentsLabel: 'Comments', - voteCommentsForumButton: 'Open discussion', - voteCommentsSectionTitle: 'Open discussion', - voteNoCommentsYet: 'There are no comments yet. Be the first to reply.', + voteNoCommentsYet: 'There are no comments yet.', commentsGoToLast: "Go to the last comment", voteNewCommentPlaceholder: 'Write your comment hereโ€ฆ', voteNewCommentButton: 'Post comment', @@ -1813,7 +1812,6 @@ module.exports = { pollVoteButton: "Vote", pollChangeVote: "Change Vote", pollVoters: "Voters", - pollComments: "Comments", pollStatusLabel: "Status", pollStatusOpen: "OPEN", pollStatusClosed: "CLOSED", @@ -1835,9 +1833,22 @@ module.exports = { blogAllowComments: "Allow comments", blogCommentsClosed: "The author has closed comments on this blog.", blogNoItems: "No blogs found.", - viewBlog: "View Blog", forumTitle: "Forums", forumCategoryLabel: "Category", + forumCatGENERAL: "GENERAL", + forumCatPOLITICS: "POLITICS", + forumCatTECH: "TECH", + forumCatSCIENCE: "SCIENCE", + forumCatMUSIC: "MUSIC", + forumCatART: "ART", + forumCatGAMING: "GAMING", + forumCatBOOKS: "BOOKS", + forumCatFILMS: "FILMS", + forumCatPHILOSOPHY: "PHILOSOPHY", + forumCatSOCIETY: "SOCIETY", + forumCatPRIVACY: "PRIVACY", + forumCatCYBERWARFARE: "CYBERWARFARE", + forumCatSURVIVALISM: "SURVIVALISM", forumTitleLabel: "Title", forumTitlePlaceholder: "Forum title...", forumCreateButton: "Create forum", @@ -2026,8 +2037,6 @@ module.exports = { feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Open discussion", - feedPostComment: "Post comment", - noComments: "No comments yet", createFeedTitle: "Create Feed", createFeedButton: "Send Feed!", feedPlaceholder: "What's happening? (max 280 characters)", @@ -2248,43 +2257,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Under Review Reports", reportsResolvedSectionTitle: "Resolved Reports", reportsInvalidSectionTitle: "Invalid Reports", - reportsBugTemplateTitle: 'Reproduction details (Bugs)', - reportsFeatureTemplateTitle: 'Details (Features)', - reportsAbuseTemplateTitle: 'Details (Abuse)', - reportsContentTemplateTitle: 'Details (Content Issues)', - reportsStepsToReproduceLabel: 'Steps to reproduce', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Expected result', - reportsExpectedBehaviorPlaceholder: 'What should happen?', - reportsActualBehaviorLabel: 'Actual result', - reportsActualBehaviorPlaceholder: 'What actually happens?', - reportsEnvironmentLabel: 'Environment', - reportsEnvironmentPlaceholder: 'Version, device, OS, browser, settings, logs, etc.', - reportsReproduceRateLabel: 'Reproduction rate', - reportsReproduceRateAlways: 'Always', - reportsReproduceRateOften: 'Often', - reportsReproduceRateSometimes: 'Sometimes', - reportsReproduceRateRarely: 'Rarely', - reportsReproduceRateUnable: 'Unable to reproduce', - reportsReproduceRateUnknown: 'Not specified', - reportsProblemStatementLabel: 'Problem / need', - reportsProblemStatementPlaceholder: 'What problem does this feature solve?', - reportsUserStoryLabel: 'Inhabitant story', - reportsUserStoryPlaceholder: 'As a , I want , so that .', - reportsAcceptanceCriteriaLabel: 'Acceptance criteria', - reportsAcceptanceCriteriaPlaceholder: '- Given...\n- When...\n- Then...', - reportsWhatHappenedLabel: 'What happened?', - reportsWhatHappenedPlaceholder: 'Describe the incident with context and approximate dates.', - reportsReportedUserLabel: 'Reported inhabitant / entity', - reportsReportedUserPlaceholder: '@inhabitant, feed, ID, link, etc.', - reportsEvidenceLinksLabel: 'Evidence / links', - reportsEvidenceLinksPlaceholder: 'Paste links, message IDs, screenshots (if applicable), etc.', - reportsContentLocationLabel: 'Where the content is', - reportsContentLocationPlaceholder: 'Link, ID, channel, thread, author, etc.', - reportsWhyInappropriateLabel: "Why it's inappropriate", - reportsWhyInappropriatePlaceholder: 'Explain the reason and impact.', - reportsRequestedActionLabel: 'Requested action', - reportsRequestedActionPlaceholder: 'Remove, hide, tag, warn, etc.', tribesTitle: "Tribes", tribeFilterAll: "ALL", tribeFilterRecent: "RECENT", @@ -2528,6 +2500,7 @@ module.exports = { voteWellResearched: "Well researched", voteNeedsSources: "Needs sources", voteWrong: "Wrong", + voteOutdated: "Outdated", voteLowQuality: "Low quality", voteLove: "Love", voteClear: "Clear", @@ -2772,7 +2745,6 @@ module.exports = { shopNoProducts: "No products yet.", shopOutOfStock: "Out of stock", shopBuy: "Buy", - shopBuyEncryptedNote: "Your delivery details are sent encrypted, visible only to the shop owner.", shopBuyDeliveryAddress: "Delivery address", shopBuyContact: "Contact", shopBuyContactPlaceholder: "email, phone, etc.", @@ -3289,6 +3261,16 @@ module.exports = { industryDescriptionLabel: "Description", industryDescriptionPlaceholder: "What does this facility produce?", industrySector: "Sector", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Agriculture", + industrySector_textile: "Textile", + industrySector_energy: "Energy", + industrySector_food: "Food", + industrySector_construction: "Construction", + industrySector_media: "Media", + industrySector_services: "Services", + industrySector_other: "Other", industryMembershipPolicy: "Membership policy", industryQuorum: "Quorum", industryMajority: "Majority", @@ -3490,6 +3472,18 @@ module.exports = { modulesFavoritesDescription: "Module to manage your favorite content.", fileTooLargeTitle: "File too large", fileTooLargeMessage: "The file exceeds the maximum allowed size (75 MB). Please select a smaller file.", + fileWrongType: "This file is not the right type for this section.", + mediaLockedAfterOpinions: "It already has opinions: you can edit the rest, but not change what was rated.", + pdfPrevPage: "Previous page", + pdfNextPage: "Next page", + pdfPage: "Page", + pdfZoomIn: "Zoom in", + pdfZoomOut: "Zoom out", + pdfFullscreen: "Full screen", + pdfClose: "Close", + pdfRotate: "Rotate", + contentNotAvailableYet: "This content is not available, yet.", + pdfRetry: "Retry", goBack: "Go back", errorDismiss: "OK", errorPageTitle: "Something went wrong", @@ -3834,6 +3828,8 @@ module.exports = { podcastTagsPlaceholder: "Enter tags separated by commas", podcastFilterAll: "ALL", podcastFilterMine: "MINE", + podcastFilterFavorites: "FAVORITES", + podcastEpisodeNumberLabel: "Episode number", podcastFilterRecent: "RECENT", podcastFilterTop: "TOP", podcastFilterViewers: "VIEWERS", @@ -4052,7 +4048,6 @@ module.exports = { wikiBackToPage: "Back to page", wikiLinkedChip: "Linked", wikiRestoredSummary: "Restored version", - wikiExportPdf: "Generate Page", wikiPreview: "Preview", tribeSectionWiki: "WIKI", tribeWikiCreate: "Create Wiki", @@ -4121,6 +4116,9 @@ module.exports = { roomRecStop: "Stop recording", roomStreamFile: "Stream", roomStreamStop: "Stop", + roomScreen: "Screen", + roomScreenSharing: "Sharing screen", + roomScreenWarning: "Everyone in the room will see your whole screen until you stop.", roomStreaming: "Streaming", liveFullscreen: "Full screen", roomStreamPickTitle: "Choose a video", @@ -4178,6 +4176,7 @@ module.exports = { inviteAlreadyMember: "You are already in. You can open it from its module.", actionFailed: "That could not be done. Please try again.", publishTooShort: "Your post is too short.", + publishEmpty: "Write something or attach a file.", walletSendFailed: "The payment could not be sent: {reason}", pixeliaCoordsError: "Those coordinates are outside the grid.", courtsErrTitle: "A title is required.", @@ -4751,6 +4750,9 @@ module.exports = { torrentTagsLabel: "Tags", torrentTagsPlaceholder: "Enter tags separated by commas", torrentSizeLabel: "Size", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Files", + torrentMagnet: "MAGNET", torrentFromContent: "Generate torrent", torrentFromContentMissing: "The file is not available on this node yet. Try again in a while.", torrentDownloads: "Downloads", @@ -4836,9 +4838,6 @@ module.exports = { inhabitantsPendingAccept: "Accept", inhabitantsPendingReject: "Reject", tribeSectionGovernance: "GOVERNANCE", - tribePadInviteRequired: "You do not have access to the pad. Ask for an invitation to access the content.", - tribeChatInviteRequired: "You do not have access to the chat. Ask for an invitation to access the content.", - tribeGovernanceDesc: "Internal governance for this tribe. Propose candidatures, debate rules, elect leaders.", tribeGovernanceNoGov: "No active government", tribeGovernanceNoGovDesc: "This tribe has not yet elected a government. Propose candidatures to start the process.", tribeGovCardTitle: "Current Government", @@ -4895,7 +4894,6 @@ module.exports = { logsFilterMonth: "LAST MONTH", logsFilterWeek: "LAST WEEK", logsFilterToday: "TODAY", - logsComments: "Comments", logsAuthorEntries: "Entries", logsCreate: "Create Log", logsExport: "Export Logs", diff --git a/src/client/assets/translations/oasis_es.js b/src/client/assets/translations/oasis_es.js index cb106b8..19a1e27 100644 --- a/src/client/assets/translations/oasis_es.js +++ b/src/client/assets/translations/oasis_es.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† Mi red", graphosShowing: "Mostrando", graphosYou: "Tรบ", - graphosTotalNodes: "Nodos totales", + graphosTotalNodes: "Totales", + onlinePlural: "Conectados", mentions: "Menciones", mentionsDescription: [ strong("Todos los mensajes que te @mencionan"), @@ -412,6 +413,7 @@ module.exports = { menuMedia: "Biblioteca", menuTools: "Herramientas", comment: "Comentar", + typeComment: "Comentario", subtopic: "Subtema", json: "JSON", unfollow: "Dejar de soportar", @@ -441,9 +443,6 @@ module.exports = { publishCustomDescription: [ "RECUERDA: debido a la tecnologรญa blockchain, una vez publicas algo, no se puede editar ni borrar. ยกPiรฉnsalo bien!", ], - commentWarning: [ - "RECUERDA: debido a la tecnologรญa blockchain, una vez publicas algo, no se puede editar ni borrar. ยกPiรฉnsalo bien!", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -718,10 +717,6 @@ module.exports = { votesQuestionLabel:"Pregunta", voteStatusOpen:"ABIERTO", voteStatusClosed:"CERRADO", - commentDescription: ({ parentUrl }) => [ - " ha comentado en ", - a({ href: parentUrl }, " hilo"), - ], commentTitle: ({ authorName }) => [`Ha comentado en el post de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " ha creado un subtema desde ", @@ -856,6 +851,8 @@ module.exports = { bankRulesEligibility5: "El PUB paga en su siguiente ciclo y publica un transfer RBU que puedes confirmar.", bankRulesEligibility6: "Si falta tu direcciรณn, el feed es demasiado joven, rechazaste el mes o el PUB no tiene fondos, el reclamo se salta y queda registrado en el PUB.", copyLinkTitle: "Copiar enlace", + cardMenuTitle: "Mรกs acciones", + shareContent: "Compartir", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Rechazar RBU", @@ -889,7 +886,7 @@ module.exports = { walletNotConfigured: "Tu cartera de ECOin aรบn no estรก configurada.", walletSettingsDescription: "Integrar Oasis con tu cartera de ECOin.", walletSettingsMobileDescription: "Vincula Oasis con la app ECOin Wallet de este dispositivo.", - walletDownloadEcoin: "ยกDescargar ECOin!", + walletDownloadEcoin: "Descargar ECOin", walletRpcUrl: "URL del RPC", walletStatusMessages: { invalid_amount: "Cantidad invรกlida", @@ -942,6 +939,7 @@ module.exports = { bookmarkUpdateButton: "Actualizar", bookmarkAddFavoriteButton: "Aรฑadir favorito", bookmarkRemoveFavoriteButton: "Quitar favorito", + bookmarkTitleLabel: "Tรญtulo", bookmarkUrlLabel: "Enlace", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descripciรณn", @@ -1086,9 +1084,9 @@ module.exports = { allCVs: "CVs", discoverPeople: "Descubre habitantes en tu red.", searchInhabitantsPlaceholder: "Buscar en Habitantes...", - filterLocation: "FILTRAR habitantes POR UBICACIร“N โ€ฆ", - filterLanguage: "FILTRAR habitantes POR IDIOMA โ€ฆ", - filterSkills: "FILTRAR habitantes POR HABILIDADES โ€ฆ", + filterLocation: "Buscar por ubicaciรณnโ€ฆ", + filterLanguage: "Buscar por idiomaโ€ฆ", + filterSkills: "Buscar por habilidadesโ€ฆ", cvSkillsCount: "Habilidades", applyFilters: "Aplicar Filtros", locationLabel: "Ubicaciรณn", @@ -1104,6 +1102,7 @@ module.exports = { topactivitySectionTitle: "Top Actividad", "TOP INACTIVITYButton": "MรS INACTIVOS", topinactivitySectionTitle: "Habitantes menos activos", + contactsSectionTitle: "Contactos", blockedSectionTitle: "Bloqueadas", gallerySectionTitle: "GALERรA", topSectionTitle: "TOP", @@ -1558,9 +1557,7 @@ module.exports = { voteUnknownDate: "Fecha Desconocida", voteDetailSectionTitle: 'Detalles de la votaciรณn', voteCommentsLabel: 'Comentarios', - voteCommentsForumButton: 'Abrir conversaciรณn', - voteCommentsSectionTitle: 'Conversaciรณn abierta', - voteNoCommentsYet: 'Todavรญa no hay comentarios. Sรฉ el primer habitante en opinar.', + voteNoCommentsYet: 'Todavรญa no hay comentarios.', commentsGoToLast: "Ir al รบltimo comentario", voteNewCommentPlaceholder: 'Escribe aquรญ tu comentarioโ€ฆ', voteNewCommentButton: 'Publicar comentario', @@ -1687,7 +1684,6 @@ module.exports = { pollVoteButton: "Votar", pollChangeVote: "Cambiar Voto", pollVoters: "Votantes", - pollComments: "Comentarios", pollStatusLabel: "Estado", pollStatusOpen: "ABIERTA", pollStatusClosed: "CERRADA", @@ -1709,9 +1705,22 @@ module.exports = { blogAllowComments: "Permitir comentarios", blogCommentsClosed: "La autora ha cerrado los comentarios de este blog.", blogNoItems: "No se han encontrado blogs.", - viewBlog: "Ver Blog", forumTitle: "Foros", forumCategoryLabel: "Categorรญa", + forumCatGENERAL: "GENERAL", + forumCatPOLITICS: "POLรTICA", + forumCatTECH: "TECNOLOGรA", + forumCatSCIENCE: "CIENCIA", + forumCatMUSIC: "MรšSICA", + forumCatART: "ARTE", + forumCatGAMING: "JUEGOS", + forumCatBOOKS: "LIBROS", + forumCatFILMS: "PELรCULAS", + forumCatPHILOSOPHY: "FILOSOFรA", + forumCatSOCIETY: "SOCIEDAD", + forumCatPRIVACY: "PRIVACIDAD", + forumCatCYBERWARFARE: "CIBERGUERRA", + forumCatSURVIVALISM: "SUPERVIVENCIA", forumTitleLabel: "Tรญtulo", forumTitlePlaceholder: "Tรญtulo del foro...", forumCreateButton: "Crear foro", @@ -1770,8 +1779,6 @@ module.exports = { feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Abrir discusiรณn", - feedPostComment: "Publicar comentario", - noComments: "Aรบn no hay comentarios", createFeedTitle: "Crear Feed", createFeedButton: "ยกEnviar feed!", feedPlaceholder: "ยฟQuรฉ estรก pasando? (mรกximo 280 caracteres)", @@ -1982,42 +1989,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Informes en Revisiรณn", reportsResolvedSectionTitle: "Informes Resueltos", reportsInvalidSectionTitle: "Informes Invรกlidos", - reportsBugTemplateTitle: 'Datos para reproducir (Bugs)', - reportsFeatureTemplateTitle: 'Detalles (Features)', - reportsAbuseTemplateTitle: 'Detalles (Abuso)', - reportsContentTemplateTitle: 'Detalles (Problemas de Contenido)', - reportsStepsToReproduceLabel: 'Pasos para reproducir', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Resultado esperado', - reportsExpectedBehaviorPlaceholder: 'ยฟQuรฉ deberรญa pasar?', - reportsActualBehaviorLabel: 'Resultado actual', - reportsActualBehaviorPlaceholder: 'ยฟQuรฉ pasa realmente?', - reportsEnvironmentLabel: 'Entorno', - reportsEnvironmentPlaceholder: 'Versiรณn, dispositivo, OS, navegador, configuraciรณn, logs, etc.', - reportsReproduceRateLabel: 'Frecuencia', - reportsReproduceRateAlways: 'Siempre', - reportsReproduceRateOften: 'A menudo', - reportsReproduceRateSometimes: 'A veces', - reportsReproduceRateRarely: 'Rara vez', - reportsReproduceRateUnable: 'No se puede reproducir', - reportsReproduceRateUnknown: 'No especificado', - reportsProblemStatementLabel: 'Problema / necesidad', - reportsProblemStatementPlaceholder: 'ยฟQuรฉ problema resuelve esta feature?', - reportsUserStoryLabel: 'Historia de habitante', - reportsUserStoryPlaceholder: 'Como , quiero , para .', - reportsAcceptanceCriteriaLabel: 'Criterios de aceptaciรณn', - reportsAcceptanceCriteriaPlaceholder: '- Dado...\n- Cuando...\n- Entonces...', - reportsWhatHappenedLabel: 'ยฟQuรฉ ocurriรณ?', - reportsWhatHappenedPlaceholder: 'Describe el incidente con contexto y fechas aproximadas.', - reportsReportedUserLabel: 'Habitante / entidad reportada', - reportsReportedUserPlaceholder: '@habitante, feed, ID, enlace, etc.', - reportsEvidenceLinksLabel: 'Evidencia / enlaces', - reportsEvidenceLinksPlaceholder: 'Pega enlaces, IDs de mensajes, capturas (si aplica), etc.', - reportsContentLocationLabel: 'Dรณnde estรก el contenido', - reportsContentLocationPlaceholder: 'Enlace, ID, canal, hilo, autorรญa, etc.', - reportsWhyInappropriatePlaceholder: 'Explica el motivo y el impacto.', - reportsRequestedActionLabel: 'Acciรณn solicitada', - reportsRequestedActionPlaceholder: 'Eliminar, ocultar, marcar, advertir, etc.', tribesTitle: "Tribus", tribeFilterAll: "TODAS", tribeFilterRecent: "RECIENTES", @@ -2261,6 +2232,7 @@ module.exports = { voteWellResearched: "Bien investigado", voteNeedsSources: "Necesita fuentes", voteWrong: "Incorrecto", + voteOutdated: "Desactualizado", voteLowQuality: "Baja calidad", voteLove: "Amor", voteClear: "Claro", @@ -2505,7 +2477,6 @@ module.exports = { shopNoProducts: "Aรบn no hay productos.", shopOutOfStock: "Agotado", shopBuy: "Comprar", - shopBuyEncryptedNote: "Tus datos de entrega se envรญan cifrados; solo la dueรฑa de la tienda puede verlos.", shopBuyDeliveryAddress: "Direcciรณn de entrega", shopBuyContact: "Contacto", shopBuyContactPlaceholder: "correo, telรฉfono, etc.", @@ -3022,6 +2993,16 @@ module.exports = { industryDescriptionLabel: "Descripciรณn", industryDescriptionPlaceholder: "ยฟQuรฉ produce esta fรกbrica?", industrySector: "Sector", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Agricultura", + industrySector_textile: "Textil", + industrySector_energy: "Energรญa", + industrySector_food: "Alimentaciรณn", + industrySector_construction: "Construcciรณn", + industrySector_media: "Medios", + industrySector_services: "Servicios", + industrySector_other: "Otros", industryMembershipPolicy: "Polรญtica de membresรญa", industryQuorum: "Quรณrum", industryMajority: "Mayorรญa", @@ -3223,6 +3204,18 @@ module.exports = { modulesFavoritesDescription: "Mรณdulo para gestionar tu contenido favorito.", fileTooLargeTitle: "Archivo demasiado grande", fileTooLargeMessage: "El archivo supera el tamaรฑo mรกximo permitido (75 MB). Por favor, selecciona un archivo mรกs pequeรฑo.", + fileWrongType: "Este fichero no es del tipo adecuado para esta secciรณn.", + mediaLockedAfterOpinions: "Ya tiene opiniones: puedes editar el resto, pero no cambiar lo que se ha valorado.", + pdfPrevPage: "Pรกgina anterior", + pdfNextPage: "Pรกgina siguiente", + pdfPage: "Pรกgina", + pdfZoomIn: "Acercar", + pdfZoomOut: "Alejar", + pdfFullscreen: "Pantalla completa", + pdfClose: "Cerrar", + pdfRotate: "Girar", + contentNotAvailableYet: "Este contenido aรบn no estรก disponible.", + pdfRetry: "Reintentar", goBack: "Volver", errorDismiss: "OK", errorPageTitle: "Ha ocurrido un error", @@ -3267,7 +3260,6 @@ module.exports = { invitesFollow: "Seguir", invitesUnfollowedInvites: "No federadas", invitesNoUnfollowed: "No hay pubs no federados.", - reportsWhyInappropriateLabel: "ยฟPor quรฉ es inapropiado?", blockchainContentDeleted: "Este contenido ha sido eliminado", visitContent: "Visitar Contenido", favoriteAdd: "Fijar en Favoritos", @@ -3579,6 +3571,8 @@ module.exports = { podcastTagsPlaceholder: "Introduce etiquetas separadas por comas", podcastFilterAll: "TODOS", podcastFilterMine: "MรOS", + podcastFilterFavorites: "FAVORITOS", + podcastEpisodeNumberLabel: "Nรบmero de episodio", podcastFilterRecent: "RECIENTES", podcastFilterTop: "TOP", podcastFilterViewers: "OYENTES", @@ -3797,7 +3791,6 @@ module.exports = { wikiBackToPage: "Volver a la pรกgina", wikiLinkedChip: "Enlazada", wikiRestoredSummary: "Versiรณn restaurada", - wikiExportPdf: "Generar pรกgina", wikiPreview: "Vista previa", tribeSectionWiki: "WIKI", tribeWikiCreate: "Crear wiki", @@ -3865,6 +3858,9 @@ module.exports = { roomRecStop: "Parar grabaciรณn", roomStreamFile: "Emitir", roomStreamStop: "Parar", + roomScreen: "Pantalla", + roomScreenSharing: "Compartiendo pantalla", + roomScreenWarning: "Todos en la sala verรกn tu pantalla completa hasta que pares.", roomStreaming: "Emitiendo", liveFullscreen: "Pantalla completa", roomStreamPickTitle: "Elige un vรญdeo", @@ -3922,6 +3918,7 @@ module.exports = { inviteAlreadyMember: "Ya formas parte. Puedes abrirlo desde su mรณdulo.", actionFailed: "No se ha podido hacer. Intรฉntalo de nuevo.", publishTooShort: "Tu publicaciรณn es demasiado corta.", + publishEmpty: "Escribe algo o adjunta un fichero.", walletSendFailed: "No se ha podido enviar el pago: {reason}", pixeliaCoordsError: "Esas coordenadas estรกn fuera de la cuadrรญcula.", courtsErrTitle: "Hace falta un tรญtulo.", @@ -4434,6 +4431,9 @@ module.exports = { torrentTagsLabel: "Etiquetas", torrentTagsPlaceholder: "Introduce etiquetas separadas por comas", torrentSizeLabel: "Tamaรฑo", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Ficheros", + torrentMagnet: "MAGNET", torrentFromContent: "Generar torrent", torrentFromContentMissing: "El fichero aรบn no estรก disponible en este nodo. Intรฉntalo en un rato.", torrentDownloads: "Descargas", @@ -4519,9 +4519,6 @@ module.exports = { inhabitantsPendingAccept: "Aceptar", inhabitantsPendingReject: "Rechazar", tribeSectionGovernance: "GOBIERNO", - tribePadInviteRequired: "No tienes acceso al pad. Pide una invitaciรณn para acceder al contenido.", - tribeChatInviteRequired: "No tienes acceso al chat. Pide una invitaciรณn para acceder al contenido.", - tribeGovernanceDesc: "Gobierno interno de esta tribu.", tribeGovernanceNoGov: "Sin gobierno activo", tribeGovernanceNoGovDesc: "Esta tribu aรบn no tiene gobierno. Propรณn candidaturas para iniciar el proceso.", tribeGovCardTitle: "Gobierno Actual", @@ -4578,7 +4575,6 @@ module.exports = { logsFilterMonth: "รšLTIMO MES", logsFilterWeek: "รšLTIMA SEMANA", logsFilterToday: "HOY", - logsComments: "Comentarios", logsAuthorEntries: "Entradas", logsCreate: "Crear entrada", logsExport: "Exportar bitรกcora", @@ -4600,7 +4596,7 @@ module.exports = { larpNoHousesMatch: "Ninguna casa coincide con tu bรบsqueda.", larpSearchPlaceholder: "Buscar en L.A.R.P....", larpAllHouses: "Casas:", - larpFilterRuling: "RIGE", + larpFilterRuling: "GOBERNANDO", larpFilterHouses: "CASAS", larpFilterRules: "FAQ", larpRulesTitle: "FAQ del L.A.R.P.", @@ -4657,6 +4653,8 @@ module.exports = { larpWillTestTitle: "Test WILL", settingsLanDesc: "Anuncia periรณdicamente este peer a otras instancias de Oasis en la misma red local.", settingsLanEnable: "Activar difusiรณn LAN", + settingsConnectionsTitle: "Conexiones", + settingsConnectionsDesc: "Cuรกntos peers mantiene conectados este nodo a la vez.", settingsLanTitle: "Difusiรณn LAN", settingsReplicationTitle: "Replicaciรณn", settingsReplicationDesc: "Configura el nรบmero de saltos que tu nodo sigue desde tu propio feed al replicar contenido.", @@ -4790,7 +4788,7 @@ module.exports = { larpGoToHouse: "Ir a tu casa", larpBackToAcademia: "Volver a ACADEMIA", larpBadgeYou: "Tรบ", - larpBadgeRuling: "Rige", + larpBadgeRuling: "Gobernando", modulesLarpLabel: "L.A.R.P.", modulesLarpDescription: "Mรณdulo para la capa de juego de rol en vivo.", larpProfileQ1: "Cuando te enfrentas a un problema complejo, ยฟquรฉ haces primero?", diff --git a/src/client/assets/translations/oasis_eu.js b/src/client/assets/translations/oasis_eu.js index c5fc2a2..66ba75c 100644 --- a/src/client/assets/translations/oasis_eu.js +++ b/src/client/assets/translations/oasis_eu.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† Nire sarea", graphosShowing: "Erakusten", graphosYou: "Zu", - graphosTotalNodes: "Nodo guztiak", + graphosTotalNodes: "Guztira", + onlinePlural: "Linean", mentions: "Aipamenak", mentionsDescription: [ strong("Zu @aipatzen zaituzten mezu guztiak"), @@ -414,6 +415,7 @@ module.exports = { menuMedia: "Liburutegia", menuTools: "Tresnak", comment: "Iruzkindu", + typeComment: "Iruzkina", subtopic: "Azpi-gaia", json: "JSON", unfollow: "Laguntza eten", @@ -443,9 +445,6 @@ module.exports = { publishCustomDescription: [ "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", ], - commentWarning: [ - "GOGORATU: Blockchain teknologia dela eta, bidalketa argitaratu ezkero, ezin izango da aldatu ezta ezabatu.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -724,10 +723,6 @@ module.exports = { votesQuestionLabel:"Galdera", voteStatusOpen:"IREKITA", voteStatusClosed:"ITXITA", - commentDescription: ({ parentUrl }) => [ - " noiz komentatuta ", - a({ href: parentUrl }, " haria"), - ], commentTitle: ({ authorName }) => [`Komentatu @${authorName}-(r)en bidalketa`], subtopicDescription: ({ parentUrl }) => [ " azpigaia hemendik sortuta ", @@ -862,6 +857,8 @@ module.exports = { bankRulesEligibility5: "PUBak hurrengo zikloan ordaintzen du eta berretsi dezakezun OBU transferentzia bat argitaratzen du.", bankRulesEligibility6: "Helbidea falta bada, feeda gazteegia bada, hilabeteari uko egin badiozu edo PUBak fondorik ez badu, eskaera saltatu eta PUBean erregistratzen da.", copyLinkTitle: "Kopiatu esteka", + cardMenuTitle: "Ekintza gehiago", + shareContent: "Partekatu", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "OBUari uko egin", @@ -895,7 +892,7 @@ module.exports = { walletNotConfigured: "Zure ECOin zorroa ez dago oraindik konfiguratuta.", walletSettingsDescription: "Integratu Oasis zure ECOin zorroarekin.", walletSettingsMobileDescription: "Lotu Oasis gailu honetako ECOin Wallet aplikazioarekin.", - walletDownloadEcoin: "Deskargatu ECOin!", + walletDownloadEcoin: "Deskargatu ECOin", walletRpcUrl: "RPC URLa", walletStatusMessages: { invalid_amount: "Zenbatekoa oker", @@ -948,6 +945,7 @@ module.exports = { bookmarkUpdateButton: "Eguneratu", bookmarkAddFavoriteButton: "Gogokoetara gehitu", bookmarkRemoveFavoriteButton: "Gogokoetatik kendu", + bookmarkTitleLabel: "Izenburua", bookmarkUrlLabel: "Esteka", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Deskribapena", @@ -1092,9 +1090,9 @@ module.exports = { allCVs: "CVak", discoverPeople: "Aurkitu bizilagunak zure sarean.", searchInhabitantsPlaceholder: "Bilatu hemen: Bizilagunak...", - filterLocation: "IRAGAZI bizilagunak KOKAPENAREN ARABERA โ€ฆ", - filterLanguage: "IRAGAZI bizilagunak BY HIZKUNTZAREN ARABERA โ€ฆ", - filterSkills: "IRAGAZI bizilagunak BY TREBETASUNEN ARABERA โ€ฆ", + filterLocation: "Bilatu kokapenaren araberaโ€ฆ", + filterLanguage: "Bilatu hizkuntzaren araberaโ€ฆ", + filterSkills: "Bilatu trebetasunen araberaโ€ฆ", cvSkillsCount: "Trebetasunak", applyFilters: "Ezarri Iragazkiak", locationLabel: "Kokapena", @@ -1110,6 +1108,7 @@ module.exports = { topactivitySectionTitle: "Top Jarduera", "TOP INACTIVITYButton": "INAKTIBOENAK", topinactivitySectionTitle: "Biztanle gutxien aktiboak", + contactsSectionTitle: "Kontaktuak", blockedSectionTitle: "Blokeatuta", gallerySectionTitle: "GALERIA", topSectionTitle: "TOP", @@ -1599,9 +1598,7 @@ module.exports = { voteUnknownDate: "Data Ezezaguna", voteDetailSectionTitle: 'Bozketa xehetasunak', voteCommentsLabel: 'Iruzkinak', - voteCommentsForumButton: 'Eztabaida ireki', - voteCommentsSectionTitle: 'Eztabaida irekia', - voteNoCommentsYet: 'Oraindik ez dago iruzkinik. Izan zaitez lehen erantzuten.', + voteNoCommentsYet: 'Oraindik ez dago iruzkinik.', commentsGoToLast: "Joan azken iruzkinera", voteNewCommentPlaceholder: 'Idatzi hemen zure iruzkinaโ€ฆ', voteNewCommentButton: 'Bidali iruzkina', @@ -1728,7 +1725,6 @@ module.exports = { pollVoteButton: "Bozkatu", pollChangeVote: "Aldatu Botoa", pollVoters: "Bozkatzaileak", - pollComments: "Iruzkinak", pollStatusLabel: "Egoera", pollStatusOpen: "IREKIA", pollStatusClosed: "ITXIA", @@ -1750,9 +1746,22 @@ module.exports = { blogAllowComments: "Baimendu iruzkinak", blogCommentsClosed: "Egileak blog honen iruzkinak itxi ditu.", blogNoItems: "Ez da blogik aurkitu.", - viewBlog: "Ikusi Bloga", forumTitle: "Foroak", forumCategoryLabel: "Kategoria", + forumCatGENERAL: "OROKORRA", + forumCatPOLITICS: "POLITIKA", + forumCatTECH: "TEKNOLOGIA", + forumCatSCIENCE: "ZIENTZIA", + forumCatMUSIC: "MUSIKA", + forumCatART: "ARTEA", + forumCatGAMING: "JOKOAK", + forumCatBOOKS: "LIBURUAK", + forumCatFILMS: "FILMAK", + forumCatPHILOSOPHY: "FILOSOFIA", + forumCatSOCIETY: "GIZARTEA", + forumCatPRIVACY: "PRIBATUTASUNA", + forumCatCYBERWARFARE: "ZIBERGERRA", + forumCatSURVIVALISM: "BIZIRAUPENA", forumTitleLabel: "Izenburua", forumTitlePlaceholder: "Foroaren izenburua...", forumCreateButton: "Sortu foroa", @@ -1811,8 +1820,6 @@ module.exports = { feedTitle: "Jarioa", feedDetailTitle: "Feed", feedOpenDiscussion: "Eztabaida ireki", - feedPostComment: "Iruzkina argitaratu", - noComments: "Oraindik iruzkinik ez", createFeedTitle: "Sortu Jarioa", createFeedButton: "Bidali Jarioa!", feedPlaceholder: "Zer berri? (max 280 characters)", @@ -1986,42 +1993,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Txostenak Berrikuspenean", reportsResolvedSectionTitle: "Zuzendutako Txostenak", reportsInvalidSectionTitle: "Txosten Okerrak", - reportsBugTemplateTitle: 'Erreprodukzio xehetasunak (Bug-ak)', - reportsFeatureTemplateTitle: 'Xehetasunak (Ezaugarriak)', - reportsAbuseTemplateTitle: 'Xehetasunak (Abusua)', - reportsContentTemplateTitle: 'Xehetasunak (Arazoak Edukiarekin)', - reportsStepsToReproduceLabel: 'Erreproduzitzeko urratsak', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Espero den emaitza', - reportsExpectedBehaviorPlaceholder: 'Zer gertatu beharko litzateke?', - reportsActualBehaviorLabel: 'Benetako emaitza', - reportsActualBehaviorPlaceholder: 'Zer gertatzen da benetan?', - reportsEnvironmentLabel: 'Ingurunea', - reportsEnvironmentPlaceholder: 'Bertsioa, gailua, OS, nabigatzailea, ezarpenak, log-ak, etab.', - reportsReproduceRateLabel: 'Maiztasuna', - reportsReproduceRateAlways: 'Beti', - reportsReproduceRateOften: 'Askotan', - reportsReproduceRateSometimes: 'Batzuetan', - reportsReproduceRateRarely: 'Gutxitan', - reportsReproduceRateUnable: 'Ezin da erreproduzitu', - reportsReproduceRateUnknown: 'Zehaztu gabe', - reportsProblemStatementLabel: 'Arazoa / beharra', - reportsProblemStatementPlaceholder: 'Zein arazo konpontzen du ezaugarri honek?', - reportsUserStoryLabel: 'User story', - reportsUserStoryPlaceholder: ' naizenez, nahi dut, lortzeko.', - reportsAcceptanceCriteriaLabel: 'Onarpen-irizpideak', - reportsAcceptanceCriteriaPlaceholder: '- Emanda...\n- Noiz...\n- Orduan...', - reportsWhatHappenedLabel: 'Zer gertatu da?', - reportsWhatHappenedPlaceholder: 'Deskribatu gertakaria testuinguruarekin eta gutxi gorabeherako datekin.', - reportsReportedUserLabel: 'Salatutako erabiltzailea / entitatea', - reportsReportedUserPlaceholder: '@erabiltzailea, feed, ID, esteka, etab.', - reportsEvidenceLinksLabel: 'Frogak / estekak', - reportsEvidenceLinksPlaceholder: 'Itsatsi estekak, mezu ID-ak, pantaila-argazkiak (bada), etab.', - reportsContentLocationLabel: 'Non dago edukia', - reportsContentLocationPlaceholder: 'Esteka, ID, kanala, haria, egilea, etab.', - reportsWhyInappropriatePlaceholder: 'Azaldu arrazoia eta eragina.', - reportsRequestedActionLabel: 'Eskatutako ekintza', - reportsRequestedActionPlaceholder: 'Kendu, ezkutatu, etiketatu, ohartarazi, etab.', tribesTitle: "Tribuak", tribeFilterAll: "GUZTIAK", tribeFilterRecent: "BERRIAK", @@ -2265,6 +2236,7 @@ module.exports = { voteWellResearched: "Ondo ikertua", voteNeedsSources: "Iturririk behar du", voteWrong: "Okerrak", + voteOutdated: "Zaharkitua", voteLowQuality: "Behera kalitatea", voteLove: "Maitasuna", voteClear: "Argi", @@ -2510,7 +2482,6 @@ module.exports = { shopNoProducts: "Oraindik produkturik ez.", shopOutOfStock: "Agortuta", shopBuy: "Erosi", - shopBuyEncryptedNote: "Zure entrega datuak zifratuta bidaltzen dira, dendaren jabeak soilik ikus ditzake.", shopBuyDeliveryAddress: "Entrega helbidea", shopBuyContact: "Kontaktua", shopBuyContactPlaceholder: "e-posta, telefonoa, etab.", @@ -3026,6 +2997,16 @@ module.exports = { industryDescriptionLabel: "Deskribapena", industryDescriptionPlaceholder: "Zer ekoizten du fabrika honek?", industrySector: "Sektorea", + industrySector_software: "Softwarea", + industrySector_hardware: "Hardwarea", + industrySector_agriculture: "Nekazaritza", + industrySector_textile: "Ehungintza", + industrySector_energy: "Energia", + industrySector_food: "Elikadura", + industrySector_construction: "Eraikuntza", + industrySector_media: "Hedabideak", + industrySector_services: "Zerbitzuak", + industrySector_other: "Besteak", industryMembershipPolicy: "Kidetza politika", industryQuorum: "Quoruma", industryMajority: "Gehiengoa", @@ -3227,6 +3208,18 @@ module.exports = { modulesFavoritesDescription: "Zure gogoko edukia kudeatzeko modulua.", fileTooLargeTitle: "Fitxategia handiegia", fileTooLargeMessage: "Fitxategiak onartutako gehienezko tamaina gainditzen du (75 MB). Mesedez, hautatu fitxategi txikiago bat.", + fileWrongType: "Fitxategi hau ez da atal honetarako mota egokikoa.", + mediaLockedAfterOpinions: "Iritziak ditu dagoeneko: gainerakoa edita dezakezu, baina ez baloratutakoa aldatu.", + pdfPrevPage: "Aurreko orria", + pdfNextPage: "Hurrengo orria", + pdfPage: "Orria", + pdfZoomIn: "Hurbildu", + pdfZoomOut: "Urrundu", + pdfFullscreen: "Pantaila osoa", + pdfClose: "Itxi", + pdfRotate: "Biratu", + contentNotAvailableYet: "Eduki hau ez dago oraindik eskuragarri.", + pdfRetry: "Saiatu berriro", goBack: "Itzuli", errorDismiss: "Ados", errorPageTitle: "Zerbait gaizki joan da", @@ -3268,7 +3261,6 @@ module.exports = { statsCarbonTombstone: "Tombstoning-aren aztarna", uploadMedia: "Multimedia igo (max: 75MB)", - reportsWhyInappropriateLabel: "Zergatik da desegokia?", visitContent: "Bisitatu Edukia", favoriteAdd: "Ainguratu Gogokoetan", pmContentTooltip: "Bidali Mezu Pribatua", @@ -3579,6 +3571,8 @@ module.exports = { podcastTagsPlaceholder: "Idatzi etiketak komaz bereizita", podcastFilterAll: "GUZTIAK", podcastFilterMine: "NIREAK", + podcastFilterFavorites: "GOGOKOAK", + podcastEpisodeNumberLabel: "Atal zenbakia", podcastFilterRecent: "AZKENAK", podcastFilterTop: "TOP", podcastFilterViewers: "ENTZULEAK", @@ -3797,7 +3791,6 @@ module.exports = { wikiBackToPage: "Itzuli orrira", wikiLinkedChip: "Lotuta", wikiRestoredSummary: "Berreskuratutako bertsioa", - wikiExportPdf: "Orria sortu", wikiPreview: "Aurrebista", tribeSectionWiki: "WIKIA", tribeWikiCreate: "Wikia sortu", @@ -3865,6 +3858,9 @@ module.exports = { roomRecStop: "Grabazioa gelditu", roomStreamFile: "Igorri", roomStreamStop: "Gelditu", + roomScreen: "Pantaila", + roomScreenSharing: "Pantaila partekatzen", + roomScreenWarning: "Gelako guztiek zure pantaila osoa ikusiko dute gelditu arte.", roomStreaming: "Igortzen", liveFullscreen: "Pantaila osoa", roomStreamPickTitle: "Aukeratu bideo bat", @@ -3922,6 +3918,7 @@ module.exports = { inviteAlreadyMember: "Dagoeneko bertan zaude. Bere modulutik ireki dezakezu.", actionFailed: "Ezin izan da egin. Saiatu berriro.", publishTooShort: "Zure argitalpena laburregia da.", + publishEmpty: "Idatzi zerbait edo erantsi fitxategi bat.", walletSendFailed: "Ezin izan da ordainketa bidali: {reason}", pixeliaCoordsError: "Koordenatu horiek saretik kanpo daude.", courtsErrTitle: "Izenburu bat behar da.", @@ -4433,6 +4430,9 @@ module.exports = { torrentTagsLabel: "Etiketak", torrentTagsPlaceholder: "Idatzi etiketak komaz bereizita", torrentSizeLabel: "Tamaina", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Fitxategiak", + torrentMagnet: "MAGNET", torrentFromContent: "Torrenta sortu", torrentFromContentMissing: "Fitxategia oraindik ez dago nodo honetan eskuragarri. Saiatu geroago.", torrentDownloads: "Deskargak", @@ -4518,9 +4518,6 @@ module.exports = { inhabitantsPendingAccept: "Onartu", inhabitantsPendingReject: "Ukatu", tribeSectionGovernance: "GOBERNANTZA", - tribePadInviteRequired: "Ez duzu pada atzipena. Eskatu gonbidapena.", - tribeChatInviteRequired: "Ez duzu txata atzipena. Eskatu gonbidapena.", - tribeGovernanceDesc: "Tribu honen barne gobernantza.", tribeGovernanceNoGov: "Ez dago gobernu aktiborik", tribeGovernanceNoGovDesc: "Tribu honek ez du oraindik gobernurik aukeratu.", tribeGovCardTitle: "Egungo Gobernua", @@ -4577,7 +4574,6 @@ module.exports = { logsFilterMonth: "AZKEN HILABETEA", logsFilterWeek: "AZKEN ASTEA", logsFilterToday: "GAUR", - logsComments: "Iruzkinak", logsAuthorEntries: "Sarrerak", logsCreate: "Sarrera sortu", logsExport: "Egunkaria esportatu", @@ -4736,6 +4732,8 @@ module.exports = { larpWillTestTitle: "WILL proba", settingsLanDesc: "Aldian behin iragarri pareko hau sare lokal bereko beste Oasis instantziei.", settingsLanEnable: "Gaitu LAN igorpena", + settingsConnectionsTitle: "Konexioak", + settingsConnectionsDesc: "Nodo honek aldi berean konektatuta mantentzen dituen peer kopurua.", settingsLanTitle: "LAN igorpena", settingsReplicationTitle: "Erreplikazioa", settingsReplicationDesc: "Konfiguratu zenbat jauzi jarraitu behar dituen zure kideak zure jariotik abiatuz edukia erreplikatzean.", diff --git a/src/client/assets/translations/oasis_fr.js b/src/client/assets/translations/oasis_fr.js index 00cfd95..8fa9d37 100644 --- a/src/client/assets/translations/oasis_fr.js +++ b/src/client/assets/translations/oasis_fr.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† Mon rรฉseau", graphosShowing: "Affichage", graphosYou: "Toi", - graphosTotalNodes: "Nล“uds totaux", + graphosTotalNodes: "Totaux", + onlinePlural: "En ligne", mentions: "Mentions", mentionsDescription: [ strong("Chaque message qui vous @mentionne"), @@ -412,6 +413,7 @@ module.exports = { menuMedia: "Bibliothรจque", menuTools: "Outils", comment: "Commenter", + typeComment: "Commentaire", subtopic: "Sous-sujet", json: "JSON", unfollow: "Ne plus soutenir", @@ -441,9 +443,6 @@ module.exports = { publishCustomDescription: [ "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publiรฉ quelque chose, cela ne peut pas รชtre modifiรฉ ni supprimรฉ. Rรฉflรฉchissez bien !", ], - commentWarning: [ - "RAPPELEZ-VOUS : En raison de la technologie blockchain, une fois que vous avez publiรฉ quelque chose, cela ne peut pas รชtre modifiรฉ ni supprimรฉ. Rรฉflรฉchissez bien !", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -718,10 +717,6 @@ module.exports = { votesQuestionLabel:"Question", voteStatusOpen:"OUVERT", voteStatusClosed:"FERMร‰", - commentDescription: ({ parentUrl }) => [ - " a commentรฉ dans ", - a({ href: parentUrl }, " fil"), - ], commentTitle: ({ authorName }) => [`A commentรฉ sur le post de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " a crรฉรฉ un sous-sujet depuis ", @@ -856,6 +851,8 @@ module.exports = { bankRulesEligibility5: "Le PUB paie ร  son prochain cycle et publie un transfert RBU que vous pouvez confirmer.", bankRulesEligibility6: "Sans adresse, avec un feed trop jeune, un mois refusรฉ ou un PUB sans fonds, la rรฉclamation est ignorรฉe et journalisรฉe sur le PUB.", copyLinkTitle: "Copier le lien", + cardMenuTitle: "Plus d'actions", + shareContent: "Partager", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Refuser le RBU", @@ -889,7 +886,7 @@ module.exports = { walletNotConfigured: "Votre portefeuille ECOin n'est pas encore configurรฉ.", walletSettingsDescription: "Intรฉgrer Oasis avec votre portefeuille ECOin.", walletSettingsMobileDescription: "Associez Oasis ร  lโ€™application ECOin Wallet de cet appareil.", - walletDownloadEcoin: "Tรฉlรฉcharger ECOin !", + walletDownloadEcoin: "Tรฉlรฉcharger ECOin", walletRpcUrl: "URL du RPC", walletStatusMessages: { invalid_amount: "Montant invalide", @@ -942,6 +939,7 @@ module.exports = { bookmarkUpdateButton: "Mettre ร  jour", bookmarkAddFavoriteButton: "Ajouter aux favoris", bookmarkRemoveFavoriteButton: "Retirer des favoris", + bookmarkTitleLabel: "Titre", bookmarkUrlLabel: "Lien", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Description", @@ -1086,9 +1084,9 @@ module.exports = { allCVs: "CV", discoverPeople: "Dรฉcouvrez des habitants dans votre rรฉseau.", searchInhabitantsPlaceholder: "Rechercher dans Habitants...", - filterLocation: "FILTRER les habitants PAR LOCALISATION โ€ฆ", - filterLanguage: "FILTRER les habitants PAR LANGUE โ€ฆ", - filterSkills: "FILTRER les habitants PAR COMPร‰TENCES โ€ฆ", + filterLocation: "Rechercher par localisationโ€ฆ", + filterLanguage: "Rechercher par langueโ€ฆ", + filterSkills: "Rechercher par compรฉtencesโ€ฆ", cvSkillsCount: "Compรฉtences", applyFilters: "Appliquer les filtres", locationLabel: "Localisation", @@ -1104,6 +1102,7 @@ module.exports = { topactivitySectionTitle: "Top activitรฉ", "TOP INACTIVITYButton": "PLUS INACTIFS", topinactivitySectionTitle: "Habitants les moins actifs", + contactsSectionTitle: "Contacts", blockedSectionTitle: "Bloquรฉs", gallerySectionTitle: "GALERIE", topSectionTitle: "TOP", @@ -1557,9 +1556,7 @@ module.exports = { voteUnknownDate: "Date inconnue", voteDetailSectionTitle: 'Dรฉtails du vote', voteCommentsLabel: 'Commentaires', - voteCommentsForumButton: 'Ouvrir la discussion', - voteCommentsSectionTitle: 'Discussion ouverte', - voteNoCommentsYet: 'Il nโ€™y a pas encore de commentaires. Soyez le premier ร  rรฉpondre.', + voteNoCommentsYet: 'Il nโ€™y a pas encore de commentaires.', commentsGoToLast: "Aller au dernier commentaire", voteNewCommentPlaceholder: 'ร‰crivez votre commentaire iciโ€ฆ', voteNewCommentButton: 'Publier le commentaire', @@ -1686,7 +1683,6 @@ module.exports = { pollVoteButton: "Voter", pollChangeVote: "Changer de Vote", pollVoters: "Votants", - pollComments: "Commentaires", pollStatusLabel: "Statut", pollStatusOpen: "OUVERT", pollStatusClosed: "FERMร‰", @@ -1708,9 +1704,22 @@ module.exports = { blogAllowComments: "Autoriser les commentaires", blogCommentsClosed: "L'auteur a fermรฉ les commentaires de ce blog.", blogNoItems: "Aucun blog trouvรฉ.", - viewBlog: "Voir le Blog", forumTitle: "Forums", forumCategoryLabel: "Catรฉgorie", + forumCatGENERAL: "Gร‰Nร‰RAL", + forumCatPOLITICS: "POLITIQUE", + forumCatTECH: "TECHNOLOGIE", + forumCatSCIENCE: "SCIENCE", + forumCatMUSIC: "MUSIQUE", + forumCatART: "ART", + forumCatGAMING: "JEUX", + forumCatBOOKS: "LIVRES", + forumCatFILMS: "FILMS", + forumCatPHILOSOPHY: "PHILOSOPHIE", + forumCatSOCIETY: "SOCIร‰Tร‰", + forumCatPRIVACY: "VIE PRIVร‰E", + forumCatCYBERWARFARE: "CYBERGUERRE", + forumCatSURVIVALISM: "SURVIVALISME", forumTitleLabel: "Titre", forumTitlePlaceholder: "Titre du forum...", forumCreateButton: "Crรฉer un forum", @@ -1769,8 +1778,6 @@ module.exports = { feedTitle: "Fil", feedDetailTitle: "Fil", feedOpenDiscussion: "Ouvrir la discussion", - feedPostComment: "Publier un commentaire", - noComments: "Pas encore de commentaires", createFeedTitle: "Crรฉer un fil", createFeedButton: "Envoyer le fil !", feedPlaceholder: "Que se passe-t-il ? (maximum 280 caractรจres)", @@ -1981,36 +1988,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Rapports en rรฉvision", reportsResolvedSectionTitle: "Rapports rรฉsolus", reportsInvalidSectionTitle: "Rapports invalides", - reportsBugTemplateTitle: 'Dรฉtails de reproduction (Bugs)', - reportsFeatureTemplateTitle: 'Dรฉtails (Fonctionnalitรฉs)', - reportsAbuseTemplateTitle: 'Dรฉtails (Abus)', - reportsContentTemplateTitle: 'Dรฉtails (Problรจmes de Contenu)', - reportsStepsToReproduceLabel: 'ร‰tapes pour reproduire', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Rรฉsultat attendu', - reportsExpectedBehaviorPlaceholder: 'Que devrait-il se passer ?', - reportsActualBehaviorLabel: 'Rรฉsultat obtenu', - reportsEnvironmentLabel: 'Environnement', - reportsEnvironmentPlaceholder: 'Version, appareil, OS, navigateur, paramรจtres, logs, etc.', - reportsReproduceRateLabel: 'Frรฉquence', - reportsReproduceRateAlways: 'Toujours', - reportsReproduceRateOften: 'Souvent', - reportsReproduceRateSometimes: 'Parfois', - reportsReproduceRateRarely: 'Rarement', - reportsReproduceRateUnable: 'Impossible ร  reproduire', - reportsReproduceRateUnknown: 'Non prรฉcisรฉ', - reportsProblemStatementLabel: 'Problรจme / besoin', - reportsProblemStatementPlaceholder: 'Quel problรจme cette fonctionnalitรฉ rรฉsout-elle ?', - reportsAcceptanceCriteriaPlaceholder: '- ร‰tant donnรฉ...\n- Quand...\n- Alors...', - reportsReportedUserLabel: 'Utilisateur / entitรฉ signalรฉ(e)', - reportsReportedUserPlaceholder: '@utilisateur, feed, ID, lien, etc.', - reportsEvidenceLinksLabel: 'Preuves / liens', - reportsContentLocationLabel: 'Oรน se trouve le contenu', - reportsContentLocationPlaceholder: 'Lien, ID, canal, fil, auteur, etc.', - reportsWhyInappropriateLabel: "Pourquoi est-ce inappropriรฉ ?", - reportsWhyInappropriatePlaceholder: 'Expliquez la raison et lโ€™impact.', - reportsRequestedActionLabel: 'Action demandรฉe', - reportsRequestedActionPlaceholder: 'Supprimer, masquer, รฉtiqueter, avertir, etc.', tribesTitle: "Tribus", tribeFilterAll: "TOUTES", tribeFilterRecent: "Rร‰CENTES", @@ -2254,6 +2231,7 @@ module.exports = { voteWellResearched: "Bien recherchรฉ", voteNeedsSources: "Besoin de sources", voteWrong: "Faux", + voteOutdated: "Obsolรจte", voteLowQuality: "Faible qualitรฉ", voteLove: "Amour", voteClear: "Clair", @@ -2499,7 +2477,6 @@ module.exports = { shopNoProducts: "Pas encore de produits.", shopOutOfStock: "ร‰puisรฉ", shopBuy: "Acheter", - shopBuyEncryptedNote: "Vos coordonnรฉes de livraison sont chiffrรฉes et visibles uniquement par le propriรฉtaire de la boutique.", shopBuyDeliveryAddress: "Adresse de livraison", shopBuyContact: "Contact", shopBuyContactPlaceholder: "e-mail, tรฉlรฉphone, etc.", @@ -3015,6 +2992,16 @@ module.exports = { industryDescriptionLabel: "Description", industryDescriptionPlaceholder: "Que produit cette usine ?", industrySector: "Secteur", + industrySector_software: "Logiciel", + industrySector_hardware: "Matรฉriel", + industrySector_agriculture: "Agriculture", + industrySector_textile: "Textile", + industrySector_energy: "ร‰nergie", + industrySector_food: "Alimentation", + industrySector_construction: "Construction", + industrySector_media: "Mรฉdias", + industrySector_services: "Services", + industrySector_other: "Autre", industryMembershipPolicy: "Politique d'adhรฉsion", industryQuorum: "Quorum", industryMajority: "Majoritรฉ", @@ -3216,6 +3203,18 @@ module.exports = { modulesFavoritesDescription: "Module pour gรฉrer votre contenu favori.", fileTooLargeTitle: "Fichier trop volumineux", fileTooLargeMessage: "Le fichier dรฉpasse la taille maximale autorisรฉe (50 Mo). Veuillez sรฉlectionner un fichier plus petit.", + fileWrongType: "Ce fichier n'est pas du bon type pour cette section.", + mediaLockedAfterOpinions: "Il a dรฉjร  des avis : tu peux modifier le reste, mais pas ce qui a รฉtรฉ รฉvaluรฉ.", + pdfPrevPage: "Page prรฉcรฉdente", + pdfNextPage: "Page suivante", + pdfPage: "Page", + pdfZoomIn: "Zoom avant", + pdfZoomOut: "Zoom arriรจre", + pdfFullscreen: "Plein รฉcran", + pdfClose: "Fermer", + pdfRotate: "Pivoter", + contentNotAvailableYet: "Ce contenu nโ€™est pas encore disponible.", + pdfRetry: "Rรฉessayer", goBack: "Retour", errorDismiss: "OK", errorPageTitle: "Une erreur est survenue", @@ -3571,6 +3570,8 @@ module.exports = { podcastTagsPlaceholder: "Saisissez des tags sรฉparรฉs par des virgules", podcastFilterAll: "TOUS", podcastFilterMine: "MIENS", + podcastFilterFavorites: "FAVORIS", + podcastEpisodeNumberLabel: "Numรฉro d'รฉpisode", podcastFilterRecent: "Rร‰CENTS", podcastFilterTop: "TOP", podcastFilterViewers: "AUDITEURS", @@ -3789,7 +3790,6 @@ module.exports = { wikiBackToPage: "Retour ร  la page", wikiLinkedChip: "Liรฉe", wikiRestoredSummary: "Version restaurรฉe", - wikiExportPdf: "Gรฉnรฉrer la page", wikiPreview: "Aperรงu", tribeSectionWiki: "WIKI", tribeWikiCreate: "Crรฉer un wiki", @@ -3857,6 +3857,9 @@ module.exports = { roomRecStop: "Arrรชter l'enregistrement", roomStreamFile: "Diffuser", roomStreamStop: "Arrรชter", + roomScreen: "ร‰cran", + roomScreenSharing: "Partage d'รฉcran", + roomScreenWarning: "Tout le monde dans la salle verra votre รฉcran entier jusqu'ร  ce que vous arrรชtiez.", roomStreaming: "En diffusion", liveFullscreen: "Plein รฉcran", roomStreamPickTitle: "Choisir une vidรฉo", @@ -3914,6 +3917,7 @@ module.exports = { inviteAlreadyMember: "Vous en faites dรฉjร  partie. Vous pouvez l'ouvrir depuis son module.", actionFailed: "Cela n'a pas pu รชtre fait. Rรฉessayez.", publishTooShort: "Votre publication est trop courte.", + publishEmpty: "ร‰crivez quelque chose ou joignez un fichier.", walletSendFailed: "Le paiement n'a pas pu รชtre envoyรฉ : {reason}", pixeliaCoordsError: "Ces coordonnรฉes sont en dehors de la grille.", courtsErrTitle: "Un titre est requis.", @@ -4425,6 +4429,9 @@ module.exports = { torrentTagsLabel: "ร‰tiquettes", torrentTagsPlaceholder: "Saisissez des tags sรฉparรฉs par des virgules", torrentSizeLabel: "Taille", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Fichiers", + torrentMagnet: "MAGNET", torrentFromContent: "Gรฉnรฉrer un torrent", torrentFromContentMissing: "Le fichier n'est pas encore disponible sur ce nล“ud. Rรฉessayez plus tard.", torrentDownloads: "Tรฉlรฉchargements", @@ -4510,9 +4517,6 @@ module.exports = { inhabitantsPendingAccept: "Accepter", inhabitantsPendingReject: "Refuser", tribeSectionGovernance: "GOUVERNANCE", - tribePadInviteRequired: "Vous n'avez pas accรจs au pad. Demandez une invitation.", - tribeChatInviteRequired: "Vous n'avez pas accรจs au chat. Demandez une invitation.", - tribeGovernanceDesc: "Gouvernance interne de cette tribu.", tribeGovernanceNoGov: "Pas de gouvernement actif", tribeGovernanceNoGovDesc: "Cette tribu n'a pas encore de gouvernement.", tribeGovCardTitle: "Gouvernement Actuel", @@ -4569,7 +4573,6 @@ module.exports = { logsFilterMonth: "MOIS", logsFilterWeek: "SEMAINE", logsFilterToday: "AUJOURD'HUI", - logsComments: "Commentaires", logsAuthorEntries: "Entrรฉes", logsCreate: "Crรฉer une entrรฉe", logsExport: "Exporter le journal", @@ -4632,13 +4635,6 @@ module.exports = { padAttributionView: "Par autrice", padVersionView: "Voir", parliamentRevocationPublish: "Publier la rรฉvocation", - reportsAcceptanceCriteriaLabel: "Critรจres d'acceptation", - reportsActualBehaviorPlaceholder: "Que se passe-t-il rรฉellement ?", - reportsEvidenceLinksPlaceholder: "Collez des liens, des identifiants de messages, des captures d'รฉcran (le cas รฉchรฉant), etc.", - reportsUserStoryLabel: "Rรฉcit d'habitant", - reportsUserStoryPlaceholder: "En tant que , je veux , afin de .", - reportsWhatHappenedLabel: "Que s'est-il passรฉ ?", - reportsWhatHappenedPlaceholder: "Dรฉcrivez l'incident avec le contexte et des dates approximatives.", statsChat: "Chats", statsChatMessage: "Messages de chat", statsGameScore: "Jeux", @@ -4656,6 +4652,8 @@ module.exports = { larpWillTestTitle: "Test WILL", settingsLanDesc: "Annoncer pรฉriodiquement ce pair aux autres instances d'Oasis sur le mรชme rรฉseau local.", settingsLanEnable: "Activer la diffusion LAN", + settingsConnectionsTitle: "Connexions", + settingsConnectionsDesc: "Nombre de pairs que ce nล“ud garde connectรฉs en mรชme temps.", settingsLanTitle: "Diffusion LAN", settingsReplicationTitle: "Rรฉplication", settingsReplicationDesc: "Configure le nombre de sauts que ton pair suit ร  partir de ton propre flux lors de la rรฉplication du contenu.", diff --git a/src/client/assets/translations/oasis_gl.js b/src/client/assets/translations/oasis_gl.js index 1b55db8..7954031 100644 --- a/src/client/assets/translations/oasis_gl.js +++ b/src/client/assets/translations/oasis_gl.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† A miรฑa rede", graphosShowing: "Amosando", graphosYou: "Ti", - graphosTotalNodes: "Nodos totais", + graphosTotalNodes: "Totais", + onlinePlural: "Conectados", mentions: "Menciรณns", mentionsDescription: [ strong("Todas as mensaxes que te @mencionan"), @@ -412,6 +413,7 @@ module.exports = { menuMedia: "Biblioteca", menuTools: "Ferramentas", comment: "Comentar", + typeComment: "Comentario", subtopic: "Subtema", json: "JSON", unfollow: "Deixar de apoiar", @@ -441,9 +443,6 @@ module.exports = { publishCustomDescription: [ "LEMBRA: Por mor da tecnoloxรญa blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pรฉnsao ben!", ], - commentWarning: [ - "LEMBRA: Por mor da tecnoloxรญa blockchain, unha vez que publicas algo, non se pode editar nin borrar. Pรฉnsao ben!", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -718,10 +717,6 @@ module.exports = { votesQuestionLabel:"Pregunta", voteStatusOpen:"ABERTO", voteStatusClosed:"PECHADO", - commentDescription: ({ parentUrl }) => [ - " comentou en ", - a({ href: parentUrl }, " fรญo"), - ], commentTitle: ({ authorName }) => [`Comentou no post de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " creou un subtema desde ", @@ -856,6 +851,8 @@ module.exports = { bankRulesEligibility5: "O PUB paga no seu seguinte ciclo e publica un transfer RBU que podes confirmar.", bankRulesEligibility6: "Se falta o teu enderezo, o feed รฉ demasiado novo, rexeitaches o mes ou o PUB non ten fondos, a reclamaciรณn sรกltase e queda rexistrada no PUB.", copyLinkTitle: "Copiar a ligazรณn", + cardMenuTitle: "Mรกis acciรณns", + shareContent: "Compartir", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Rexeitar RBU", @@ -889,7 +886,7 @@ module.exports = { walletNotConfigured: "A tรบa carteira de ECOin aรญnda non estรก configurada.", walletSettingsDescription: "Integrar Oasis coa tรบa carteira de ECOin.", walletSettingsMobileDescription: "Vincula Oasis coa app ECOin Wallet deste dispositivo.", - walletDownloadEcoin: "Descargar ECOin!", + walletDownloadEcoin: "Descargar ECOin", walletRpcUrl: "URL do RPC", walletStatusMessages: { invalid_amount: "Cantidade non vรกlida", @@ -942,6 +939,7 @@ module.exports = { bookmarkUpdateButton: "Actualizar", bookmarkAddFavoriteButton: "Engadir a favoritos", bookmarkRemoveFavoriteButton: "Quitar de favoritos", + bookmarkTitleLabel: "Tรญtulo", bookmarkUrlLabel: "Ligazรณn", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descriciรณn", @@ -1086,9 +1084,9 @@ module.exports = { allCVs: "CV", discoverPeople: "Descobre habitantes na tรบa rede.", searchInhabitantsPlaceholder: "Buscar en Habitantes...", - filterLocation: "FILTRAR habitantes POR LOCALIZACIร“N โ€ฆ", - filterLanguage: "FILTRAR habitantes POR IDIOMA โ€ฆ", - filterSkills: "FILTRAR habitantes POR HABILIDADES โ€ฆ", + filterLocation: "Buscar por localizaciรณnโ€ฆ", + filterLanguage: "Buscar por idiomaโ€ฆ", + filterSkills: "Buscar por habilidadesโ€ฆ", cvSkillsCount: "Habilidades", applyFilters: "Aplicar os filtros", locationLabel: "Localizaciรณn", @@ -1104,6 +1102,7 @@ module.exports = { topactivitySectionTitle: "Mรกis actividade", "TOP INACTIVITYButton": "MรIS INACTIVOS", topinactivitySectionTitle: "Habitantes menos activos", + contactsSectionTitle: "Contactos", blockedSectionTitle: "Bloqueados", gallerySectionTitle: "GALERรA", topSectionTitle: "TOP", @@ -1558,9 +1557,7 @@ module.exports = { voteUnknownDate: "Data descoรฑecida", voteDetailSectionTitle: "Detalles da votaciรณn", voteCommentsLabel: "Comentarios", - voteCommentsForumButton: "Abrir a conversa", - voteCommentsSectionTitle: "Conversa aberta", - voteNoCommentsYet: "Aรญnda non hai comentarios. Sรฉ a primeira persoa en opinar.", + voteNoCommentsYet: "Aรญnda non hai comentarios.", commentsGoToLast: "Ir ao รบltimo comentario", voteNewCommentPlaceholder: "Escribe aquรญ o teu comentarioโ€ฆ", voteNewCommentButton: "Publicar o comentario", @@ -1687,7 +1684,6 @@ module.exports = { pollVoteButton: "Votar", pollChangeVote: "Cambiar o voto", pollVoters: "Votantes", - pollComments: "Comentarios", pollStatusLabel: "Estado", pollStatusOpen: "ABERTA", pollStatusClosed: "PECHADA", @@ -1709,9 +1705,22 @@ module.exports = { blogAllowComments: "Permitir comentarios", blogCommentsClosed: "O autor pechou os comentarios deste blog.", blogNoItems: "Non se atoparon blogs.", - viewBlog: "Ver o blog", forumTitle: "Foros", forumCategoryLabel: "Categorรญa", + forumCatGENERAL: "XERAL", + forumCatPOLITICS: "POLรTICA", + forumCatTECH: "TECNOLOXรA", + forumCatSCIENCE: "CIENCIA", + forumCatMUSIC: "MรšSICA", + forumCatART: "ARTE", + forumCatGAMING: "XOGOS", + forumCatBOOKS: "LIBROS", + forumCatFILMS: "PELรCULAS", + forumCatPHILOSOPHY: "FILOSOFรA", + forumCatSOCIETY: "SOCIEDADE", + forumCatPRIVACY: "PRIVACIDADE", + forumCatCYBERWARFARE: "CIBERGUERRA", + forumCatSURVIVALISM: "SUPERVIVENCIA", forumTitleLabel: "Tรญtulo", forumTitlePlaceholder: "Tรญtulo do foro...", forumCreateButton: "Crear un foro", @@ -1770,8 +1779,6 @@ module.exports = { feedTitle: "Feed", feedDetailTitle: "Feed", feedOpenDiscussion: "Abrir o debate", - feedPostComment: "Publicar o comentario", - noComments: "Aรญnda non hai comentarios", createFeedTitle: "Crear un feed", createFeedButton: "Enviar o feed!", feedPlaceholder: "Que estรก a pasar? (mรกximo 280 caracteres)", @@ -1982,42 +1989,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Informes en revisiรณn", reportsResolvedSectionTitle: "Informes resoltos", reportsInvalidSectionTitle: "Informes non vรกlidos", - reportsBugTemplateTitle: "Datos para reproducir (erros)", - reportsFeatureTemplateTitle: "Detalles (funciรณns)", - reportsAbuseTemplateTitle: "Detalles (abuso)", - reportsContentTemplateTitle: "Detalles (problemas de contido)", - reportsStepsToReproduceLabel: "Pasos para reproducilo", - reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...", - reportsExpectedBehaviorLabel: "Resultado esperado", - reportsExpectedBehaviorPlaceholder: "Que deberรญa pasar?", - reportsActualBehaviorLabel: "Resultado real", - reportsActualBehaviorPlaceholder: "Que pasa realmente?", - reportsEnvironmentLabel: "Contorno", - reportsEnvironmentPlaceholder: "Versiรณn, dispositivo, SO, navegador, configuraciรณn, rexistros, etc.", - reportsReproduceRateLabel: "Frecuencia", - reportsReproduceRateAlways: "Sempre", - reportsReproduceRateOften: "A miรบdo", - reportsReproduceRateSometimes: "รs veces", - reportsReproduceRateRarely: "Raramente", - reportsReproduceRateUnable: "Non se pode reproducir", - reportsReproduceRateUnknown: "Non especificado", - reportsProblemStatementLabel: "Problema / necesidade", - reportsProblemStatementPlaceholder: "Que problema resolve esta funciรณn?", - reportsUserStoryLabel: "Historia de habitante", - reportsUserStoryPlaceholder: "Como , quero , para .", - reportsAcceptanceCriteriaLabel: "Criterios de aceptaciรณn", - reportsAcceptanceCriteriaPlaceholder: "- Dado...\n- Cando...\n- Entรณn...", - reportsWhatHappenedLabel: "Que ocorreu?", - reportsWhatHappenedPlaceholder: "Describe o incidente con contexto e datas aproximadas.", - reportsReportedUserLabel: "Habitante / entidade denunciada", - reportsReportedUserPlaceholder: "@habitante, feed, ID, ligazรณn, etc.", - reportsEvidenceLinksLabel: "Probas / ligazรณns", - reportsEvidenceLinksPlaceholder: "Pega ligazรณns, IDs de mensaxes, capturas (se procede), etc.", - reportsContentLocationLabel: "Onde estรก o contido", - reportsContentLocationPlaceholder: "Ligazรณn, ID, canle, fรญo, autor, etc.", - reportsWhyInappropriatePlaceholder: "Explica o motivo e o impacto.", - reportsRequestedActionLabel: "Acciรณn solicitada", - reportsRequestedActionPlaceholder: "Eliminar, agochar, marcar, advertir, etc.", tribesTitle: "Tribos", tribeFilterAll: "TODAS", tribeFilterRecent: "RECENTES", @@ -2261,6 +2232,7 @@ module.exports = { voteWellResearched: "Ben investigado", voteNeedsSources: "Necesita fontes", voteWrong: "Incorrecto", + voteOutdated: "Desactualizado", voteLowQuality: "Baixa calidade", voteLove: "Amor", voteClear: "Claro", @@ -2505,7 +2477,6 @@ module.exports = { shopNoProducts: "Aรญnda non hai produtos.", shopOutOfStock: "Esgotado", shopBuy: "Comprar", - shopBuyEncryptedNote: "Os teus datos de entrega envรญanse cifrados; sรณ o dono da tenda pode velos.", shopBuyDeliveryAddress: "Enderezo de entrega", shopBuyContact: "Contacto", shopBuyContactPlaceholder: "correo, telรฉfono, etc.", @@ -3022,6 +2993,16 @@ module.exports = { industryDescriptionLabel: "Descriciรณn", industryDescriptionPlaceholder: "Que produce esta fรกbrica?", industrySector: "Sector", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Agricultura", + industrySector_textile: "Tรฉxtil", + industrySector_energy: "Enerxรญa", + industrySector_food: "Alimentaciรณn", + industrySector_construction: "Construciรณn", + industrySector_media: "Medios", + industrySector_services: "Servizos", + industrySector_other: "Outros", industryMembershipPolicy: "Polรญtica de pertenza", industryQuorum: "Quรณrum", industryMajority: "Maiorรญa", @@ -3223,6 +3204,18 @@ module.exports = { modulesFavoritesDescription: "Mรณdulo para xestionar o teu contido favorito.", fileTooLargeTitle: "Ficheiro demasiado grande", fileTooLargeMessage: "O ficheiro supera o tamaรฑo mรกximo permitido (75 MB). Por favor, selecciona un ficheiro mรกis pequeno.", + fileWrongType: "Este ficheiro non รฉ do tipo axeitado para esta secciรณn.", + mediaLockedAfterOpinions: "Xa ten opiniรณns: podes editar o resto, pero non cambiar o que se valorou.", + pdfPrevPage: "Pรกxina anterior", + pdfNextPage: "Pรกxina seguinte", + pdfPage: "Pรกxina", + pdfZoomIn: "Achegar", + pdfZoomOut: "Afastar", + pdfFullscreen: "Pantalla completa", + pdfClose: "Pechar", + pdfRotate: "Xirar", + contentNotAvailableYet: "Este contido aรญnda non estรก dispoรฑible.", + pdfRetry: "Tentar de novo", goBack: "Volver", errorDismiss: "De acordo", errorPageTitle: "Produciuse un erro", @@ -3267,7 +3260,6 @@ module.exports = { invitesFollow: "Seguir", invitesUnfollowedInvites: "Non federadas", invitesNoUnfollowed: "Non hai pubs non federados.", - reportsWhyInappropriateLabel: "Por que รฉ inadecuado?", blockchainContentDeleted: "Este contido foi eliminado", visitContent: "Visitar o contido", favoriteAdd: "Fixar en favoritos", @@ -3579,6 +3571,8 @@ module.exports = { podcastTagsPlaceholder: "Introduce etiquetas separadas por comas", podcastFilterAll: "TODOS", podcastFilterMine: "MEUS", + podcastFilterFavorites: "FAVORITOS", + podcastEpisodeNumberLabel: "Nรบmero de episodio", podcastFilterRecent: "RECENTES", podcastFilterTop: "TOP", podcastFilterViewers: "OรNTES", @@ -3797,7 +3791,6 @@ module.exports = { wikiBackToPage: "Volver รก pรกxina", wikiLinkedChip: "Ligada", wikiRestoredSummary: "Versiรณn restaurada", - wikiExportPdf: "Xerar a pรกxina", wikiPreview: "Vista previa", tribeSectionWiki: "WIKI", tribeWikiCreate: "Crear unha wiki", @@ -3865,6 +3858,9 @@ module.exports = { roomRecStop: "Parar a gravaciรณn", roomStreamFile: "Emitir", roomStreamStop: "Parar", + roomScreen: "Pantalla", + roomScreenSharing: "Compartindo pantalla", + roomScreenWarning: "Todos na sala verรกn a tรบa pantalla completa ata que pares.", roomStreaming: "Emitindo", liveFullscreen: "Pantalla completa", roomStreamPickTitle: "Escolle un vรญdeo", @@ -3922,6 +3918,7 @@ module.exports = { inviteAlreadyMember: "Xa formas parte. Podes abrilo desde o seu mรณdulo.", actionFailed: "Non se puido facer. Tรฉntao de novo.", publishTooShort: "A tรบa publicaciรณn รฉ demasiado curta.", + publishEmpty: "Escribe algo ou achega un ficheiro.", walletSendFailed: "Non se puido enviar o pagamento: {reason}", pixeliaCoordsError: "Esas coordenadas estรกn fรณra da grella.", courtsErrTitle: "Cรณmpre un tรญtulo.", @@ -4434,6 +4431,9 @@ module.exports = { torrentTagsLabel: "Etiquetas", torrentTagsPlaceholder: "Introduce etiquetas separadas por comas", torrentSizeLabel: "Tamaรฑo", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Ficheiros", + torrentMagnet: "MAGNET", torrentFromContent: "Xerar torrent", torrentFromContentMissing: "O ficheiro aรญnda non estรก dispoรฑible neste nodo. Tรฉntao mรกis tarde.", torrentDownloads: "Descargas", @@ -4519,9 +4519,6 @@ module.exports = { inhabitantsPendingAccept: "Aceptar", inhabitantsPendingReject: "Rexeitar", tribeSectionGovernance: "GOBERNO", - tribePadInviteRequired: "Non tes acceso ao pad. Pide un convite para acceder ao contido.", - tribeChatInviteRequired: "Non tes acceso ao chat. Pide un convite para acceder ao contido.", - tribeGovernanceDesc: "Goberno interno desta tribo.", tribeGovernanceNoGov: "Sen goberno activo", tribeGovernanceNoGovDesc: "Esta tribo aรญnda non ten goberno. Propรณn candidaturas para iniciar o proceso.", tribeGovCardTitle: "Goberno actual", @@ -4578,7 +4575,6 @@ module.exports = { logsFilterMonth: "รšLTIMO MES", logsFilterWeek: "รšLTIMA SEMANA", logsFilterToday: "HOXE", - logsComments: "Comentarios", logsAuthorEntries: "Entradas", logsCreate: "Crear unha entrada", logsExport: "Exportar a bitรกcora", @@ -4657,6 +4653,8 @@ module.exports = { larpWillTestTitle: "Test WILL", settingsLanDesc: "Anuncia periodicamente este nodo a outras instancias de Oasis na mesma rede local.", settingsLanEnable: "Activar a difusiรณn LAN", + settingsConnectionsTitle: "Conexiรณns", + settingsConnectionsDesc: "Cantos peers mantรฉn conectados este nodo รก vez.", settingsLanTitle: "Difusiรณn LAN", settingsReplicationTitle: "Replicaciรณn", settingsReplicationDesc: "Configura o nรบmero de saltos que o teu nodo segue desde o teu propio feed ao replicar contido.", diff --git a/src/client/assets/translations/oasis_hi.js b/src/client/assets/translations/oasis_hi.js index 67dbe0e..49292a6 100644 --- a/src/client/assets/translations/oasis_hi.js +++ b/src/client/assets/translations/oasis_hi.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† เคฎเฅ‡เคฐเคพ เคจเฅ‡เคŸเคตเคฐเฅเค•", graphosShowing: "เคฆเคฟเค–เคพ เคฐเคนเฅ‡", graphosYou: "เค†เคช", - graphosTotalNodes: "เค•เฅเคฒ เคจเฅ‹เคกเฅเคธ", + graphosTotalNodes: "เค•เฅเคฒ", + onlinePlural: "เค‘เคจเคฒเคพเค‡เคจ", mentions: "เค‰เคฒเฅเคฒเฅ‡เค–", mentionsDescription: [ strong("เคนเคฐ เคธเค‚เคฆเฅ‡เคถ เคœเฅ‹ เค†เคชเค•เฅ‹ @เค‰เคฒเฅเคฒเฅ‡เค– เค•เคฐเคคเคพ เคนเฅˆ"), @@ -420,6 +421,7 @@ module.exports = { menuMedia: "เคชเฅเคธเฅเคคเค•เคพเคฒเคฏ", menuTools: "เค‰เคชเค•เคฐเคฃ", comment: "เคŸเคฟเคชเฅเคชเคฃเฅ€", + typeComment: "เคŸเคฟเคชเฅเคชเคฃเฅ€", subtopic: "เค‰เคชเคตเคฟเคทเคฏ", json: "JSON", unfollow: "เคธเคฎเคฐเฅเคฅเคจ เคตเคพเคชเคธ เคฒเฅ‡เค‚", @@ -449,9 +451,6 @@ module.exports = { publishCustomDescription: [ "เคฏเคพเคฆ เคฐเค–เฅ‡เค‚: เคฌเฅเคฒเฅ‰เค•เคšเฅ‡เคจ เคคเค•เคจเฅ€เค• เค•เฅ‡ เค•เคพเคฐเคฃ, เคเค• เคฌเคพเคฐ เคชเฅ‹เคธเฅเคŸ เคชเฅเคฐเค•เคพเคถเคฟเคค เคนเฅ‹เคจเฅ‡ เค•เฅ‡ เคฌเคพเคฆ เค‡เคธเฅ‡ เคธเค‚เคชเคพเคฆเคฟเคค เคฏเคพ เคนเคŸเคพเคฏเคพ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เคคเคพเฅค", ], - commentWarning: [ - "เคฏเคพเคฆ เคฐเค–เฅ‡เค‚: เคฌเฅเคฒเฅ‰เค•เคšเฅ‡เคจ เคคเค•เคจเฅ€เค• เค•เฅ‡ เค•เคพเคฐเคฃ, เคเค• เคฌเคพเคฐ เคชเฅ‹เคธเฅเคŸ เคชเฅเคฐเค•เคพเคถเคฟเคค เคนเฅ‹เคจเฅ‡ เค•เฅ‡ เคฌเคพเคฆ เค‡เคธเฅ‡ เคธเค‚เคชเคพเคฆเคฟเคค เคฏเคพ เคนเคŸเคพเคฏเคพ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เคคเคพเฅค", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -730,10 +729,6 @@ module.exports = { votesQuestionLabel:"เคชเฅเคฐเคถเฅเคจ", voteStatusOpen:"เค–เฅเคฒเคพ", voteStatusClosed:"เคฌเค‚เคฆ", - commentDescription: ({ parentUrl }) => [ - " เคจเฅ‡ เคŸเคฟเคชเฅเคชเคฃเฅ€ เค•เฅ€ ", - a({ href: parentUrl }, " เคฅเฅเคฐเฅ‡เคก เคชเคฐ"), - ], commentTitle: ({ authorName }) => [`@${authorName} เค•เฅ€ เคชเฅ‹เคธเฅเคŸ เคชเคฐ เคŸเคฟเคชเฅเคชเคฃเฅ€`], subtopicDescription: ({ parentUrl }) => [ " เคจเฅ‡ เคเค• เค‰เคชเคตเคฟเคทเคฏ เคฌเคจเคพเคฏเคพ ", @@ -868,6 +863,8 @@ module.exports = { bankRulesEligibility5: "PUB เค…เค—เคฒเฅ‡ เคšเค•เฅเคฐ เคฎเฅ‡เค‚ เคญเฅเค—เคคเคพเคจ เค•เคฐเคคเคพ เคนเฅˆ เค”เคฐ เคเค• UBI เคŸเฅเคฐเคพเค‚เคธเคซเคฐ เคชเฅเคฐเค•เคพเคถเคฟเคค เค•เคฐเคคเคพ เคนเฅˆ เคœเคฟเคธเฅ‡ เค†เคช เคชเฅเคทเฅเคŸเคฟ เค•เคฐ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค", bankRulesEligibility6: "เคฏเคฆเคฟ เคชเคคเคพ เคจเคนเฅ€เค‚ เคนเฅˆ, เคซเคผเฅ€เคก เคฌเคนเฅเคค เคจเคˆ เคนเฅˆ, เค†เคชเคจเฅ‡ เคฎเคพเคน เค…เคธเฅเคตเฅ€เค•เคพเคฐ เค•เคฟเคฏเคพ เคฏเคพ PUB เค•เฅ‡ เคชเคพเคธ เคงเคจ เคจเคนเฅ€เค‚ เคนเฅˆ, เคคเฅ‹ เคฆเคพเคตเคพ เค›เฅ‹เคกเคผ เคฆเคฟเคฏเคพ เคœเคพเคคเคพ เคนเฅˆ เค”เคฐ PUB เคชเคฐ เคฒเฅ‰เค— เค•เคฟเคฏเคพ เคœเคพเคคเคพ เคนเฅˆเฅค", copyLinkTitle: "เคฒเคฟเค‚เค• เค•เฅ‰เคชเฅ€ เค•เคฐเฅ‡เค‚", + cardMenuTitle: "เค”เคฐ เค•เคพเคฐเฅเคฐเคตเคพเค‡เคฏเคพเค", + shareContent: "เคธเคพเคเคพ เค•เคฐเฅ‡เค‚", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "UBI เค…เคธเฅเคตเฅ€เค•เคพเคฐ เค•เคฐเฅ‡เค‚", @@ -901,7 +898,7 @@ module.exports = { walletNotConfigured: "เค†เคชเค•เคพ ECOin เคตเฅ‰เคฒเฅ‡เคŸ เค…เคญเฅ€ เค•เฅ‰เคจเฅเคซเคผเคฟเค—เคฐ เคจเคนเฅ€เค‚ เคนเฅˆเฅค", walletSettingsDescription: "Oasis เค•เฅ‹ เค…เคชเคจเฅ‡ ECOin เคตเฅ‰เคฒเฅ‡เคŸ เค•เฅ‡ เคธเคพเคฅ เคเค•เฅ€เค•เฅƒเคค เค•เคฐเฅ‡เค‚เฅค", walletSettingsMobileDescription: "Oasis เค•เฅ‹ เค‡เคธ เคกเคฟเคตเคพเค‡เคธ เค•เฅ‡ ECOin Wallet เคเคช เคธเฅ‡ เคœเฅ‹เคกเคผเฅ‡เค‚เฅค", - walletDownloadEcoin: "ECOin เคกเคพเค‰เคจเคฒเฅ‹เคก เค•เคฐเฅ‡เค‚!", + walletDownloadEcoin: "ECOin เคกเคพเค‰เคจเคฒเฅ‹เคก เค•เคฐเฅ‡เค‚", walletRpcUrl: "RPC URL", walletStatusMessages: { invalid_amount: "เค…เคฎเคพเคจเฅเคฏ เคฐเคพเคถเคฟ", @@ -954,6 +951,7 @@ module.exports = { bookmarkUpdateButton: "เค…เคชเคกเฅ‡เคŸ", bookmarkAddFavoriteButton: "เคชเคธเค‚เคฆเฅ€เคฆเคพ เคœเฅ‹เคกเคผเฅ‡เค‚", bookmarkRemoveFavoriteButton: "เคชเคธเค‚เคฆเฅ€เคฆเคพ เคนเคŸเคพเคเค", + bookmarkTitleLabel: "เคถเฅ€เคฐเฅเคทเค•", bookmarkUrlLabel: "เคฒเคฟเค‚เค•", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "เคตเคฟเคตเคฐเคฃ", @@ -1098,9 +1096,9 @@ module.exports = { allCVs: "CVs", discoverPeople: "เค…เคชเคจเฅ‡ เคจเฅ‡เคŸเคตเคฐเฅเค• เคฎเฅ‡เค‚ เคจเคฟเคตเคพเคธเคฟเคฏเฅ‹เค‚ เค•เฅ‹ เค–เฅ‹เคœเฅ‡เค‚เฅค", searchInhabitantsPlaceholder: "เคจเคฟเคตเคพเคธเฅ€ เคฎเฅ‡เค‚ เค–เฅ‹เคœเฅ‡เค‚...", - filterLocation: "เคธเฅเคฅเคพเคจ เคธเฅ‡ เคจเคฟเคตเคพเคธเฅ€ เคซเคผเคฟเคฒเฅเคŸเคฐ เค•เคฐเฅ‡เค‚ โ€ฆ", - filterLanguage: "เคญเคพเคทเคพ เคธเฅ‡ เคจเคฟเคตเคพเคธเฅ€ เคซเคผเคฟเคฒเฅเคŸเคฐ เค•เคฐเฅ‡เค‚ โ€ฆ", - filterSkills: "เค•เฅŒเคถเคฒ เคธเฅ‡ เคจเคฟเคตเคพเคธเฅ€ เคซเคผเคฟเคฒเฅเคŸเคฐ เค•เคฐเฅ‡เค‚ โ€ฆ", + filterLocation: "เคธเฅเคฅเคพเคจ เคธเฅ‡ เค–เฅ‹เคœเฅ‡เค‚โ€ฆ", + filterLanguage: "เคญเคพเคทเคพ เคธเฅ‡ เค–เฅ‹เคœเฅ‡เค‚โ€ฆ", + filterSkills: "เค•เฅŒเคถเคฒ เคธเฅ‡ เค–เฅ‹เคœเฅ‡เค‚โ€ฆ", cvSkillsCount: "เค•เฅŒเคถเคฒ", applyFilters: "เคซเคผเคฟเคฒเฅเคŸเคฐ เคฒเคพเค—เฅ‚ เค•เคฐเฅ‡เค‚", locationLabel: "เคธเฅเคฅเคพเคจ", @@ -1116,6 +1114,7 @@ module.exports = { topactivitySectionTitle: "เคถเฅ€เคฐเฅเคท เค—เคคเคฟเคตเคฟเคงเคฟ", "TOP INACTIVITYButton": "เคธเคฌเคธเฅ‡ เคจเคฟเคทเฅเค•เฅเคฐเคฟเคฏ", topinactivitySectionTitle: "เคธเคฌเคธเฅ‡ เค•เคฎ เคธเค•เฅเคฐเคฟเคฏ เคจเคฟเคตเคพเคธเฅ€", + contactsSectionTitle: "เคธเค‚เคชเคฐเฅเค•", blockedSectionTitle: "เคฌเฅเคฒเฅ‰เค• เค•เคฟเค เค—เค", gallerySectionTitle: "เค—เฅˆเคฒเคฐเฅ€", topSectionTitle: "เคถเฅ€เคฐเฅเคท", @@ -1569,9 +1568,7 @@ module.exports = { voteUnknownDate: "เค…เคœเฅเคžเคพเคค เคฆเคฟเคจเคพเค‚เค•", voteDetailSectionTitle: 'เคฎเคค เคตเคฟเคตเคฐเคฃ', voteCommentsLabel: 'เคŸเคฟเคชเฅเคชเคฃเคฟเคฏเคพเค', - voteCommentsForumButton: 'เคšเคฐเฅเคšเคพ เค–เฅ‹เคฒเฅ‡เค‚', - voteCommentsSectionTitle: 'เคšเคฐเฅเคšเคพ เค–เฅ‹เคฒเฅ‡เค‚', - voteNoCommentsYet: 'เค…เคญเฅ€ เคคเค• เค•เฅ‹เคˆ เคŸเคฟเคชเฅเคชเคฃเฅ€ เคจเคนเฅ€เค‚ เคนเฅˆเฅค เคชเคนเคฒเฅ‡ เค‰เคคเฅเคคเคฐ เคฆเฅ‡เคจเฅ‡ เคตเคพเคฒเฅ‡ เคฌเคจเฅ‡เค‚เฅค', + voteNoCommentsYet: 'เค…เคญเฅ€ เคคเค• เค•เฅ‹เคˆ เคŸเคฟเคชเฅเคชเคฃเฅ€ เคจเคนเฅ€เค‚ เคนเฅˆเฅค', commentsGoToLast: "เค…เค‚เคคเคฟเคฎ เคŸเคฟเคชเฅเคชเคฃเฅ€ เคชเคฐ เคœเคพเคเค", voteNewCommentPlaceholder: 'เค…เคชเคจเฅ€ เคŸเคฟเคชเฅเคชเคฃเฅ€ เคฏเคนเคพเค เคฒเคฟเค–เฅ‡เค‚โ€ฆ', voteNewCommentButton: 'เคŸเคฟเคชเฅเคชเคฃเฅ€ เคชเฅ‹เคธเฅเคŸ เค•เคฐเฅ‡เค‚', @@ -1698,7 +1695,6 @@ module.exports = { pollVoteButton: "เคฎเคค เคฆเฅ‡เค‚", pollChangeVote: "เคตเฅ‹เคŸ เคฌเคฆเคฒเฅ‡เค‚", pollVoters: "เคฎเคคเคฆเคพเคคเคพ", - pollComments: "เคŸเคฟเคชเฅเคชเคฃเคฟเคฏเคพเค", pollStatusLabel: "เคธเฅเคฅเคฟเคคเคฟ", pollStatusOpen: "เค–เฅเคฒเคพ", pollStatusClosed: "เคฌเค‚เคฆ", @@ -1720,9 +1716,22 @@ module.exports = { blogAllowComments: "เคŸเคฟเคชเฅเคชเคฃเคฟเคฏเคพเค เค…เคจเฅเคฎเคคเคฟ เคฆเฅ‡เค‚", blogCommentsClosed: "เคฒเฅ‡เค–เค• เคจเฅ‡ เค‡เคธ เคฌเฅเคฒเฅ‰เค— เค•เฅ€ เคŸเคฟเคชเฅเคชเคฃเคฟเคฏเคพเค เคฌเค‚เคฆ เค•เคฐ เคฆเฅ€ เคนเฅˆเค‚เฅค", blogNoItems: "เค•เฅ‹เคˆ เคฌเฅเคฒเฅ‰เค— เคจเคนเฅ€เค‚ เคฎเคฟเคฒเคพเฅค", - viewBlog: "เคฌเฅเคฒเฅ‰เค— เคฆเฅ‡เค–เฅ‡เค‚", forumTitle: "เคฎเค‚เคš", forumCategoryLabel: "เคถเฅเคฐเฅ‡เคฃเฅ€", + forumCatGENERAL: "เคธเคพเคฎเคพเคจเฅเคฏ", + forumCatPOLITICS: "เคฐเคพเคœเคจเฅ€เคคเคฟ", + forumCatTECH: "เคคเค•เคจเฅ€เค•", + forumCatSCIENCE: "เคตเคฟเคœเฅเคžเคพเคจ", + forumCatMUSIC: "เคธเค‚เค—เฅ€เคค", + forumCatART: "เค•เคฒเคพ", + forumCatGAMING: "เค—เฅ‡เคฎเคฟเค‚เค—", + forumCatBOOKS: "เค•เคฟเคคเคพเคฌเฅ‡เค‚", + forumCatFILMS: "เคซเคผเคฟเคฒเฅเคฎเฅ‡เค‚", + forumCatPHILOSOPHY: "เคฆเคฐเฅเคถเคจ", + forumCatSOCIETY: "เคธเคฎเคพเคœ", + forumCatPRIVACY: "เคจเคฟเคœเคคเคพ", + forumCatCYBERWARFARE: "เคธเคพเค‡เคฌเคฐ เคฏเฅเคฆเฅเคง", + forumCatSURVIVALISM: "เค‰เคคเฅเคคเคฐเคœเฅ€เคตเคฟเคคเคพ", forumTitleLabel: "เคถเฅ€เคฐเฅเคทเค•", forumTitlePlaceholder: "เคฎเค‚เคš เคถเฅ€เคฐเฅเคทเค•...", forumCreateButton: "เคฎเค‚เคš เคฌเคจเคพเคเค", @@ -1991,43 +2000,6 @@ module.exports = { reportsUnderReviewSectionTitle: "เคธเคฎเฅ€เค•เฅเคทเคพเคงเฅ€เคจ เคฐเคฟเคชเฅ‹เคฐเฅเคŸ", reportsResolvedSectionTitle: "เคนเคฒ เค•เฅ€ เค—เคˆ เคฐเคฟเคชเฅ‹เคฐเฅเคŸ", reportsInvalidSectionTitle: "เค…เคฎเคพเคจเฅเคฏ เคฐเคฟเคชเฅ‹เคฐเฅเคŸ", - reportsBugTemplateTitle: 'เคชเฅเคจเคฐเฅเคคเฅเคชเคพเคฆเคจ เคตเคฟเคตเคฐเคฃ (เคฌเค—)', - reportsFeatureTemplateTitle: 'เคตเคฟเคตเคฐเคฃ (เคธเฅเคตเคฟเคงเคพเคเค)', - reportsAbuseTemplateTitle: 'เคตเคฟเคตเคฐเคฃ (เคฆเฅเคฐเฅเคชเคฏเฅ‹เค—)', - reportsContentTemplateTitle: 'เคตเคฟเคตเคฐเคฃ (เคธเคพเคฎเค—เฅเคฐเฅ€ เคธเคฎเคธเฅเคฏเคพเคเค)', - reportsStepsToReproduceLabel: 'เคชเฅเคจเคฐเฅเคคเฅเคชเคพเคฆเคจ เค•เฅ‡ เคšเคฐเคฃ', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'เค…เคชเฅ‡เค•เฅเคทเคฟเคค เคชเคฐเคฟเคฃเคพเคฎ', - reportsExpectedBehaviorPlaceholder: 'เค•เฅเคฏเคพ เคนเฅ‹เคจเคพ เคšเคพเคนเคฟเค?', - reportsActualBehaviorLabel: 'เคตเคพเคธเฅเคคเคตเคฟเค• เคชเคฐเคฟเคฃเคพเคฎ', - reportsActualBehaviorPlaceholder: 'เคตเคพเคธเฅเคคเคต เคฎเฅ‡เค‚ เค•เฅเคฏเคพ เคนเฅ‹เคคเคพ เคนเฅˆ?', - reportsEnvironmentLabel: 'เคตเคพเคคเคพเคตเคฐเคฃ', - reportsEnvironmentPlaceholder: 'เคธเค‚เคธเฅเค•เคฐเคฃ, เค‰เคชเค•เคฐเคฃ, OS, เคฌเฅเคฐเคพเค‰เคœเคผเคฐ, เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ, เคฒเฅ‰เค—, เค†เคฆเคฟเฅค', - reportsReproduceRateLabel: 'เคชเฅเคจเคฐเฅเคคเฅเคชเคพเคฆเคจ เคฆเคฐ', - reportsReproduceRateAlways: 'เคนเคฎเฅ‡เคถเคพ', - reportsReproduceRateOften: 'เค…เค•เฅเคธเคฐ', - reportsReproduceRateSometimes: 'เค•เคญเฅ€-เค•เคญเฅ€', - reportsReproduceRateRarely: 'เคถเคพเคฏเคฆ เคนเฅ€ เค•เคญเฅ€', - reportsReproduceRateUnable: 'เคชเฅเคจเคฐเฅเคคเฅเคชเคพเคฆเคจ เค•เคฐเคจเฅ‡ เคฎเฅ‡เค‚ เค…เคธเคฎเคฐเฅเคฅ', - reportsReproduceRateUnknown: 'เคจเคฟเคฐเฅเคฆเคฟเคทเฅเคŸ เคจเคนเฅ€เค‚', - reportsProblemStatementLabel: 'เคธเคฎเคธเฅเคฏเคพ / เค†เคตเคถเฅเคฏเค•เคคเคพ', - reportsProblemStatementPlaceholder: 'เคฏเคน เคธเฅเคตเคฟเคงเคพ เค•เฅŒเคจ เคธเฅ€ เคธเคฎเคธเฅเคฏเคพ เคนเคฒ เค•เคฐเคคเฅ€ เคนเฅˆ?', - reportsUserStoryLabel: 'เคจเคฟเคตเคพเคธเฅ€ เค•เคนเคพเคจเฅ€', - reportsUserStoryPlaceholder: 'เคเค• <เคจเคฟเคตเคพเคธเฅ€> เค•เฅ‡ เคฐเฅ‚เคช เคฎเฅ‡เค‚, เคฎเฅˆเค‚ <เค•เคพเคฐเฅเคฐเคตเคพเคˆ> เคšเคพเคนเคคเคพ เคนเฅ‚เค, เคคเคพเค•เคฟ <เคฒเคพเคญ>เฅค', - reportsAcceptanceCriteriaLabel: 'เคธเฅเคตเฅ€เค•เฅƒเคคเคฟ เคฎเคพเคจเคฆเค‚เคก', - reportsAcceptanceCriteriaPlaceholder: '- เคฆเคฟเคฏเคพ เค—เคฏเคพ...\n- เคœเคฌ...\n- เคคเคฌ...', - reportsWhatHappenedLabel: 'เค•เฅเคฏเคพ เคนเฅเค†?', - reportsWhatHappenedPlaceholder: 'เคธเค‚เคฆเคฐเฅเคญ เค”เคฐ เค…เคจเฅเคฎเคพเคจเคฟเคค เคคเคฟเคฅเคฟเคฏเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เค˜เคŸเคจเคพ เค•เคพ เคตเคฐเฅเคฃเคจ เค•เคฐเฅ‡เค‚เฅค', - reportsReportedUserLabel: 'เคฐเคฟเคชเฅ‹เคฐเฅเคŸ เค•เคฟเคฏเคพ เค—เคฏเคพ เคจเคฟเคตเคพเคธเฅ€ / เคธเค‚เคธเฅเคฅเคพ', - reportsReportedUserPlaceholder: '@เคจเคฟเคตเคพเคธเฅ€, เคซเคผเฅ€เคก, ID, เคฒเคฟเค‚เค•, เค†เคฆเคฟเฅค', - reportsEvidenceLinksLabel: 'เคธเคพเค•เฅเคทเฅเคฏ / เคฒเคฟเค‚เค•', - reportsEvidenceLinksPlaceholder: 'เคฒเคฟเค‚เค•, เคธเค‚เคฆเฅ‡เคถ IDs, เคธเฅเค•เฅเคฐเฅ€เคจเคถเฅ‰เคŸ (เคฏเคฆเคฟ เคฒเคพเค—เฅ‚ เคนเฅ‹), เค†เคฆเคฟ เคชเฅ‡เคธเฅเคŸ เค•เคฐเฅ‡เค‚เฅค', - reportsContentLocationLabel: 'เคธเคพเคฎเค—เฅเคฐเฅ€ เค•เคนเคพเค เคนเฅˆ', - reportsContentLocationPlaceholder: 'เคฒเคฟเค‚เค•, ID, เคšเฅˆเคจเคฒ, เคฅเฅเคฐเฅ‡เคก, เคฒเฅ‡เค–เค•, เค†เคฆเคฟเฅค', - reportsWhyInappropriateLabel: "เคฏเคน เค…เคจเฅเคšเคฟเคค เค•เฅเคฏเฅ‹เค‚ เคนเฅˆ", - reportsWhyInappropriatePlaceholder: 'เค•เคพเคฐเคฃ เค”เคฐ เคชเฅเคฐเคญเคพเคต เคฌเคคเคพเคเคเฅค', - reportsRequestedActionLabel: 'เค…เคจเฅเคฐเฅ‹เคงเคฟเคค เค•เคพเคฐเฅเคฐเคตเคพเคˆ', - reportsRequestedActionPlaceholder: 'เคนเคŸเคพเคเค, เค›เฅเคชเคพเคเค, เคŸเฅˆเค— เค•เคฐเฅ‡เค‚, เคšเฅ‡เคคเคพเคตเคจเฅ€ เคฆเฅ‡เค‚, เค†เคฆเคฟเฅค', tribesTitle: "เคœเคจเคœเคพเคคเคฟเคฏเคพเค", tribeFilterAll: "เคธเคญเฅ€", tribeFilterRecent: "เคนเคพเคฒ เค•เฅ‡", @@ -2271,6 +2243,7 @@ module.exports = { voteWellResearched: "เค…เคšเฅเค›เฅ€ เคคเคฐเคน เคถเฅ‹เคงเคฟเคค", voteNeedsSources: "เคธเฅเคฐเฅ‹เคค เคšเคพเคนเคฟเค", voteWrong: "เค—เคฒเคค", + voteOutdated: "เคชเฅเคฐเคพเคจเคพ", voteLowQuality: "เคจเคฟเคฎเฅเคจ เค—เฅเคฃเคตเคคเฅเคคเคพ", voteLove: "เคชเฅเคฏเคพเคฐ", voteClear: "เคธเฅเคชเคทเฅเคŸ", @@ -2515,7 +2488,6 @@ module.exports = { shopNoProducts: "เค…เคญเฅ€ เคคเค• เค•เฅ‹เคˆ เค‰เคคเฅเคชเคพเคฆ เคจเคนเฅ€เค‚เฅค", shopOutOfStock: "เคธเฅเคŸเฅ‰เค• เคฎเฅ‡เค‚ เคจเคนเฅ€เค‚", shopBuy: "เค–เคฐเฅ€เคฆเฅ‡เค‚", - shopBuyEncryptedNote: "เค†เคชเค•เฅ‡ เคกเคฟเคฒเฅ€เคตเคฐเฅ€ เคตเคฟเคตเคฐเคฃ เคเคจเฅเค•เฅเคฐเคฟเคชเฅเคŸเฅ‡เคก เคญเฅ‡เคœเฅ‡ เคœเคพเคคเฅ‡ เคนเฅˆเค‚ เค”เคฐ เค•เฅ‡เคตเคฒ เคฆเฅเค•เคพเคจ เคฎเคพเคฒเคฟเค• เคฆเฅ‡เค– เคธเค•เคคเคพ เคนเฅˆเฅค", shopBuyDeliveryAddress: "เคกเคฟเคฒเฅ€เคตเคฐเฅ€ เคชเคคเคพ", shopBuyContact: "เคธเค‚เคชเคฐเฅเค•", shopBuyContactPlaceholder: "เคˆเคฎเฅ‡เคฒ, เคซเฅ‹เคจ, เค†เคฆเคฟเฅค", @@ -3032,6 +3004,16 @@ module.exports = { industryDescriptionLabel: "เคตเคฟเคตเคฐเคฃ", industryDescriptionPlaceholder: "เคฏเคน เคซเฅˆเค•เฅเคŸเคฐเฅ€ เค•เฅเคฏเคพ เค‰เคคเฅเคชเคพเคฆเคฟเคค เค•เคฐเคคเฅ€ เคนเฅˆ?", industrySector: "เค•เฅเคทเฅ‡เคคเฅเคฐ", + industrySector_software: "เคธเฅ‰เคซเคผเฅเคŸเคตเฅ‡เคฏเคฐ", + industrySector_hardware: "เคนเคพเคฐเฅเคกเคตเฅ‡เคฏเคฐ", + industrySector_agriculture: "เค•เฅƒเคทเคฟ", + industrySector_textile: "เคตเคธเฅเคคเฅเคฐ", + industrySector_energy: "เคŠเคฐเฅเคœเคพ", + industrySector_food: "เค–เคพเคฆเฅเคฏ", + industrySector_construction: "เคจเคฟเคฐเฅเคฎเคพเคฃ", + industrySector_media: "เคฎเฅ€เคกเคฟเคฏเคพ", + industrySector_services: "เคธเฅ‡เคตเคพเคเค", + industrySector_other: "เค…เคจเฅเคฏ", industryMembershipPolicy: "เคธเคฆเคธเฅเคฏเคคเคพ เคจเฅ€เคคเคฟ", industryQuorum: "เค•เฅ‹เคฐเคฎ", industryMajority: "เคฌเคนเฅเคฎเคค", @@ -3233,6 +3215,18 @@ module.exports = { modulesFavoritesDescription: "เค…เคชเคจเฅ€ เคชเคธเค‚เคฆเฅ€เคฆเคพ เคธเคพเคฎเค—เฅเคฐเฅ€ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค•เคฐเคจเฅ‡ เค•เคพ เคฎเฅ‰เคกเฅเคฏเฅ‚เคฒเฅค", fileTooLargeTitle: "เคซเคผเคพเค‡เคฒ เคฌเคนเฅเคค เคฌเคกเคผเฅ€", fileTooLargeMessage: "เคซเคผเคพเค‡เคฒ เค…เคงเคฟเค•เคคเคฎ เค…เคจเฅเคฎเคค เค†เค•เคพเคฐ (75 MB) เคธเฅ‡ เค…เคงเคฟเค• เคนเฅˆเฅค เค•เฅƒเคชเคฏเคพ เคเค• เค›เฅ‹เคŸเฅ€ เคซเคผเคพเค‡เคฒ เคšเฅเคจเฅ‡เค‚เฅค", + fileWrongType: "เคฏเคน เคซเคผเคพเค‡เคฒ เค‡เคธ เค…เคจเฅเคญเคพเค— เค•เฅ‡ เคฒเคฟเค เคธเคนเฅ€ เคชเฅเคฐเค•เคพเคฐ เค•เฅ€ เคจเคนเฅ€เค‚ เคนเฅˆเฅค", + mediaLockedAfterOpinions: "เค‡เคธ เคชเคฐ เคชเคนเคฒเฅ‡ เคธเฅ‡ เคฐเคพเคฏ เคนเฅˆเค‚: เค†เคช เคฌเคพเค•เฅ€ เคธเค‚เคชเคพเคฆเคฟเคค เค•เคฐ เคธเค•เคคเฅ‡ เคนเฅˆเค‚, เคฒเฅ‡เค•เคฟเคจ เคœเคฟเคธเค•เคพ เคฎเฅ‚เคฒเฅเคฏเคพเค‚เค•เคจ เคนเฅเค† เค‰เคธเฅ‡ เคจเคนเฅ€เค‚ เคฌเคฆเคฒ เคธเค•เคคเฅ‡เฅค", + pdfPrevPage: "เคชเคฟเค›เคฒเคพ เคชเฅƒเคทเฅเค ", + pdfNextPage: "เค…เค—เคฒเคพ เคชเฅƒเคทเฅเค ", + pdfPage: "เคชเฅƒเคทเฅเค ", + pdfZoomIn: "เคœเคผเฅ‚เคฎ เค‡เคจ", + pdfZoomOut: "เคœเคผเฅ‚เคฎ เค†เค‰เคŸ", + pdfFullscreen: "เคชเฅ‚เคฐเฅเคฃ เคธเฅเค•เฅเคฐเฅ€เคจ", + pdfClose: "เคฌเค‚เคฆ เค•เคฐเฅ‡เค‚", + pdfRotate: "เค˜เฅเคฎเคพเคเค", + contentNotAvailableYet: "เคฏเคน เคธเคพเคฎเค—เฅเคฐเฅ€ เค…เคญเฅ€ เค‰เคชเคฒเคฌเฅเคง เคจเคนเฅ€เค‚ เคนเฅˆเฅค", + pdfRetry: "เคซเคฟเคฐ เคธเฅ‡ เค•เฅ‹เคถเคฟเคถ เค•เคฐเฅ‡เค‚", goBack: "เคตเคพเคชเคธ เคœเคพเคเค", errorDismiss: "เค เฅ€เค•", errorPageTitle: "เค•เฅเค› เค—เคฒเคค เคนเฅ‹ เค—เคฏเคพ", @@ -3275,9 +3269,7 @@ module.exports = { uploadMedia: "เคฎเฅ€เคกเคฟเคฏเคพ เค…เคชเคฒเฅ‹เคก เค•เคฐเฅ‡เค‚ (เค…เคงเคฟเค•เคคเคฎ-เค†เค•เคพเคฐ: 75MB)", feedOpenDiscussion: "เคšเคฐเฅเคšเคพ เค–เฅ‹เคฒเฅ‡เค‚", feedDetailTitle: "เคซเคผเฅ€เคก", - feedPostComment: "เคŸเคฟเคชเฅเคชเคฃเฅ€ เคชเฅ‹เคธเฅเคŸ เค•เคฐเฅ‡เค‚", courtsRespondentInvalid: "เคเค• เคตเฅˆเคง SSB ID เคนเฅ‹เคจเฅ€ เคšเคพเคนเคฟเค (@...ed25519)", - noComments: "เค…เคญเฅ€ เคคเค• เค•เฅ‹เคˆ เคŸเคฟเคชเฅเคชเคฃเฅ€ เคจเคนเฅ€เค‚", mapsLabel: "เคฎเคพเคจเคšเคฟเคคเฅเคฐ", mapTitle: "เคฎเคพเคจเคšเคฟเคคเฅเคฐ", mapDescription: "เค…เคชเคจเฅ‡ เคจเฅ‡เคŸเคตเคฐเฅเค• เคฎเฅ‡เค‚ เค‘เคซเคผเคฒเคพเค‡เคจ เคฎเคพเคจเคšเคฟเคคเฅเคฐ เคชเฅเคฐเคฌเค‚เคงเคฟเคค เค”เคฐ เคธเคพเคเคพ เค•เคฐเฅ‡เค‚เฅค", @@ -3581,6 +3573,8 @@ module.exports = { podcastTagsPlaceholder: "เค…เคฒเฅเคชเคตเคฟเคฐเคพเคฎ เคธเฅ‡ เค…เคฒเค— เค•เคฐเค•เฅ‡ เคŸเฅˆเค— เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚", podcastFilterAll: "เคธเคญเฅ€", podcastFilterMine: "เคฎเฅ‡เคฐเฅ‡", + podcastFilterFavorites: "เคชเคธเค‚เคฆเฅ€เคฆเคพ", + podcastEpisodeNumberLabel: "เคเคชเคฟเคธเฅ‹เคก เคธเค‚เค–เฅเคฏเคพ", podcastFilterRecent: "เคนเคพเคฒ เค•เฅ‡", podcastFilterTop: "เคŸเฅ‰เคช", podcastFilterViewers: "เคถเฅเคฐเฅ‹เคคเคพ", @@ -3799,7 +3793,6 @@ module.exports = { wikiBackToPage: "เคชเฅƒเคทเฅเค  เคชเคฐ เคตเคพเคชเคธ", wikiLinkedChip: "เคœเฅเคกเคผเคพ", wikiRestoredSummary: "เคชเฅเคจเคฐเฅเคธเฅเคฅเคพเคชเคฟเคค เคธเค‚เคธเฅเค•เคฐเคฃ", - wikiExportPdf: "เคชเฅƒเคทเฅเค  เคฌเคจเคพเคเค", wikiPreview: "เคชเฅ‚เคฐเฅเคตเคพเคตเคฒเฅ‹เค•เคจ", tribeSectionWiki: "เคตเคฟเค•เฅ€", tribeWikiCreate: "เคตเคฟเค•เฅ€ เคฌเคจเคพเคเค", @@ -3867,6 +3860,9 @@ module.exports = { roomRecStop: "เคฐเคฟเค•เฅ‰เคฐเฅเคกเคฟเค‚เค— เคฐเฅ‹เค•เฅ‡เค‚", roomStreamFile: "เคธเฅเคŸเฅเคฐเฅ€เคฎ เค•เคฐเฅ‡เค‚", roomStreamStop: "เคฐเฅ‹เค•เฅ‡เค‚", + roomScreen: "เคธเฅเค•เฅเคฐเฅ€เคจ", + roomScreenSharing: "เคธเฅเค•เฅเคฐเฅ€เคจ เคธเคพเคเคพ เคนเฅ‹ เคฐเคนเฅ€ เคนเฅˆ", + roomScreenWarning: "เคœเคฌ เคคเค• เค†เคช เคฐเฅ‹เค•เคคเฅ‡ เคจเคนเฅ€เค‚, เค•เคฎเคฐเฅ‡ เคฎเฅ‡เค‚ เคธเคญเฅ€ เค†เคชเค•เฅ€ เคชเฅ‚เคฐเฅ€ เคธเฅเค•เฅเคฐเฅ€เคจ เคฆเฅ‡เค–เฅ‡เค‚เค—เฅ‡เฅค", roomStreaming: "เคธเฅเคŸเฅเคฐเฅ€เคฎ เคนเฅ‹ เคฐเคนเคพ เคนเฅˆ", liveFullscreen: "เคชเฅ‚เคฐเฅเคฃ เคธเฅเค•เฅเคฐเฅ€เคจ", roomStreamPickTitle: "เคตเฅ€เคกเคฟเคฏเฅ‹ เคšเฅเคจเฅ‡เค‚", @@ -3924,6 +3920,7 @@ module.exports = { inviteAlreadyMember: "เค†เคช เคชเคนเคฒเฅ‡ เคธเฅ‡ เคถเคพเคฎเคฟเคฒ เคนเฅˆเค‚เฅค เค†เคช เค‡เคธเฅ‡ เค‡เคธเค•เฅ‡ เคฎเฅ‰เคกเฅเคฏเฅ‚เคฒ เคธเฅ‡ เค–เฅ‹เคฒ เคธเค•เคคเฅ‡ เคนเฅˆเค‚เฅค", actionFailed: "เคฏเคน เคจเคนเฅ€เค‚ เคนเฅ‹ เคธเค•เคพเฅค เค•เฅƒเคชเคฏเคพ เคซเคฟเคฐ เคธเฅ‡ เคชเฅเคฐเคฏเคพเคธ เค•เคฐเฅ‡เค‚เฅค", publishTooShort: "เค†เคชเค•เฅ€ เคชเฅ‹เคธเฅเคŸ เคฌเคนเฅเคค เค›เฅ‹เคŸเฅ€ เคนเฅˆเฅค", + publishEmpty: "เค•เฅเค› เคฒเคฟเค–เฅ‡เค‚ เคฏเคพ เค•เฅ‹เคˆ เคซเคผเคพเค‡เคฒ เคœเฅ‹เคกเคผเฅ‡เค‚เฅค", walletSendFailed: "เคญเฅเค—เคคเคพเคจ เคจเคนเฅ€เค‚ เคญเฅ‡เคœเคพ เคœเคพ เคธเค•เคพ: {reason}", pixeliaCoordsError: "เคฏเฅ‡ เคจเคฟเคฐเฅเคฆเฅ‡เคถเคพเค‚เค• เค—เฅเคฐเคฟเคก เคธเฅ‡ เคฌเคพเคนเคฐ เคนเฅˆเค‚เฅค", courtsErrTitle: "เคถเฅ€เคฐเฅเคทเค• เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค", @@ -4435,6 +4432,9 @@ module.exports = { torrentTagsLabel: "เคŸเฅˆเค—", torrentTagsPlaceholder: "เค…เคฒเฅเคชเคตเคฟเคฐเคพเคฎ เคธเฅ‡ เค…เคฒเค— เค•เคฐเค•เฅ‡ เคŸเฅˆเค— เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚", torrentSizeLabel: "เค†เค•เคพเคฐ", + torrentInfoHash: "เค‡เคจเฅเคซเฅ‹เคนเฅˆเคถ", + torrentFilesLabel: "เคซเคผเคพเค‡เคฒเฅ‡เค‚", + torrentMagnet: "MAGNET", torrentFromContent: "เคŸเฅ‹เคฐเฅ‡เค‚เคŸ เคฌเคจเคพเคเค", torrentFromContentMissing: "เคซเคผเคพเค‡เคฒ เค…เคญเฅ€ เค‡เคธ เคจเฅ‹เคก เคชเคฐ เค‰เคชเคฒเคฌเฅเคง เคจเคนเฅ€เค‚ เคนเฅˆเฅค เคฅเฅ‹เคกเคผเฅ€ เคฆเฅ‡เคฐ เคฌเคพเคฆ เคซเคฟเคฐ เค•เฅ‹เคถเคฟเคถ เค•เคฐเฅ‡เค‚เฅค", torrentDownloads: "เคกเคพเค‰เคจเคฒเฅ‹เคก", @@ -4520,9 +4520,6 @@ module.exports = { inhabitantsPendingAccept: "เคธเฅเคตเฅ€เค•เคพเคฐ", inhabitantsPendingReject: "เค…เคธเฅเคตเฅ€เค•เคพเคฐ", tribeSectionGovernance: "เคถเคพเคธเคจ", - tribePadInviteRequired: "เคชเฅˆเคก เคคเค• เคชเคนเฅเค‚เคš เคจเคนเฅ€เค‚เฅค เคจเคฟเคฎเค‚เคคเฅเคฐเคฃ เคฎเคพเค‚เค—เฅ‡เค‚เฅค", - tribeChatInviteRequired: "เคšเฅˆเคŸ เคคเค• เคชเคนเฅเค‚เคš เคจเคนเฅ€เค‚เฅค เคจเคฟเคฎเค‚เคคเฅเคฐเคฃ เคฎเคพเค‚เค—เฅ‡เค‚เฅค", - tribeGovernanceDesc: "เค‡เคธ เคœเคจเคœเคพเคคเคฟ เค•เคพ เค†เค‚เคคเคฐเคฟเค• เคถเคพเคธเคจเฅค", tribeGovernanceNoGov: "เค•เฅ‹เคˆ เคธเค•เฅเคฐเคฟเคฏ เคธเคฐเค•เคพเคฐ เคจเคนเฅ€เค‚", tribeGovernanceNoGovDesc: "เค‡เคธ เคœเคจเคœเคพเคคเคฟ เคจเฅ‡ เค…เคญเฅ€ เคธเคฐเค•เคพเคฐ เคจเคนเฅ€เค‚ เคšเฅเคจเฅ€เฅค", tribeGovCardTitle: "เคตเคฐเฅเคคเคฎเคพเคจ เคธเคฐเค•เคพเคฐ", @@ -4579,7 +4576,6 @@ module.exports = { logsFilterMonth: "เคชเคฟเค›เคฒเคพ เคฎเคนเฅ€เคจเคพ", logsFilterWeek: "เคชเคฟเค›เคฒเคพ เคธเคชเฅเคคเคพเคน", logsFilterToday: "เค†เคœ", - logsComments: "เคŸเคฟเคชเฅเคชเคฃเคฟเคฏเคพเค", logsAuthorEntries: "เคชเฅเคฐเคตเคฟเคทเฅเคŸเคฟเคฏเคพเค", logsCreate: "เคชเฅเคฐเคตเคฟเคทเฅเคŸเคฟ เคฌเคจเคพเคเค", logsExport: "เคฒเฅ‰เค— เคจเคฟเคฐเฅเคฏเคพเคค เค•เคฐเฅ‡เค‚", @@ -4735,6 +4731,8 @@ module.exports = { larpWillTestTitle: "WILL เคชเคฐเฅ€เค•เฅเคทเคฃ", settingsLanDesc: "เค‡เคธ เคชเฅ€เคฏเคฐ เค•เฅ‹ เคธเคฎเคฏ-เคธเคฎเคฏ เคชเคฐ เค‰เคธเฅ€ เคธเฅเคฅเคพเคจเฅ€เคฏ เคจเฅ‡เคŸเคตเคฐเฅเค• เคฎเฅ‡เค‚ เค…เคจเฅเคฏ Oasis เค‡เค‚เคธเฅเคŸเฅ‡เค‚เคธ เค•เฅ‹ เค˜เฅ‹เคทเคฟเคค เค•เคฐเฅ‡เค‚เฅค", settingsLanEnable: "LAN เคชเฅเคฐเคธเคพเคฐเคฃ เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚", + settingsConnectionsTitle: "เค•เคจเฅ‡เค•เฅเคถเคจ", + settingsConnectionsDesc: "เคฏเคน เคจเฅ‹เคก เคเค• เคธเคพเคฅ เค•เคฟเคคเคจเฅ‡ เคชเฅ€เคฏเคฐ เคธเฅ‡ เคœเฅเคกเคผเคพ เคฐเคนเคคเคพ เคนเฅˆเฅค", settingsLanTitle: "LAN เคชเฅเคฐเคธเคพเคฐเคฃ", settingsReplicationTitle: "เคชเฅเคฐเคคเคฟเค•เฅƒเคคเคฟ", settingsReplicationDesc: "เคธเคพเคฎเค—เฅเคฐเฅ€ เค•เฅ€ เคชเฅเคฐเคคเคฟเค•เฅƒเคคเคฟ เคฌเคจเคพเคคเฅ‡ เคธเคฎเคฏ เค†เคชเค•เฅ‡ เคชเฅ€เคฏเคฐ เคฆเฅเคตเคพเคฐเคพ เค†เคชเค•เฅ‡ เค…เคชเคจเฅ‡ เคซเคผเฅ€เคก เคธเฅ‡ เค†เค—เฅ‡ เคœเคพเคจเฅ‡ เคตเคพเคฒเฅ‡ เคนเฅ‰เคช เค•เฅ€ เคธเค‚เค–เฅเคฏเคพ เค•เฅ‰เคจเฅเคซเคผเคฟเค—เคฐ เค•เคฐเฅ‡เค‚เฅค", diff --git a/src/client/assets/translations/oasis_it.js b/src/client/assets/translations/oasis_it.js index c84d3af..2701e38 100644 --- a/src/client/assets/translations/oasis_it.js +++ b/src/client/assets/translations/oasis_it.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† La mia rete", graphosShowing: "Mostrando", graphosYou: "Tu", - graphosTotalNodes: "Nodi totali", + graphosTotalNodes: "Totali", + onlinePlural: "Online", mentions: "Menzioni", mentionsDescription: [ strong("Ogni messaggio che ti @menziona"), @@ -418,6 +419,7 @@ module.exports = { menuMedia: "Biblioteca", menuTools: "Strumenti", comment: "Commento", + typeComment: "Commento", subtopic: "Sotto-argomento", json: "JSON", unfollow: "Rimuovi supporto", @@ -447,9 +449,6 @@ module.exports = { publishCustomDescription: [ "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non puรฒ essere modificato nรฉ eliminato.", ], - commentWarning: [ - "RICORDA: A causa della tecnologia blockchain, una volta pubblicato un post non puรฒ essere modificato nรฉ eliminato.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -728,10 +727,6 @@ module.exports = { votesQuestionLabel:"Domanda", voteStatusOpen:"APERTO", voteStatusClosed:"CHIUSO", - commentDescription: ({ parentUrl }) => [ - " ha commentato in ", - a({ href: parentUrl }, " discussione"), - ], commentTitle: ({ authorName }) => [`Commento al post di @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " ha creato un sotto-argomento da ", @@ -866,6 +861,8 @@ module.exports = { bankRulesEligibility5: "Il PUB paga nel ciclo successivo e pubblica un trasferimento RBU che puoi confermare.", bankRulesEligibility6: "Se manca l'indirizzo, il feed รจ troppo giovane, hai rifiutato il mese o il PUB non ha fondi, la richiesta viene saltata e registrata sul PUB.", copyLinkTitle: "Copia link", + cardMenuTitle: "Altre azioni", + shareContent: "Condividi", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Rifiuta il RBU", @@ -899,7 +896,7 @@ module.exports = { walletNotConfigured: "Il tuo portafoglio ECOin non รจ ancora configurato.", walletSettingsDescription: "Integra Oasis con il tuo portafoglio ECOin.", walletSettingsMobileDescription: "Collega Oasis all'app ECOin Wallet di questo dispositivo.", - walletDownloadEcoin: "Scarica ECOin!", + walletDownloadEcoin: "Scarica ECOin", walletRpcUrl: "URL RPC", walletStatusMessages: { invalid_amount: "Importo non valido", @@ -952,6 +949,7 @@ module.exports = { bookmarkUpdateButton: "Aggiorna", bookmarkAddFavoriteButton: "Aggiungi preferito", bookmarkRemoveFavoriteButton: "Rimuovi preferito", + bookmarkTitleLabel: "Titolo", bookmarkUrlLabel: "Link", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descrizione", @@ -1096,9 +1094,9 @@ module.exports = { allCVs: "CV", discoverPeople: "Scopri abitanti nella tua rete.", searchInhabitantsPlaceholder: "Cerca in Abitanti...", - filterLocation: "FILTRA abitanti PER POSIZIONE โ€ฆ", - filterLanguage: "FILTRA abitanti PER LINGUA โ€ฆ", - filterSkills: "FILTRA abitanti PER COMPETENZE โ€ฆ", + filterLocation: "Cerca per posizioneโ€ฆ", + filterLanguage: "Cerca per linguaโ€ฆ", + filterSkills: "Cerca per competenzeโ€ฆ", cvSkillsCount: "Competenze", applyFilters: "Applica filtri", locationLabel: "Posizione", @@ -1114,6 +1112,7 @@ module.exports = { topactivitySectionTitle: "Top Attivitร ", "TOP INACTIVITYButton": "PIร™ INATTIVI", topinactivitySectionTitle: "Abitanti meno attivi", + contactsSectionTitle: "Contatti", blockedSectionTitle: "Bloccati", gallerySectionTitle: "GALLERIA", topSectionTitle: "TOP", @@ -1567,9 +1566,7 @@ module.exports = { voteUnknownDate: "Data sconosciuta", voteDetailSectionTitle: 'Dettagli voto', voteCommentsLabel: 'Commenti', - voteCommentsForumButton: 'Apri discussione', - voteCommentsSectionTitle: 'Apri discussione', - voteNoCommentsYet: 'Non ci sono ancora commenti. Sii il primo a rispondere.', + voteNoCommentsYet: 'Non ci sono ancora commenti.', commentsGoToLast: "Vai all'ultimo commento", voteNewCommentPlaceholder: 'Scrivi il tuo commento quiโ€ฆ', voteNewCommentButton: 'Pubblica commento', @@ -1696,7 +1693,6 @@ module.exports = { pollVoteButton: "Vota", pollChangeVote: "Cambia Voto", pollVoters: "Votanti", - pollComments: "Commenti", pollStatusLabel: "Stato", pollStatusOpen: "APERTO", pollStatusClosed: "CHIUSO", @@ -1718,9 +1714,22 @@ module.exports = { blogAllowComments: "Consenti commenti", blogCommentsClosed: "L'autore ha chiuso i commenti di questo blog.", blogNoItems: "Nessun blog trovato.", - viewBlog: "Vedi Blog", forumTitle: "Forum", forumCategoryLabel: "Categoria", + forumCatGENERAL: "GENERALE", + forumCatPOLITICS: "POLITICA", + forumCatTECH: "TECNOLOGIA", + forumCatSCIENCE: "SCIENZA", + forumCatMUSIC: "MUSICA", + forumCatART: "ARTE", + forumCatGAMING: "GIOCHI", + forumCatBOOKS: "LIBRI", + forumCatFILMS: "FILM", + forumCatPHILOSOPHY: "FILOSOFIA", + forumCatSOCIETY: "SOCIETร€", + forumCatPRIVACY: "PRIVACY", + forumCatCYBERWARFARE: "CYBERGUERRA", + forumCatSURVIVALISM: "SOPRAVVIVENZA", forumTitleLabel: "Titolo", forumTitlePlaceholder: "Titolo del forum...", forumCreateButton: "Crea forum", @@ -1989,43 +1998,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Segnalazioni in revisione", reportsResolvedSectionTitle: "Segnalazioni risolte", reportsInvalidSectionTitle: "Segnalazioni non valide", - reportsBugTemplateTitle: 'Dettagli di riproduzione (Bug)', - reportsFeatureTemplateTitle: 'Dettagli (Funzionalitร )', - reportsAbuseTemplateTitle: 'Dettagli (Abuso)', - reportsContentTemplateTitle: 'Dettagli (Problemi di contenuto)', - reportsStepsToReproduceLabel: 'Passaggi per riprodurre', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Risultato atteso', - reportsExpectedBehaviorPlaceholder: 'Cosa dovrebbe succedere?', - reportsActualBehaviorLabel: 'Risultato effettivo', - reportsActualBehaviorPlaceholder: 'Cosa succede effettivamente?', - reportsEnvironmentLabel: 'Ambiente', - reportsEnvironmentPlaceholder: 'Versione, dispositivo, SO, browser, impostazioni, log, ecc.', - reportsReproduceRateLabel: 'Frequenza di riproduzione', - reportsReproduceRateAlways: 'Sempre', - reportsReproduceRateOften: 'Spesso', - reportsReproduceRateSometimes: 'A volte', - reportsReproduceRateRarely: 'Raramente', - reportsReproduceRateUnable: 'Impossibile riprodurre', - reportsReproduceRateUnknown: 'Non specificato', - reportsProblemStatementLabel: 'Problema / necessitร ', - reportsProblemStatementPlaceholder: 'Quale problema risolve questa funzionalitร ?', - reportsUserStoryLabel: 'Storia abitante', - reportsUserStoryPlaceholder: 'Come , voglio , cosรฌ che .', - reportsAcceptanceCriteriaLabel: 'Criteri di accettazione', - reportsAcceptanceCriteriaPlaceholder: '- Dato che...\n- Quando...\n- Allora...', - reportsWhatHappenedLabel: 'Cosa รจ successo?', - reportsWhatHappenedPlaceholder: 'Descrivi l\'incidente con contesto e date approssimative.', - reportsReportedUserLabel: 'Abitante / entitร  segnalata', - reportsReportedUserPlaceholder: '@abitante, feed, ID, link, ecc.', - reportsEvidenceLinksLabel: 'Prove / link', - reportsEvidenceLinksPlaceholder: 'Incolla link, ID messaggi, screenshot (se applicabile), ecc.', - reportsContentLocationLabel: 'Dove si trova il contenuto', - reportsContentLocationPlaceholder: 'Link, ID, canale, discussione, autore, ecc.', - reportsWhyInappropriateLabel: "Perchรฉ รจ inappropriato", - reportsWhyInappropriatePlaceholder: 'Spiega il motivo e l\'impatto.', - reportsRequestedActionLabel: 'Azione richiesta', - reportsRequestedActionPlaceholder: 'Rimuovi, nascondi, tagga, avvisa, ecc.', tribesTitle: "Tribรน", tribeFilterAll: "TUTTE", tribeFilterRecent: "RECENTI", @@ -2269,6 +2241,7 @@ module.exports = { voteWellResearched: "Ben documentato", voteNeedsSources: "Servono fonti", voteWrong: "Sbagliato", + voteOutdated: "Obsoleto", voteLowQuality: "Bassa qualitร ", voteLove: "Amore", voteClear: "Cancella", @@ -2513,7 +2486,6 @@ module.exports = { shopNoProducts: "Nessun prodotto ancora.", shopOutOfStock: "Esaurito", shopBuy: "Acquista", - shopBuyEncryptedNote: "I tuoi dati di consegna vengono inviati cifrati e visibili solo al proprietario del negozio.", shopBuyDeliveryAddress: "Indirizzo di consegna", shopBuyContact: "Contatto", shopBuyContactPlaceholder: "email, telefono, ecc.", @@ -3030,6 +3002,16 @@ module.exports = { industryDescriptionLabel: "Descrizione", industryDescriptionPlaceholder: "Cosa produce questa fabbrica?", industrySector: "Settore", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Agricoltura", + industrySector_textile: "Tessile", + industrySector_energy: "Energia", + industrySector_food: "Alimentare", + industrySector_construction: "Edilizia", + industrySector_media: "Media", + industrySector_services: "Servizi", + industrySector_other: "Altro", industryMembershipPolicy: "Politica di adesione", industryQuorum: "Quorum", industryMajority: "Maggioranza", @@ -3231,6 +3213,18 @@ module.exports = { modulesFavoritesDescription: "Modulo per gestire i tuoi contenuti preferiti.", fileTooLargeTitle: "File troppo grande", fileTooLargeMessage: "Il file supera la dimensione massima consentita (75 MB). Seleziona un file piรน piccolo.", + fileWrongType: "Questo file non รจ del tipo giusto per questa sezione.", + mediaLockedAfterOpinions: "Ha giร  delle opinioni: puoi modificare il resto, ma non ciรฒ che รจ stato valutato.", + pdfPrevPage: "Pagina precedente", + pdfNextPage: "Pagina successiva", + pdfPage: "Pagina", + pdfZoomIn: "Ingrandisci", + pdfZoomOut: "Riduci", + pdfFullscreen: "Schermo intero", + pdfClose: "Chiudi", + pdfRotate: "Ruota", + contentNotAvailableYet: "Questo contenuto non รจ ancora disponibile.", + pdfRetry: "Riprova", goBack: "Torna indietro", errorDismiss: "OK", errorPageTitle: "Si รจ verificato un errore", @@ -3275,8 +3269,6 @@ module.exports = { courtsRespondentInvalid: "Deve essere un ID SSB valido (@...ed25519)", feedDetailTitle: "Feed", feedOpenDiscussion: "Apri discussione", - feedPostComment: "Pubblica commento", - noComments: "Nessun commento ancora", mapsLabel: "Mappe", mapTitle: "Mappe", mapDescription: "Gestisci e condividi mappe offline nella tua rete.", @@ -3580,6 +3572,8 @@ module.exports = { podcastTagsPlaceholder: "Inserisci tag separati da virgole", podcastFilterAll: "TUTTI", podcastFilterMine: "MIEI", + podcastFilterFavorites: "PREFERITI", + podcastEpisodeNumberLabel: "Numero di episodio", podcastFilterRecent: "RECENTI", podcastFilterTop: "TOP", podcastFilterViewers: "ASCOLTATORI", @@ -3798,7 +3792,6 @@ module.exports = { wikiBackToPage: "Torna alla pagina", wikiLinkedChip: "Collegata", wikiRestoredSummary: "Versione ripristinata", - wikiExportPdf: "Genera pagina", wikiPreview: "Anteprima", tribeSectionWiki: "WIKI", tribeWikiCreate: "Crea wiki", @@ -3866,6 +3859,9 @@ module.exports = { roomRecStop: "Ferma la registrazione", roomStreamFile: "Trasmetti", roomStreamStop: "Ferma", + roomScreen: "Schermo", + roomScreenSharing: "Condivisione schermo", + roomScreenWarning: "Tutti nella stanza vedranno il tuo schermo intero finchรฉ non ti fermi.", roomStreaming: "In trasmissione", liveFullscreen: "Schermo intero", roomStreamPickTitle: "Scegli un video", @@ -3923,6 +3919,7 @@ module.exports = { inviteAlreadyMember: "Ne fai giร  parte. Puoi aprirlo dal suo modulo.", actionFailed: "Non รจ stato possibile farlo. Riprova.", publishTooShort: "Il tuo post รจ troppo corto.", + publishEmpty: "Scrivi qualcosa o allega un file.", walletSendFailed: "Non รจ stato possibile inviare il pagamento: {reason}", pixeliaCoordsError: "Quelle coordinate sono fuori dalla griglia.", courtsErrTitle: "Serve un titolo.", @@ -4434,6 +4431,9 @@ module.exports = { torrentTagsLabel: "Tag", torrentTagsPlaceholder: "Inserisci tag separati da virgole", torrentSizeLabel: "Dimensione", + torrentInfoHash: "Infohash", + torrentFilesLabel: "File", + torrentMagnet: "MAGNET", torrentFromContent: "Genera torrent", torrentFromContentMissing: "Il file non รจ ancora disponibile su questo nodo. Riprova tra un po'.", torrentDownloads: "Download", @@ -4519,9 +4519,6 @@ module.exports = { inhabitantsPendingAccept: "Accetta", inhabitantsPendingReject: "Rifiuta", tribeSectionGovernance: "GOVERNANCE", - tribePadInviteRequired: "Non hai accesso al pad. Richiedi un invito.", - tribeChatInviteRequired: "Non hai accesso alla chat. Richiedi un invito.", - tribeGovernanceDesc: "Governance interna di questa tribรน.", tribeGovernanceNoGov: "Nessun governo attivo", tribeGovernanceNoGovDesc: "Questa tribรน non ha ancora eletto un governo.", tribeGovCardTitle: "Governo Attuale", @@ -4578,7 +4575,6 @@ module.exports = { logsFilterMonth: "ULTIMO MESE", logsFilterWeek: "ULTIMA SETTIMANA", logsFilterToday: "OGGI", - logsComments: "Commenti", logsAuthorEntries: "Voci", logsCreate: "Crea voce", logsExport: "Esporta diario", @@ -4657,6 +4653,8 @@ module.exports = { larpWillTestTitle: "Test WILL", settingsLanDesc: "Annuncia periodicamente questo peer ad altre istanze di Oasis sulla stessa rete locale.", settingsLanEnable: "Abilita trasmissione LAN", + settingsConnectionsTitle: "Connessioni", + settingsConnectionsDesc: "Quanti peer questo nodo mantiene connessi contemporaneamente.", settingsLanTitle: "Trasmissione LAN", settingsReplicationTitle: "Replicazione", settingsReplicationDesc: "Configura il numero di salti che il tuo peer segue dal tuo feed durante la replica dei contenuti.", diff --git a/src/client/assets/translations/oasis_pt.js b/src/client/assets/translations/oasis_pt.js index c3dd6d3..2755b8f 100644 --- a/src/client/assets/translations/oasis_pt.js +++ b/src/client/assets/translations/oasis_pt.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† Minha rede", graphosShowing: "Mostrando", graphosYou: "Vocรช", - graphosTotalNodes: "Total de nรณs", + graphosTotalNodes: "Totais", + onlinePlural: "Online", mentions: "Menรงรตes", mentionsDescription: [ strong("Todas as mensagens que te @mencionam"), @@ -418,6 +419,7 @@ module.exports = { menuMedia: "Biblioteca", menuTools: "Ferramentas", comment: "Comentรกrio", + typeComment: "Comentรกrio", subtopic: "Subtรณpico", json: "JSON", unfollow: "Deixar de apoiar", @@ -447,9 +449,6 @@ module.exports = { publishCustomDescription: [ "LEMBRA-TE: Devido ร  tecnologia blockchain, uma vez publicado, nรฃo pode ser editado nem eliminado.", ], - commentWarning: [ - "LEMBRA-TE: Devido ร  tecnologia blockchain, uma vez publicado, nรฃo pode ser editado nem eliminado.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -728,10 +727,6 @@ module.exports = { votesQuestionLabel:"Pergunta", voteStatusOpen:"ABERTO", voteStatusClosed:"FECHADO", - commentDescription: ({ parentUrl }) => [ - " comentou em ", - a({ href: parentUrl }, " conversa"), - ], commentTitle: ({ authorName }) => [`Comentรกrio na publicaรงรฃo de @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " criou um subtรณpico de ", @@ -866,6 +861,8 @@ module.exports = { bankRulesEligibility5: "O PUB paga no ciclo seguinte e publica um transfer RBU que podes confirmar.", bankRulesEligibility6: "Se falta o endereรงo, o feed รฉ demasiado novo, recusaste o mรชs ou o PUB nรฃo tem fundos, a reclamaรงรฃo รฉ ignorada e registada no PUB.", copyLinkTitle: "Copiar ligaรงรฃo", + cardMenuTitle: "Mais aรงรตes", + shareContent: "Partilhar", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "Recusar RBU", @@ -899,7 +896,7 @@ module.exports = { walletNotConfigured: "A sua carteira ECOin ainda nรฃo estรก configurada.", walletSettingsDescription: "Integra o Oasis com a tua carteira ECOin.", walletSettingsMobileDescription: "Ligue o Oasis ร  app ECOin Wallet deste dispositivo.", - walletDownloadEcoin: "Descarregar ECOin!", + walletDownloadEcoin: "Descarregar ECOin", walletRpcUrl: "URL do RPC", walletStatusMessages: { invalid_amount: "Montante invรกlido", @@ -952,6 +949,7 @@ module.exports = { bookmarkUpdateButton: "Atualizar", bookmarkAddFavoriteButton: "Adicionar favorito", bookmarkRemoveFavoriteButton: "Remover favorito", + bookmarkTitleLabel: "Tรญtulo", bookmarkUrlLabel: "Link", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "Descriรงรฃo", @@ -1096,9 +1094,9 @@ module.exports = { allCVs: "CVs", discoverPeople: "Descobre habitantes na tua rede.", searchInhabitantsPlaceholder: "Pesquisar em Habitantes...", - filterLocation: "FILTRAR habitantes POR LOCALIZAร‡รƒO โ€ฆ", - filterLanguage: "FILTRAR habitantes POR IDIOMA โ€ฆ", - filterSkills: "FILTRAR habitantes POR COMPETรŠNCIAS โ€ฆ", + filterLocation: "Pesquisar por localizaรงรฃoโ€ฆ", + filterLanguage: "Pesquisar por idiomaโ€ฆ", + filterSkills: "Pesquisar por competรชnciasโ€ฆ", cvSkillsCount: "Competรชncias", applyFilters: "Aplicar filtros", locationLabel: "Localizaรงรฃo", @@ -1114,6 +1112,7 @@ module.exports = { topactivitySectionTitle: "Top Atividade", "TOP INACTIVITYButton": "MAIS INATIVOS", topinactivitySectionTitle: "Habitantes menos ativos", + contactsSectionTitle: "Contactos", blockedSectionTitle: "Bloqueados", gallerySectionTitle: "GALERIA", topSectionTitle: "TOP", @@ -1567,9 +1566,7 @@ module.exports = { voteUnknownDate: "Data desconhecida", voteDetailSectionTitle: 'Detalhes da votaรงรฃo', voteCommentsLabel: 'Comentรกrios', - voteCommentsForumButton: 'Abrir discussรฃo', - voteCommentsSectionTitle: 'Abrir discussรฃo', - voteNoCommentsYet: 'Ainda sem comentรกrios. Sรช o primeiro a responder.', + voteNoCommentsYet: 'Ainda sem comentรกrios.', commentsGoToLast: "Ir para o รบltimo comentรกrio", voteNewCommentPlaceholder: 'Escreve o teu comentรกrio aquiโ€ฆ', voteNewCommentButton: 'Publicar comentรกrio', @@ -1696,7 +1693,6 @@ module.exports = { pollVoteButton: "Votar", pollChangeVote: "Mudar Voto", pollVoters: "Votantes", - pollComments: "Comentรกrios", pollStatusLabel: "Estado", pollStatusOpen: "ABERTA", pollStatusClosed: "FECHADA", @@ -1718,9 +1714,22 @@ module.exports = { blogAllowComments: "Permitir comentรกrios", blogCommentsClosed: "O autor fechou os comentรกrios deste blog.", blogNoItems: "Nenhum blog encontrado.", - viewBlog: "Ver Blog", forumTitle: "Fรณruns", forumCategoryLabel: "Categoria", + forumCatGENERAL: "GERAL", + forumCatPOLITICS: "POLรTICA", + forumCatTECH: "TECNOLOGIA", + forumCatSCIENCE: "CIรŠNCIA", + forumCatMUSIC: "MรšSICA", + forumCatART: "ARTE", + forumCatGAMING: "JOGOS", + forumCatBOOKS: "LIVROS", + forumCatFILMS: "FILMES", + forumCatPHILOSOPHY: "FILOSOFIA", + forumCatSOCIETY: "SOCIEDADE", + forumCatPRIVACY: "PRIVACIDADE", + forumCatCYBERWARFARE: "CIBERGUERRA", + forumCatSURVIVALISM: "SOBREVIVรŠNCIA", forumTitleLabel: "Tรญtulo", forumTitlePlaceholder: "Tรญtulo do fรณrum...", forumCreateButton: "Criar fรณrum", @@ -1989,43 +1998,6 @@ module.exports = { reportsUnderReviewSectionTitle: "Relatรณrios em anรกlise", reportsResolvedSectionTitle: "Relatรณrios resolvidos", reportsInvalidSectionTitle: "Relatรณrios invรกlidos", - reportsBugTemplateTitle: 'Detalhes de reproduรงรฃo (Bugs)', - reportsFeatureTemplateTitle: 'Detalhes (Funcionalidades)', - reportsAbuseTemplateTitle: 'Detalhes (Abuso)', - reportsContentTemplateTitle: 'Detalhes (Problemas de conteรบdo)', - reportsStepsToReproduceLabel: 'Passos para reproduzir', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: 'Resultado esperado', - reportsExpectedBehaviorPlaceholder: 'O que deveria acontecer?', - reportsActualBehaviorLabel: 'Resultado real', - reportsActualBehaviorPlaceholder: 'O que realmente acontece?', - reportsEnvironmentLabel: 'Ambiente', - reportsEnvironmentPlaceholder: 'Versรฃo, dispositivo, SO, navegador, definiรงรตes, registos, etc.', - reportsReproduceRateLabel: 'Taxa de reproduรงรฃo', - reportsReproduceRateAlways: 'Sempre', - reportsReproduceRateOften: 'Frequentemente', - reportsReproduceRateSometimes: 'ร€s vezes', - reportsReproduceRateRarely: 'Raramente', - reportsReproduceRateUnable: 'Impossรญvel reproduzir', - reportsReproduceRateUnknown: 'Nรฃo especificado', - reportsProblemStatementLabel: 'Problema / necessidade', - reportsProblemStatementPlaceholder: 'Que problema esta funcionalidade resolve?', - reportsUserStoryLabel: 'Histรณria do habitante', - reportsUserStoryPlaceholder: 'Como , quero , para que .', - reportsAcceptanceCriteriaLabel: 'Critรฉrios de aceitaรงรฃo', - reportsAcceptanceCriteriaPlaceholder: '- Dado...\n- Quando...\n- Entรฃo...', - reportsWhatHappenedLabel: 'O que aconteceu?', - reportsWhatHappenedPlaceholder: 'Descreve o incidente com contexto e datas aproximadas.', - reportsReportedUserLabel: 'Habitante / entidade denunciada', - reportsReportedUserPlaceholder: '@habitante, feed, ID, link, etc.', - reportsEvidenceLinksLabel: 'Provas / links', - reportsEvidenceLinksPlaceholder: 'Cola links, IDs de mensagens, capturas de ecrรฃ (se aplicรกvel), etc.', - reportsContentLocationLabel: 'Onde estรก o conteรบdo', - reportsContentLocationPlaceholder: 'Link, ID, canal, conversa, autor, etc.', - reportsWhyInappropriateLabel: "Porquรช inapropriado", - reportsWhyInappropriatePlaceholder: 'Explica o motivo e o impacto.', - reportsRequestedActionLabel: 'Aรงรฃo solicitada', - reportsRequestedActionPlaceholder: 'Remover, ocultar, etiquetar, avisar, etc.', tribesTitle: "Tribos", tribeFilterAll: "TODAS", tribeFilterRecent: "RECENTES", @@ -2269,6 +2241,7 @@ module.exports = { voteWellResearched: "Bem pesquisado", voteNeedsSources: "Precisa fontes", voteWrong: "Errado", + voteOutdated: "Desatualizado", voteLowQuality: "Baixa qualidade", voteLove: "Amor", voteClear: "Limpar", @@ -2513,7 +2486,6 @@ module.exports = { shopNoProducts: "Nenhum produto ainda.", shopOutOfStock: "Esgotado", shopBuy: "Comprar", - shopBuyEncryptedNote: "Os teus dados de entrega sรฃo enviados cifrados, visรญveis apenas para o dono da loja.", shopBuyDeliveryAddress: "Endereรงo de entrega", shopBuyContact: "Contacto", shopBuyContactPlaceholder: "email, telefone, etc.", @@ -3030,6 +3002,16 @@ module.exports = { industryDescriptionLabel: "Descriรงรฃo", industryDescriptionPlaceholder: "O que esta fรกbrica produz?", industrySector: "Setor", + industrySector_software: "Software", + industrySector_hardware: "Hardware", + industrySector_agriculture: "Agricultura", + industrySector_textile: "Tรชxtil", + industrySector_energy: "Energia", + industrySector_food: "Alimentaรงรฃo", + industrySector_construction: "Construรงรฃo", + industrySector_media: "Media", + industrySector_services: "Serviรงos", + industrySector_other: "Outros", industryMembershipPolicy: "Polรญtica de adesรฃo", industryQuorum: "Quรณrum", industryMajority: "Maioria", @@ -3231,6 +3213,18 @@ module.exports = { modulesFavoritesDescription: "Mรณdulo para gerir o teu conteรบdo favorito.", fileTooLargeTitle: "Ficheiro demasiado grande", fileTooLargeMessage: "O ficheiro excede o tamanho mรกximo permitido (75 MB). Por favor, seleciona um ficheiro mais pequeno.", + fileWrongType: "Este ficheiro nรฃo รฉ do tipo adequado para esta secรงรฃo.", + mediaLockedAfterOpinions: "Jรก tem opiniรตes: podes editar o resto, mas nรฃo mudar o que foi avaliado.", + pdfPrevPage: "Pรกgina anterior", + pdfNextPage: "Pรกgina seguinte", + pdfPage: "Pรกgina", + pdfZoomIn: "Aproximar", + pdfZoomOut: "Afastar", + pdfFullscreen: "Ecrรฃ inteiro", + pdfClose: "Fechar", + pdfRotate: "Rodar", + contentNotAvailableYet: "Este conteรบdo ainda nรฃo estรก disponรญvel.", + pdfRetry: "Tentar de novo", goBack: "Voltar", errorDismiss: "OK", errorPageTitle: "Algo correu mal", @@ -3275,8 +3269,6 @@ module.exports = { courtsRespondentInvalid: "Demandado invรกlido", feedDetailTitle: "Detalhe do feed", feedOpenDiscussion: "Abrir discussรฃo", - feedPostComment: "Publicar comentรกrio", - noComments: "Ainda sem comentรกrios.", mapsLabel: "Mapas", mapTitle: "Mapas", mapDescription: "Gerencie e compartilhe mapas offline na sua rede.", @@ -3580,6 +3572,8 @@ module.exports = { podcastTagsPlaceholder: "Introduz tags separadas por vรญrgulas", podcastFilterAll: "TODOS", podcastFilterMine: "MEUS", + podcastFilterFavorites: "FAVORITOS", + podcastEpisodeNumberLabel: "Nรบmero do episรณdio", podcastFilterRecent: "RECENTES", podcastFilterTop: "TOP", podcastFilterViewers: "OUVINTES", @@ -3798,7 +3792,6 @@ module.exports = { wikiBackToPage: "Voltar ร  pรกgina", wikiLinkedChip: "Ligada", wikiRestoredSummary: "Versรฃo restaurada", - wikiExportPdf: "Gerar pรกgina", wikiPreview: "Prรฉ-visualizar", tribeSectionWiki: "WIKI", tribeWikiCreate: "Criar wiki", @@ -3866,6 +3859,9 @@ module.exports = { roomRecStop: "Parar a gravaรงรฃo", roomStreamFile: "Transmitir", roomStreamStop: "Parar", + roomScreen: "Ecrรฃ", + roomScreenSharing: "Partilhando ecrรฃ", + roomScreenWarning: "Todos na sala verรฃo o teu ecrรฃ inteiro atรฉ parares.", roomStreaming: "A transmitir", liveFullscreen: "Ecrรฃ inteiro", roomStreamPickTitle: "Escolhe um vรญdeo", @@ -3923,6 +3919,7 @@ module.exports = { inviteAlreadyMember: "Jรก fazes parte. Podes abri-lo a partir do seu mรณdulo.", actionFailed: "Nรฃo foi possรญvel fazรช-lo. Tenta de novo.", publishTooShort: "A tua publicaรงรฃo รฉ demasiado curta.", + publishEmpty: "Escreve algo ou anexa um ficheiro.", walletSendFailed: "Nรฃo foi possรญvel enviar o pagamento: {reason}", pixeliaCoordsError: "Essas coordenadas estรฃo fora da grelha.", courtsErrTitle: "ร‰ necessรกrio um tรญtulo.", @@ -4434,6 +4431,9 @@ module.exports = { torrentTagsLabel: "Tags", torrentTagsPlaceholder: "Introduz tags separadas por vรญrgulas", torrentSizeLabel: "Tamanho", + torrentInfoHash: "Infohash", + torrentFilesLabel: "Ficheiros", + torrentMagnet: "MAGNET", torrentFromContent: "Gerar torrent", torrentFromContentMissing: "O ficheiro ainda nรฃo estรก disponรญvel neste nรณ. Tenta de novo mais tarde.", torrentDownloads: "Transferรชncias", @@ -4519,9 +4519,6 @@ module.exports = { inhabitantsPendingAccept: "Aceitar", inhabitantsPendingReject: "Rejeitar", tribeSectionGovernance: "GOVERNANร‡A", - tribePadInviteRequired: "Sem acesso ao pad. Peรงa um convite.", - tribeChatInviteRequired: "Sem acesso ao chat. Peรงa um convite.", - tribeGovernanceDesc: "Governanรงa interna desta tribo.", tribeGovernanceNoGov: "Sem governo ativo", tribeGovernanceNoGovDesc: "Esta tribo ainda nรฃo elegeu governo.", tribeGovCardTitle: "Governo Atual", @@ -4578,7 +4575,6 @@ module.exports = { logsFilterMonth: "รšLTIMO MรŠS", logsFilterWeek: "รšLTIMA SEMANA", logsFilterToday: "HOJE", - logsComments: "Comentรกrios", logsAuthorEntries: "Entradas", logsCreate: "Criar entrada", logsExport: "Exportar diรกrio", @@ -4672,6 +4668,8 @@ module.exports = { larpWillTestTitle: "Teste WILL", settingsLanDesc: "Anuncia periodicamente este peer a outras instรขncias do Oasis na mesma rede local.", settingsLanEnable: "Ativar difusรฃo LAN", + settingsConnectionsTitle: "Ligaรงรตes", + settingsConnectionsDesc: "Quantos peers este nรณ mantรฉm ligados ao mesmo tempo.", settingsLanTitle: "Difusรฃo LAN", settingsReplicationTitle: "Replicaรงรฃo", settingsReplicationDesc: "Configura o nรบmero de saltos que o teu peer segue a partir do teu prรณprio feed ao replicar conteรบdo.", diff --git a/src/client/assets/translations/oasis_ru.js b/src/client/assets/translations/oasis_ru.js index 910d3d6..f0d4a2f 100644 --- a/src/client/assets/translations/oasis_ru.js +++ b/src/client/assets/translations/oasis_ru.js @@ -211,7 +211,8 @@ module.exports = { graphosBackToMine: "โ† ะœะพั ัะตั‚ัŒ", graphosShowing: "ะŸะพะบะฐะทะฐะฝะพ", graphosYou: "ะ’ั‹", - graphosTotalNodes: "ะ’ัะตะณะพ ัƒะทะปะพะฒ", + graphosTotalNodes: "ะ’ัะตะณะพ", + onlinePlural: "ะ’ ัะตั‚ะธ", mentions: "ะฃะฟะพะผะธะฝะฐะฝะธั", mentionsDescription: [ strong("ะšะฐะถะดะพะต ัะพะพะฑั‰ะตะฝะธะต, ะณะดะต ะฒะฐั @ัƒะฟะพะผะธะฝะฐัŽั‚"), @@ -429,6 +430,7 @@ module.exports = { menuMedia: "ะ‘ะธะฑะปะธะพั‚ะตะบะฐ", menuTools: "ะ˜ะฝัั‚ั€ัƒะผะตะฝั‚ั‹", comment: "ะšะพะผะผะตะฝั‚ะฐั€ะธะน", + typeComment: "ะšะพะผะผะตะฝั‚ะฐั€ะธะน", subtopic: "ะŸะพะดั‚ะตะผะฐ", json: "JSON", unfollow: "ะžั‚ะผะตะฝะธั‚ัŒ ะฟะพะดะดะตั€ะถะบัƒ", @@ -458,9 +460,6 @@ module.exports = { publishCustomDescription: [ "ะŸะžะœะะ˜ะขะ•: ะ‘ะปะฐะณะพะดะฐั€ั ั‚ะตั…ะฝะพะปะพะณะธะธ ะฑะปะพะบั‡ะตะนะฝ, ะฟะพัะปะต ะฟัƒะฑะปะธะบะฐั†ะธะธ ะทะฐะฟะธััŒ ะฝะตะฒะพะทะผะพะถะฝะพ ะพั‚ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะธะปะธ ัƒะดะฐะปะธั‚ัŒ.", ], - commentWarning: [ - "ะŸะžะœะะ˜ะขะ•: ะ‘ะปะฐะณะพะดะฐั€ั ั‚ะตั…ะฝะพะปะพะณะธะธ ะฑะปะพะบั‡ะตะนะฝ, ะฟะพัะปะต ะฟัƒะฑะปะธะบะฐั†ะธะธ ะทะฐะฟะธััŒ ะฝะตะฒะพะทะผะพะถะฝะพ ะพั‚ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะธะปะธ ัƒะดะฐะปะธั‚ัŒ.", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -739,10 +738,6 @@ module.exports = { votesQuestionLabel:"ะ’ะพะฟั€ะพั", voteStatusOpen:"ะžะขะšะ ะซะขะž", voteStatusClosed:"ะ—ะะšะ ะซะขะž", - commentDescription: ({ parentUrl }) => [ - " ะฟั€ะพะบะพะผะผะตะฝั‚ะธั€ะพะฒะฐะป ", - a({ href: parentUrl }, " ะพะฑััƒะถะดะตะฝะธะต"), - ], commentTitle: ({ authorName }) => [`ะšะพะผะผะตะฝั‚ะฐั€ะธะน ะบ ะทะฐะฟะธัะธ @${authorName}`], subtopicDescription: ({ parentUrl }) => [ " ัะพะทะดะฐะป ะฟะพะดั‚ะตะผัƒ ะธะท ", @@ -877,6 +872,8 @@ module.exports = { bankRulesEligibility5: "PUB ะฟะปะฐั‚ะธั‚ ะฒ ัะปะตะดัƒัŽั‰ะตะผ ั†ะธะบะปะต ะธ ะฟัƒะฑะปะธะบัƒะตั‚ ะฟะตั€ะตะฒะพะด ะ‘ะ‘ะ”, ะบะพั‚ะพั€ั‹ะน ะฒั‹ ะผะพะถะตั‚ะต ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ.", bankRulesEligibility6: "ะ•ัะปะธ ะฝะตั‚ ะฐะดั€ะตัะฐ, ะปะตะฝั‚ะฐ ัะปะธัˆะบะพะผ ะผะพะปะพะดะฐ, ะผะตััั† ะพั‚ะบะปะพะฝั‘ะฝ ะธะปะธ ัƒ PUB ะฝะตั‚ ัั€ะตะดัั‚ะฒ, ะทะฐะฟั€ะพั ะฟั€ะพะฟัƒัะบะฐะตั‚ัั ะธ ะทะฐะฟะธัั‹ะฒะฐะตั‚ัั ะฒ ะถัƒั€ะฝะฐะป PUB.", copyLinkTitle: "ะกะบะพะฟะธั€ะพะฒะฐั‚ัŒ ััั‹ะปะบัƒ", + cardMenuTitle: "ะ”ั€ัƒะณะธะต ะดะตะนัั‚ะฒะธั", + shareContent: "ะŸะพะดะตะปะธั‚ัŒัั", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "ะžั‚ะบะฐะทะฐั‚ัŒัั ะพั‚ ะ‘ะ‘ะ”", @@ -910,7 +907,7 @@ module.exports = { walletNotConfigured: "ะ’ะฐัˆ ะบะพัˆะตะปั‘ะบ ECOin ะตั‰ั‘ ะฝะต ะฝะฐัั‚ั€ะพะตะฝ.", walletSettingsDescription: "ะ˜ะฝั‚ะตะณั€ะธั€ัƒะนั‚ะต Oasis ั ะฒะฐัˆะธะผ ะบะพัˆะตะปัŒะบะพะผ ECOin.", walletSettingsMobileDescription: "ะกะฒัะถะธั‚ะต Oasis ั ะฟั€ะธะปะพะถะตะฝะธะตะผ ECOin Wallet ะฝะฐ ัั‚ะพะผ ัƒัั‚ั€ะพะนัั‚ะฒะต.", - walletDownloadEcoin: "ะกะบะฐั‡ะฐั‚ัŒ ECOin!", + walletDownloadEcoin: "ะกะบะฐั‡ะฐั‚ัŒ ECOin", walletRpcUrl: "URL RPC", walletStatusMessages: { invalid_amount: "ะะตะฒะตั€ะฝะฐั ััƒะผะผะฐ", @@ -963,6 +960,7 @@ module.exports = { bookmarkUpdateButton: "ะžะฑะฝะพะฒะธั‚ัŒ", bookmarkAddFavoriteButton: "ะ’ ะธะทะฑั€ะฐะฝะฝะพะต", bookmarkRemoveFavoriteButton: "ะ˜ะท ะธะทะฑั€ะฐะฝะฝะพะณะพ", + bookmarkTitleLabel: "ะ—ะฐะณะพะปะพะฒะพะบ", bookmarkUrlLabel: "ะกัั‹ะปะบะฐ", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "ะžะฟะธัะฐะฝะธะต", @@ -1107,9 +1105,9 @@ module.exports = { allCVs: "ะ ะตะทัŽะผะต", discoverPeople: "ะะฐั…ะพะดะธั‚ะต ะถะธั‚ะตะปะตะน ะฒ ะฒะฐัˆะตะน ัะตั‚ะธ.", searchInhabitantsPlaceholder: "ะŸะพะธัะบ ะฒ ะ–ะธั‚ะตะปะธ...", - filterLocation: "ะคะ˜ะ›ะฌะขะ  ะถะธั‚ะตะปะตะน ะŸะž ะœะ•ะกะขะžะŸะžะ›ะžะ–ะ•ะะ˜ะฎ...", - filterLanguage: "ะคะ˜ะ›ะฌะขะ  ะถะธั‚ะตะปะตะน ะŸะž ะฏะ—ะซะšะฃ...", - filterSkills: "ะคะ˜ะ›ะฌะขะ  ะถะธั‚ะตะปะตะน ะŸะž ะะะ’ะซะšะะœ...", + filterLocation: "ะ˜ัะบะฐั‚ัŒ ะฟะพ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธัŽโ€ฆ", + filterLanguage: "ะ˜ัะบะฐั‚ัŒ ะฟะพ ัะทั‹ะบัƒโ€ฆ", + filterSkills: "ะ˜ัะบะฐั‚ัŒ ะฟะพ ะฝะฐะฒั‹ะบะฐะผโ€ฆ", cvSkillsCount: "ะะฐะฒั‹ะบะธ", applyFilters: "ะŸั€ะธะผะตะฝะธั‚ัŒ ั„ะธะปัŒั‚ั€ั‹", locationLabel: "ะœะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", @@ -1125,6 +1123,7 @@ module.exports = { topactivitySectionTitle: "ะะฐะธะฑะพะปัŒัˆะฐั ะฐะบั‚ะธะฒะฝะพัั‚ัŒ", "TOP INACTIVITYButton": "ะะ•ะะšะขะ˜ะ’ะะซะ•", topinactivitySectionTitle: "ะะฐะธะผะตะฝะตะต ะฐะบั‚ะธะฒะฝั‹ะต ะถะธั‚ะตะปะธ", + contactsSectionTitle: "ะšะพะฝั‚ะฐะบั‚ั‹", blockedSectionTitle: "ะ—ะฐะฑะปะพะบะธั€ะพะฒะฐะฝะฝั‹ะต", gallerySectionTitle: "ะ“ะะ›ะ•ะ ะ•ะฏ", topSectionTitle: "ะขะžะŸ", @@ -1577,9 +1576,7 @@ module.exports = { voteUnknownDate: "ะะตะธะทะฒะตัั‚ะฝะฐั ะดะฐั‚ะฐ", voteDetailSectionTitle: 'ะ”ะตั‚ะฐะปะธ ะณะพะปะพัะพะฒะฐะฝะธั', voteCommentsLabel: 'ะšะพะผะผะตะฝั‚ะฐั€ะธะธ', - voteCommentsForumButton: 'ะžั‚ะบั€ั‹ั‚ัŒ ะพะฑััƒะถะดะตะฝะธะต', - voteCommentsSectionTitle: 'ะžั‚ะบั€ั‹ั‚ัŒ ะพะฑััƒะถะดะตะฝะธะต', - voteNoCommentsYet: 'ะšะพะผะผะตะฝั‚ะฐั€ะธะตะฒ ะฟะพะบะฐ ะฝะตั‚. ะ‘ัƒะดัŒั‚ะต ะฟะตั€ะฒั‹ะผ.', + voteNoCommentsYet: 'ะšะพะผะผะตะฝั‚ะฐั€ะธะตะฒ ะฟะพะบะฐ ะฝะตั‚.', commentsGoToLast: "ะŸะตั€ะตะนั‚ะธ ะบ ะฟะพัะปะตะดะฝะตะผัƒ ะบะพะผะผะตะฝั‚ะฐั€ะธัŽ", voteNewCommentPlaceholder: 'ะะฐะฟะธัˆะธั‚ะต ะฒะฐัˆ ะบะพะผะผะตะฝั‚ะฐั€ะธะน...', voteNewCommentButton: 'ะžั‚ะฟั€ะฐะฒะธั‚ัŒ ะบะพะผะผะตะฝั‚ะฐั€ะธะน', @@ -1706,7 +1703,6 @@ module.exports = { pollVoteButton: "ะ“ะพะปะพัะพะฒะฐั‚ัŒ", pollChangeVote: "ะ˜ะทะผะตะฝะธั‚ัŒ ะณะพะปะพั", pollVoters: "ะŸั€ะพะณะพะปะพัะพะฒะฐะปะธ", - pollComments: "ะšะพะผะผะตะฝั‚ะฐั€ะธะธ", pollStatusLabel: "ะกั‚ะฐั‚ัƒั", pollStatusOpen: "ะžะขะšะ ะซะข", pollStatusClosed: "ะ—ะะšะ ะซะข", @@ -1728,9 +1724,22 @@ module.exports = { blogAllowComments: "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะบะพะผะผะตะฝั‚ะฐั€ะธะธ", blogCommentsClosed: "ะะฒั‚ะพั€ ะทะฐะบั€ั‹ะป ะบะพะผะผะตะฝั‚ะฐั€ะธะธ ะบ ัั‚ะพะผัƒ ะฑะปะพะณัƒ.", blogNoItems: "ะ‘ะปะพะณะธ ะฝะต ะฝะฐะนะดะตะฝั‹.", - viewBlog: "ะกะผะพั‚ั€ะตั‚ัŒ ะฑะปะพะณ", forumTitle: "ะคะพั€ัƒะผั‹", forumCategoryLabel: "ะšะฐั‚ะตะณะพั€ะธั", + forumCatGENERAL: "ะžะ‘ะฉะ•ะ•", + forumCatPOLITICS: "ะŸะžะ›ะ˜ะขะ˜ะšะ", + forumCatTECH: "ะขะ•ะฅะะžะ›ะžะ“ะ˜ะ˜", + forumCatSCIENCE: "ะะะฃะšะ", + forumCatMUSIC: "ะœะฃะ—ะซะšะ", + forumCatART: "ะ˜ะกะšะฃะกะกะขะ’ะž", + forumCatGAMING: "ะ˜ะ“ะ ะซ", + forumCatBOOKS: "ะšะะ˜ะ“ะ˜", + forumCatFILMS: "ะคะ˜ะ›ะฌะœะซ", + forumCatPHILOSOPHY: "ะคะ˜ะ›ะžะกะžะคะ˜ะฏ", + forumCatSOCIETY: "ะžะ‘ะฉะ•ะกะขะ’ะž", + forumCatPRIVACY: "ะŸะ ะ˜ะ’ะะขะะžะกะขะฌ", + forumCatCYBERWARFARE: "ะšะ˜ะ‘ะ•ะ ะ’ะžะ™ะะ", + forumCatSURVIVALISM: "ะ’ะซะ–ะ˜ะ’ะะะ˜ะ•", forumTitleLabel: "ะะฐะทะฒะฐะฝะธะต", forumTitlePlaceholder: "ะะฐะทะฒะฐะฝะธะต ั„ะพั€ัƒะผะฐ...", forumCreateButton: "ะกะพะทะดะฐั‚ัŒ ั„ะพั€ัƒะผ", @@ -2005,43 +2014,6 @@ module.exports = { reportsUnderReviewSectionTitle: "ะžั‚ั‡ั‘ั‚ั‹ ะฝะฐ ั€ะฐััะผะพั‚ั€ะตะฝะธะธ", reportsResolvedSectionTitle: "ะ ะตัˆั‘ะฝะฝั‹ะต ะพั‚ั‡ั‘ั‚ั‹", reportsInvalidSectionTitle: "ะะตะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝั‹ะต ะพั‚ั‡ั‘ั‚ั‹", - reportsBugTemplateTitle: "ะ”ะตั‚ะฐะปะธ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั (ะžัˆะธะฑะบะธ)", - reportsFeatureTemplateTitle: "ะ”ะตั‚ะฐะปะธ (ะคัƒะฝะบั†ะธะธ)", - reportsAbuseTemplateTitle: "ะ”ะตั‚ะฐะปะธ (ะ—ะปะพัƒะฟะพั‚ั€ะตะฑะปะตะฝะธั)", - reportsContentTemplateTitle: "ะ”ะตั‚ะฐะปะธ (ะŸั€ะพะฑะปะตะผั‹ ั ะบะพะฝั‚ะตะฝั‚ะพะผ)", - reportsStepsToReproduceLabel: "ะจะฐะณะธ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั", - reportsStepsToReproducePlaceholder: "1) ...\n2) ...\n3) ...", - reportsExpectedBehaviorLabel: "ะžะถะธะดะฐะตะผั‹ะน ั€ะตะทัƒะปัŒั‚ะฐั‚", - reportsExpectedBehaviorPlaceholder: "ะงั‚ะพ ะดะพะปะถะฝะพ ะฟั€ะพะธะทะพะนั‚ะธ?", - reportsActualBehaviorLabel: "ะคะฐะบั‚ะธั‡ะตัะบะธะน ั€ะตะทัƒะปัŒั‚ะฐั‚", - reportsActualBehaviorPlaceholder: "ะงั‚ะพ ะฟั€ะพะธะทะพัˆะปะพ ะฝะฐ ัะฐะผะพะผ ะดะตะปะต?", - reportsEnvironmentLabel: "ะกั€ะตะดะฐ", - reportsEnvironmentPlaceholder: "ะ’ะตั€ัะธั, ัƒัั‚ั€ะพะนัั‚ะฒะพ, ะžะก, ะฑั€ะฐัƒะทะตั€, ะฝะฐัั‚ั€ะพะนะบะธ, ะถัƒั€ะฝะฐะปั‹ ะธ ั‚.ะด.", - reportsReproduceRateLabel: "ะงะฐัั‚ะพั‚ะฐ ะฒะพัะฟั€ะพะธะทะฒะตะดะตะฝะธั", - reportsReproduceRateAlways: "ะ’ัะตะณะดะฐ", - reportsReproduceRateOften: "ะงะฐัั‚ะพ", - reportsReproduceRateSometimes: "ะ˜ะฝะพะณะดะฐ", - reportsReproduceRateRarely: "ะ ะตะดะบะพ", - reportsReproduceRateUnable: "ะะตะฒะพะทะผะพะถะฝะพ ะฒะพัะฟั€ะพะธะทะฒะตัั‚ะธ", - reportsReproduceRateUnknown: "ะะต ัƒะบะฐะทะฐะฝะพ", - reportsProblemStatementLabel: "ะŸั€ะพะฑะปะตะผะฐ / ะฟะพั‚ั€ะตะฑะฝะพัั‚ัŒ", - reportsProblemStatementPlaceholder: "ะšะฐะบัƒัŽ ะฟั€ะพะฑะปะตะผัƒ ั€ะตัˆะฐะตั‚ ัั‚ะฐ ั„ัƒะฝะบั†ะธั?", - reportsUserStoryLabel: "ะ˜ัั‚ะพั€ะธั ะถะธั‚ะตะปั", - reportsUserStoryPlaceholder: "ะšะฐะบ <ะถะธั‚ะตะปัŒ>, ั ั…ะพั‡ัƒ <ะดะตะนัั‚ะฒะธะต>, ั‡ั‚ะพะฑั‹ <ะฟะพะปัŒะทะฐ>.", - reportsAcceptanceCriteriaLabel: "ะšั€ะธั‚ะตั€ะธะธ ะฟั€ะธั‘ะผะบะธ", - reportsAcceptanceCriteriaPlaceholder: "- ะ”ะฐะฝะพ...\n- ะšะพะณะดะฐ...\n- ะขะพะณะดะฐ...", - reportsWhatHappenedLabel: "ะงั‚ะพ ะฟั€ะพะธะทะพัˆะปะพ?", - reportsWhatHappenedPlaceholder: "ะžะฟะธัˆะธั‚ะต ะธะฝั†ะธะดะตะฝั‚ ั ะบะพะฝั‚ะตะบัั‚ะพะผ ะธ ะฟั€ะธะผะตั€ะฝั‹ะผะธ ะดะฐั‚ะฐะผะธ.", - reportsReportedUserLabel: "ะกะพะพะฑั‰ะฐะตะผั‹ะน ะถะธั‚ะตะปัŒ / ััƒั‰ะฝะพัั‚ัŒ", - reportsReportedUserPlaceholder: "@ะถะธั‚ะตะปัŒ, ะปะตะฝั‚ะฐ, ID, ััั‹ะปะบะฐ ะธ ั‚.ะด.", - reportsEvidenceLinksLabel: "ะ”ะพะบะฐะทะฐั‚ะตะปัŒัั‚ะฒะฐ / ััั‹ะปะบะธ", - reportsEvidenceLinksPlaceholder: "ะ’ัั‚ะฐะฒัŒั‚ะต ััั‹ะปะบะธ, ID ัะพะพะฑั‰ะตะฝะธะน, ัะบั€ะธะฝัˆะพั‚ั‹ (ะตัะปะธ ะฟั€ะธะผะตะฝะธะผะพ) ะธ ั‚.ะด.", - reportsContentLocationLabel: "ะ“ะดะต ะฝะฐั…ะพะดะธั‚ัั ะบะพะฝั‚ะตะฝั‚", - reportsContentLocationPlaceholder: "ะกัั‹ะปะบะฐ, ID, ะบะฐะฝะฐะป, ั‚ั€ะตะด, ะฐะฒั‚ะพั€ ะธ ั‚.ะด.", - reportsWhyInappropriateLabel: "ะŸะพั‡ะตะผัƒ ัั‚ะพ ะฝะตัƒะผะตัั‚ะฝะพ", - reportsWhyInappropriatePlaceholder: "ะžะฑัŠััะฝะธั‚ะต ะฟั€ะธั‡ะธะฝัƒ ะธ ะฟะพัะปะตะดัั‚ะฒะธั.", - reportsRequestedActionLabel: "ะ—ะฐะฟั€ะฐัˆะธะฒะฐะตะผะพะต ะดะตะนัั‚ะฒะธะต", - reportsRequestedActionPlaceholder: "ะฃะดะฐะปะธั‚ัŒ, ัะบั€ั‹ั‚ัŒ, ะฟะพะผะตั‚ะธั‚ัŒ, ะฟั€ะตะดัƒะฟั€ะตะดะธั‚ัŒ ะธ ั‚.ะด.", tribesTitle: "ะŸะปะตะผะตะฝะฐ", tribeFilterAll: "ะ’ะกะ•", tribeFilterRecent: "ะะ•ะ”ะะ’ะะ˜ะ•", @@ -2282,6 +2254,7 @@ module.exports = { voteWellResearched: "ะฅะพั€ะพัˆะพ ะธััะปะตะดะพะฒะฐะฝะฝะพะต", voteNeedsSources: "ะัƒะถะฝั‹ ะธัั‚ะพั‡ะฝะธะบะธ", voteWrong: "ะะตะฟั€ะฐะฒะธะปัŒะฝะพะต", + voteOutdated: "ะฃัั‚ะฐั€ะตะปะพ", voteLowQuality: "ะะธะทะบะพะต ะบะฐั‡ะตัั‚ะฒะพ", voteLove: "ะ›ัŽะฑะปัŽ", voteClear: "ะฏัะฝะพะต", @@ -2515,7 +2488,6 @@ module.exports = { shopNoProducts: "ะขะพะฒะฐั€ะพะฒ ะฟะพะบะฐ ะฝะตั‚.", shopOutOfStock: "ะะตั‚ ะฒ ะฝะฐะปะธั‡ะธะธ", shopBuy: "ะšัƒะฟะธั‚ัŒ", - shopBuyEncryptedNote: "ะ’ะฐัˆะธ ะดะฐะฝะฝั‹ะต ะดะพัั‚ะฐะฒะบะธ ะพั‚ะฟั€ะฐะฒะปััŽั‚ัั ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝะพ ะธ ะฒะธะดะฝั‹ ั‚ะพะปัŒะบะพ ะฒะปะฐะดะตะปัŒั†ัƒ ะผะฐะณะฐะทะธะฝะฐ.", shopBuyDeliveryAddress: "ะะดั€ะตั ะดะพัั‚ะฐะฒะบะธ", shopBuyContact: "ะšะพะฝั‚ะฐะบั‚", shopBuyContactPlaceholder: "email, ั‚ะตะปะตั„ะพะฝ ะธ ั‚.ะด.", @@ -3027,6 +2999,16 @@ module.exports = { industryDescriptionLabel: "ะžะฟะธัะฐะฝะธะต", industryDescriptionPlaceholder: "ะงั‚ะพ ะฟั€ะพะธะทะฒะพะดะธั‚ ัั‚ะฐ ั„ะฐะฑั€ะธะบะฐ?", industrySector: "ะกะตะบั‚ะพั€", + industrySector_software: "ะŸั€ะพะณั€ะฐะผะผั‹", + industrySector_hardware: "ะžะฑะพั€ัƒะดะพะฒะฐะฝะธะต", + industrySector_agriculture: "ะกะตะปัŒัะบะพะต ั…ะพะทัะนัั‚ะฒะพ", + industrySector_textile: "ะขะตะบัั‚ะธะปัŒ", + industrySector_energy: "ะญะฝะตั€ะณะตั‚ะธะบะฐ", + industrySector_food: "ะŸั€ะพะดะพะฒะพะปัŒัั‚ะฒะธะต", + industrySector_construction: "ะกั‚ั€ะพะธั‚ะตะปัŒัั‚ะฒะพ", + industrySector_media: "ะœะตะดะธะฐ", + industrySector_services: "ะฃัะปัƒะณะธ", + industrySector_other: "ะ”ั€ัƒะณะพะต", industryMembershipPolicy: "ะŸะพะปะธั‚ะธะบะฐ ั‡ะปะตะฝัั‚ะฒะฐ", industryQuorum: "ะšะฒะพั€ัƒะผ", industryMajority: "ะ‘ะพะปัŒัˆะธะฝัั‚ะฒะพ", @@ -3228,6 +3210,18 @@ module.exports = { modulesFavoritesDescription: "ะœะพะดัƒะปัŒ ะดะปั ัƒะฟั€ะฐะฒะปะตะฝะธั ะธะทะฑั€ะฐะฝะฝั‹ะผ ะบะพะฝั‚ะตะฝั‚ะพะผ.", fileTooLargeTitle: "ะคะฐะนะป ัะปะธัˆะบะพะผ ะฑะพะปัŒัˆะพะน", fileTooLargeMessage: "ะคะฐะนะป ะฟั€ะตะฒั‹ัˆะฐะตั‚ ะผะฐะบัะธะผะฐะปัŒะฝะพ ะดะพะฟัƒัั‚ะธะผั‹ะน ั€ะฐะทะผะตั€ (50 ะœะ‘). ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒั‹ะฑะตั€ะธั‚ะต ั„ะฐะนะป ะผะตะฝัŒัˆะตะณะพ ั€ะฐะทะผะตั€ะฐ.", + fileWrongType: "ะญั‚ะพั‚ ั„ะฐะนะป ะฝะต ะฟะพะดั…ะพะดะธั‚ ะดะปั ัั‚ะพะณะพ ั€ะฐะทะดะตะปะฐ ะฟะพ ั‚ะธะฟัƒ.", + mediaLockedAfterOpinions: "ะฃ ัั‚ะพะณะพ ัƒะถะต ะตัั‚ัŒ ะผะฝะตะฝะธั: ะพัั‚ะฐะปัŒะฝะพะต ะผะพะถะฝะพ ะฟั€ะฐะฒะธั‚ัŒ, ะฝะพ ะฝะต ั‚ะพ, ั‡ั‚ะพ ะพั†ะตะฝะธะฒะฐะปะธ.", + pdfPrevPage: "ะŸั€ะตะดั‹ะดัƒั‰ะฐั ัั‚ั€ะฐะฝะธั†ะฐ", + pdfNextPage: "ะกะปะตะดัƒัŽั‰ะฐั ัั‚ั€ะฐะฝะธั†ะฐ", + pdfPage: "ะกั‚ั€ะฐะฝะธั†ะฐ", + pdfZoomIn: "ะฃะฒะตะปะธั‡ะธั‚ัŒ", + pdfZoomOut: "ะฃะผะตะฝัŒัˆะธั‚ัŒ", + pdfFullscreen: "ะŸะพะปะฝั‹ะน ัะบั€ะฐะฝ", + pdfClose: "ะ—ะฐะบั€ั‹ั‚ัŒ", + pdfRotate: "ะŸะพะฒะตั€ะฝัƒั‚ัŒ", + contentNotAvailableYet: "ะญั‚ะพั‚ ะบะพะฝั‚ะตะฝั‚ ะฟะพะบะฐ ะฝะตะดะพัั‚ัƒะฟะตะฝ.", + pdfRetry: "ะŸะพะฒั‚ะพั€ะธั‚ัŒ", goBack: "ะะฐะทะฐะด", errorDismiss: "ะžะš", errorPageTitle: "ะงั‚ะพ-ั‚ะพ ะฟะพัˆะปะพ ะฝะต ั‚ะฐะบ", @@ -3270,9 +3264,7 @@ module.exports = { uploadMedia: "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะผะตะดะธะฐ (ะผะฐะบั: 75MB)", feedOpenDiscussion: "ะžั‚ะบั€ั‹ั‚ัŒ ะพะฑััƒะถะดะตะฝะธะต", feedDetailTitle: "ะ›ะตะฝั‚ะฐ", - feedPostComment: "ะžะฟัƒะฑะปะธะบะพะฒะฐั‚ัŒ ะบะพะผะผะตะฝั‚ะฐั€ะธะน", courtsRespondentInvalid: "ะ”ะพะปะถะตะฝ ะฑั‹ั‚ัŒ ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝั‹ะผ SSB ID (@...ed25519)", - noComments: "ะŸะพะบะฐ ะฝะตั‚ ะบะพะผะผะตะฝั‚ะฐั€ะธะตะฒ", bankAllocId: "ID ั€ะฐัะฟั€ะตะดะตะปะตะฝะธั", pmFromLabel: "ะžั‚:", pmToLabel: "ะšะพะผัƒ:", @@ -3581,6 +3573,8 @@ module.exports = { podcastTagsPlaceholder: "ะ’ะฒะตะดะธั‚ะต ั‚ะตะณะธ ั‡ะตั€ะตะท ะทะฐะฟัั‚ัƒัŽ", podcastFilterAll: "ะ’ะกะ•", podcastFilterMine: "ะœะžะ˜", + podcastFilterFavorites: "ะ˜ะ—ะ‘ะ ะะะะžะ•", + podcastEpisodeNumberLabel: "ะะพะผะตั€ ะฒั‹ะฟัƒัะบะฐ", podcastFilterRecent: "ะะ•ะ”ะะ’ะะ˜ะ•", podcastFilterTop: "ะขะžะŸ", podcastFilterViewers: "ะกะ›ะฃะจะะขะ•ะ›ะ˜", @@ -3799,7 +3793,6 @@ module.exports = { wikiBackToPage: "ะะฐะทะฐะด ะบ ัั‚ั€ะฐะฝะธั†ะต", wikiLinkedChip: "ะกะฒัะทะฐะฝะฐ", wikiRestoredSummary: "ะ’ะพััั‚ะฐะฝะพะฒะปะตะฝะฝะฐั ะฒะตั€ัะธั", - wikiExportPdf: "ะกะพะทะดะฐั‚ัŒ ัั‚ั€ะฐะฝะธั†ัƒ", wikiPreview: "ะŸั€ะตะดะฟั€ะพัะผะพั‚ั€", tribeSectionWiki: "ะ’ะ˜ะšะ˜", tribeWikiCreate: "ะกะพะทะดะฐั‚ัŒ ะฒะธะบะธ", @@ -3867,6 +3860,9 @@ module.exports = { roomRecStop: "ะžัั‚ะฐะฝะพะฒะธั‚ัŒ ะทะฐะฟะธััŒ", roomStreamFile: "ะขั€ะฐะฝัะปะธั€ะพะฒะฐั‚ัŒ", roomStreamStop: "ะžัั‚ะฐะฝะพะฒะธั‚ัŒ", + roomScreen: "ะญะบั€ะฐะฝ", + roomScreenSharing: "ะ”ะตะผะพะฝัั‚ั€ะฐั†ะธั ัะบั€ะฐะฝะฐ", + roomScreenWarning: "ะ’ัะต ะฒ ะบะพะผะฝะฐั‚ะต ะฑัƒะดัƒั‚ ะฒะธะดะตั‚ัŒ ะฒะฐัˆ ัะบั€ะฐะฝ ั†ะตะปะธะบะพะผ, ะฟะพะบะฐ ะฒั‹ ะฝะต ะพัั‚ะฐะฝะพะฒะธั‚ะต.", roomStreaming: "ะ˜ะดั‘ั‚ ั‚ั€ะฐะฝัะปัั†ะธั", liveFullscreen: "ะ’ะพ ะฒะตััŒ ัะบั€ะฐะฝ", roomStreamPickTitle: "ะ’ั‹ะฑะตั€ะธั‚ะต ะฒะธะดะตะพ", @@ -3924,6 +3920,7 @@ module.exports = { inviteAlreadyMember: "ะ’ั‹ ัƒะถะต ัƒั‡ะฐัั‚ะฒัƒะตั‚ะต. ะžั‚ะบั€ะพะนั‚ะต ะตะณะพ ะฒ ัะพะพั‚ะฒะตั‚ัั‚ะฒัƒัŽั‰ะตะผ ะผะพะดัƒะปะต.", actionFailed: "ะะต ัƒะดะฐะปะพััŒ ัั‚ะพ ัะดะตะปะฐั‚ัŒ. ะŸะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ั‘ ั€ะฐะท.", publishTooShort: "ะ’ะฐัˆะฐ ะฟัƒะฑะปะธะบะฐั†ะธั ัะปะธัˆะบะพะผ ะบะพั€ะพั‚ะบะฐั.", + publishEmpty: "ะะฐะฟะธัˆะธั‚ะต ั‡ั‚ะพ-ะฝะธะฑัƒะดัŒ ะธะปะธ ะฟั€ะธะบั€ะตะฟะธั‚ะต ั„ะฐะนะป.", walletSendFailed: "ะะต ัƒะดะฐะปะพััŒ ะพั‚ะฟั€ะฐะฒะธั‚ัŒ ะฟะปะฐั‚ั‘ะถ: {reason}", pixeliaCoordsError: "ะญั‚ะธ ะบะพะพั€ะดะธะฝะฐั‚ั‹ ะฝะฐั…ะพะดัั‚ัั ะทะฐ ะฟั€ะตะดะตะปะฐะผะธ ัะตั‚ะบะธ.", courtsErrTitle: "ะัƒะถะตะฝ ะทะฐะณะพะปะพะฒะพะบ.", @@ -4435,6 +4432,9 @@ module.exports = { torrentTagsLabel: "ะขะตะณะธ", torrentTagsPlaceholder: "ะ’ะฒะตะดะธั‚ะต ั‚ะตะณะธ ั‡ะตั€ะตะท ะทะฐะฟัั‚ัƒัŽ", torrentSizeLabel: "ะ ะฐะทะผะตั€", + torrentInfoHash: "ะ˜ะฝั„ะพั…ะตัˆ", + torrentFilesLabel: "ะคะฐะนะปั‹", + torrentMagnet: "MAGNET", torrentFromContent: "ะกะพะทะดะฐั‚ัŒ ั‚ะพั€ั€ะตะฝั‚", torrentFromContentMissing: "ะคะฐะนะป ะตั‰ั‘ ะฝะตะดะพัั‚ัƒะฟะตะฝ ะฝะฐ ัั‚ะพะผ ัƒะทะปะต. ะŸะพะฟั€ะพะฑัƒะนั‚ะต ะฟะพะทะถะต.", torrentDownloads: "ะ—ะฐะณั€ัƒะทะบะธ", @@ -4520,9 +4520,6 @@ module.exports = { inhabitantsPendingAccept: "ะŸั€ะธะฝัั‚ัŒ", inhabitantsPendingReject: "ะžั‚ะบะปะพะฝะธั‚ัŒ", tribeSectionGovernance: "ะฃะŸะ ะะ’ะ›ะ•ะะ˜ะ•", - tribePadInviteRequired: "ะะตั‚ ะดะพัั‚ัƒะฟะฐ ะบ ะฟะฐะดัƒ. ะ—ะฐะฟั€ะพัะธั‚ะต ะฟั€ะธะณะปะฐัˆะตะฝะธะต.", - tribeChatInviteRequired: "ะะตั‚ ะดะพัั‚ัƒะฟะฐ ะบ ั‡ะฐั‚ัƒ. ะ—ะฐะฟั€ะพัะธั‚ะต ะฟั€ะธะณะปะฐัˆะตะฝะธะต.", - tribeGovernanceDesc: "ะ’ะฝัƒั‚ั€ะตะฝะฝะตะต ัƒะฟั€ะฐะฒะปะตะฝะธะต ัั‚ะธะผ ะฟะปะตะผะตะฝะตะผ.", tribeGovernanceNoGov: "ะะตั‚ ะฐะบั‚ะธะฒะฝะพะณะพ ะฟั€ะฐะฒะธั‚ะตะปัŒัั‚ะฒะฐ", tribeGovernanceNoGovDesc: "ะญั‚ะพ ะฟะปะตะผั ะตั‰ั‘ ะฝะต ะฒั‹ะฑั€ะฐะปะพ ะฟั€ะฐะฒะธั‚ะตะปัŒัั‚ะฒะพ.", tribeGovCardTitle: "ะขะตะบัƒั‰ะตะต ะฟั€ะฐะฒะธั‚ะตะปัŒัั‚ะฒะพ", @@ -4579,7 +4576,6 @@ module.exports = { logsFilterMonth: "ะœะ•ะกะฏะฆ", logsFilterWeek: "ะะ•ะ”ะ•ะ›ะฏ", logsFilterToday: "ะกะ•ะ“ะžะ”ะะฏ", - logsComments: "ะšะพะผะผะตะฝั‚ะฐั€ะธะธ", logsAuthorEntries: "ะ—ะฐะฟะธัะธ", logsCreate: "ะกะพะทะดะฐั‚ัŒ ะทะฐะฟะธััŒ", logsExport: "ะญะบัะฟะพั€ั‚ ะถัƒั€ะฝะฐะปะฐ", @@ -4735,6 +4731,8 @@ module.exports = { larpWillTestTitle: "WILL-ั‚ะตัั‚", settingsLanDesc: "ะŸะตั€ะธะพะดะธั‡ะตัะบะธ ะฐะฝะพะฝัะธั€ะพะฒะฐั‚ัŒ ัั‚ะพั‚ ัƒะทะตะป ะดั€ัƒะณะธะผ ัะบะทะตะผะฟะปัั€ะฐะผ Oasis ะฒ ั‚ะพะน ะถะต ะปะพะบะฐะปัŒะฝะพะน ัะตั‚ะธ.", settingsLanEnable: "ะ’ะบะปัŽั‡ะธั‚ัŒ LAN-ะฒะตั‰ะฐะฝะธะต", + settingsConnectionsTitle: "ะŸะพะดะบะปัŽั‡ะตะฝะธั", + settingsConnectionsDesc: "ะกะบะพะปัŒะบะพ ะฟะธั€ะพะฒ ัั‚ะพั‚ ัƒะทะตะป ะดะตั€ะถะธั‚ ะฟะพะดะบะปัŽั‡ั‘ะฝะฝั‹ะผะธ ะพะดะฝะพะฒั€ะตะผะตะฝะฝะพ.", settingsLanTitle: "LAN-ะฒะตั‰ะฐะฝะธะต", settingsReplicationTitle: "ะ ะตะฟะปะธะบะฐั†ะธั", settingsReplicationDesc: "ะะฐัั‚ั€ะพะนั‚ะต ะบะพะปะธั‡ะตัั‚ะฒะพ ะฟะตั€ะตั…ะพะดะพะฒ, ะบะพั‚ะพั€ั‹ะต ะฒะฐัˆ ัƒะทะตะป ะดะตะปะฐะตั‚ ะพั‚ ะฒะฐัˆะตะณะพ ัะพะฑัั‚ะฒะตะฝะฝะพะณะพ ะบะฐะฝะฐะปะฐ ะฟั€ะธ ั€ะตะฟะปะธะบะฐั†ะธะธ ัะพะดะตั€ะถะธะผะพะณะพ.", diff --git a/src/client/assets/translations/oasis_zh.js b/src/client/assets/translations/oasis_zh.js index e030108..12a38a7 100644 --- a/src/client/assets/translations/oasis_zh.js +++ b/src/client/assets/translations/oasis_zh.js @@ -210,7 +210,8 @@ module.exports = { graphosBackToMine: "โ† ๆˆ‘็š„็ฝ‘็ปœ", graphosShowing: "ๆ˜พ็คบ", graphosYou: "ไฝ ", - graphosTotalNodes: "่Š‚็‚นๆ€ปๆ•ฐ", + graphosTotalNodes: "ๆ€ป่ฎก", + onlinePlural: "ๅœจ็บฟ", mentions: "ๆๅŠ", mentionsDescription: [ strong("ๆฏไธ€ๆก @ๆๅˆฐไฝ ็š„ๆถˆๆฏ"), @@ -420,6 +421,7 @@ module.exports = { menuMedia: "่ต„ๆ–™ๅบ“", menuTools: "ๅทฅๅ…ท", comment: "่ฏ„่ฎบ", + typeComment: "่ฏ„่ฎบ", subtopic: "ๅญ่ฏ้ข˜", json: "JSON", unfollow: "ๅ–ๆถˆๆ”ฏๆŒ", @@ -449,9 +451,6 @@ module.exports = { publishCustomDescription: [ "ๆ้†’๏ผš็”ฑไบŽๅŒบๅ—้“พๆŠ€ๆœฏ๏ผŒๅธ–ๅญไธ€ๆ—ฆๅ‘ๅธƒๅฐฑๆ— ๆณ•็ผ–่พ‘ๆˆ–ๅˆ ้™คใ€‚", ], - commentWarning: [ - "ๆ้†’๏ผš็”ฑไบŽๅŒบๅ—้“พๆŠ€ๆœฏ๏ผŒๅธ–ๅญไธ€ๆ—ฆๅ‘ๅธƒๅฐฑๆ— ๆณ•็ผ–่พ‘ๆˆ–ๅˆ ้™คใ€‚", - ], commentLabel: ({ publicOrPrivate, markdownUrl }) => [ ], publishLabel: ({ markdownUrl, linkTarget }) => [ @@ -730,10 +729,6 @@ module.exports = { votesQuestionLabel:"้—ฎ้ข˜", voteStatusOpen:"่ฟ›่กŒไธญ", voteStatusClosed:"ๅทฒๅ…ณ้—ญ", - commentDescription: ({ parentUrl }) => [ - " ่ฏ„่ฎบไบ† ", - a({ href: parentUrl }, " ่ฎจ่ฎบไธฒ"), - ], commentTitle: ({ authorName }) => [`ๅฏน @${authorName} ๅธ–ๅญ็š„่ฏ„่ฎบ`], subtopicDescription: ({ parentUrl }) => [ " ไปŽ ", @@ -869,6 +864,8 @@ module.exports = { bankRulesEligibility5: "PUB ๅœจไธ‹ไธ€ๅ‘จๆœŸๆ”ฏไป˜ๅนถๅ‘ๅธƒไธ€็ฌ”ๅฏไพ›็กฎ่ฎค็š„ UBI ่ฝฌ่ดฆใ€‚", bankRulesEligibility6: "่‹ฅ็ผบๅฐ‘ๅœฐๅ€ใ€feed ๅคชๆ–ฐใ€ๅทฒๆ‹’็ปๅฝ“ๆœˆๆˆ– PUB ๆ— ่ต„้‡‘๏ผŒ้ข†ๅ–ๅฐ†่ขซ่ทณ่ฟ‡ๅนถ่ฎฐๅฝ•ๅœจ PUB ไธŠใ€‚", copyLinkTitle: "ๅคๅˆถ้“พๆŽฅ", + cardMenuTitle: "ๆ›ดๅคšๆ“ไฝœ", + shareContent: "ๅˆ†ไบซ", linkBoxOasis: "Oasis", linkBoxClearnet: "Clearnet", bankRefuseUBI: "ๆ‹’็ป UBI", @@ -902,7 +899,7 @@ module.exports = { walletNotConfigured: "ไฝ ็š„ ECOin ้’ฑๅŒ…ๅฐšๆœช้…็ฝฎใ€‚", walletSettingsDescription: "ๅฐ† Oasis ไธŽไฝ ็š„ ECOin ้’ฑๅŒ…้›†ๆˆใ€‚", walletSettingsMobileDescription: "ๅฐ† Oasis ไธŽๆญค่ฎพๅค‡ไธŠ็š„ ECOin Wallet ๅบ”็”จๅ…ณ่”ใ€‚", - walletDownloadEcoin: "ไธ‹่ฝฝ ECOin๏ผ", + walletDownloadEcoin: "ไธ‹่ฝฝ ECOin", walletRpcUrl: "RPC ๅœฐๅ€", walletStatusMessages: { invalid_amount: "ๆ— ๆ•ˆ้‡‘้ข", @@ -955,6 +952,7 @@ module.exports = { bookmarkUpdateButton: "ๆ›ดๆ–ฐ", bookmarkAddFavoriteButton: "ๆทปๅŠ ๆ”ถ่—", bookmarkRemoveFavoriteButton: "ๅ–ๆถˆๆ”ถ่—", + bookmarkTitleLabel: "ๆ ‡้ข˜", bookmarkUrlLabel: "้“พๆŽฅ", bookmarkUrlPlaceholder: "https://solarnethub.com", bookmarkDescriptionLabel: "ๆ่ฟฐ", @@ -1099,9 +1097,9 @@ module.exports = { allCVs: "็ฎ€ๅކ", discoverPeople: "ๅ‘็Žฐไฝ ็ฝ‘็ปœไธญ็š„ๅฑ…ๆฐ‘ใ€‚", searchInhabitantsPlaceholder: "ๅœจๅฑ…ๆฐ‘ไธญๆœ็ดข...", - filterLocation: "ๆŒ‰ไฝ็ฝฎ็ญ›้€‰ๅฑ…ๆฐ‘...", - filterLanguage: "ๆŒ‰่ฏญ่จ€็ญ›้€‰ๅฑ…ๆฐ‘...", - filterSkills: "ๆŒ‰ๆŠ€่ƒฝ็ญ›้€‰ๅฑ…ๆฐ‘...", + filterLocation: "ๆŒ‰ไฝ็ฝฎๆœ็ดขโ€ฆ", + filterLanguage: "ๆŒ‰่ฏญ่จ€ๆœ็ดขโ€ฆ", + filterSkills: "ๆŒ‰ๆŠ€่ƒฝๆœ็ดขโ€ฆ", cvSkillsCount: "ๆŠ€่ƒฝ", applyFilters: "ๅบ”็”จ็ญ›้€‰", locationLabel: "ไฝ็ฝฎ", @@ -1117,6 +1115,7 @@ module.exports = { topactivitySectionTitle: "ๆœ€้ซ˜ๆดป่ทƒๅบฆ", "TOP INACTIVITYButton": "ๆœ€ไธๆดป่ทƒ", topinactivitySectionTitle: "ๆœ€ไธๆดป่ทƒ็š„ๅฑ…ๆฐ‘", + contactsSectionTitle: "่”็ณปไบบ", blockedSectionTitle: "ๅทฒๅฑ่”ฝ", gallerySectionTitle: "็”ปๅปŠ", topSectionTitle: "ๆŽ’่กŒ", @@ -1570,9 +1569,7 @@ module.exports = { voteUnknownDate: "ๆœช็Ÿฅๆ—ฅๆœŸ", voteDetailSectionTitle: 'ๆŠ•็ฅจ่ฏฆๆƒ…', voteCommentsLabel: '่ฏ„่ฎบ', - voteCommentsForumButton: 'ๅ…ฌๅผ€่ฎจ่ฎบ', - voteCommentsSectionTitle: 'ๅ…ฌๅผ€่ฎจ่ฎบ', - voteNoCommentsYet: '่ฟ˜ๆฒกๆœ‰่ฏ„่ฎบใ€‚ๆˆไธบ็ฌฌไธ€ไธชๅ›žๅค็š„ไบบใ€‚', + voteNoCommentsYet: '่ฟ˜ๆฒกๆœ‰่ฏ„่ฎบใ€‚', commentsGoToLast: "่ทณๅˆฐๆœ€ๅŽไธ€ๆก่ฏ„่ฎบ", voteNewCommentPlaceholder: 'ๅœจๆญคๅ†™ไธ‹ไฝ ็š„่ฏ„่ฎบโ€ฆ', voteNewCommentButton: 'ๅ‘่กจ่ฏ„่ฎบ', @@ -1699,7 +1696,6 @@ module.exports = { pollVoteButton: "ๆŠ•็ฅจ", pollChangeVote: "ไฟฎๆ”นๆŠ•็ฅจ", pollVoters: "ๆŠ•็ฅจไบบ", - pollComments: "่ฏ„่ฎบ", pollStatusLabel: "็Šถๆ€", pollStatusOpen: "่ฟ›่กŒไธญ", pollStatusClosed: "ๅทฒ็ป“ๆŸ", @@ -1721,9 +1717,22 @@ module.exports = { blogAllowComments: "ๅ…่ฎธ่ฏ„่ฎบ", blogCommentsClosed: "ไฝœ่€…ๅทฒๅ…ณ้—ญๆญคๅšๅฎข็š„่ฏ„่ฎบใ€‚", blogNoItems: "ๆœชๆ‰พๅˆฐๅšๅฎขใ€‚", - viewBlog: "ๆŸฅ็œ‹ๅšๅฎข", forumTitle: "่ฎบๅ›", forumCategoryLabel: "็ฑปๅˆซ", + forumCatGENERAL: "็ปผๅˆ", + forumCatPOLITICS: "ๆ”ฟๆฒป", + forumCatTECH: "็ง‘ๆŠ€", + forumCatSCIENCE: "็ง‘ๅญฆ", + forumCatMUSIC: "้Ÿณไน", + forumCatART: "่‰บๆœฏ", + forumCatGAMING: "ๆธธๆˆ", + forumCatBOOKS: "ไนฆ็ฑ", + forumCatFILMS: "็”ตๅฝฑ", + forumCatPHILOSOPHY: "ๅ“ฒๅญฆ", + forumCatSOCIETY: "็คพไผš", + forumCatPRIVACY: "้š็ง", + forumCatCYBERWARFARE: "็ฝ‘็ปœๆˆ˜", + forumCatSURVIVALISM: "็”Ÿๅญ˜", forumTitleLabel: "ๆ ‡้ข˜", forumTitlePlaceholder: "่ฎบๅ›ๆ ‡้ข˜...", forumCreateButton: "ๅˆ›ๅปบ่ฎบๅ›", @@ -1992,43 +2001,6 @@ module.exports = { reportsUnderReviewSectionTitle: "ๅฎกๆŸฅไธญ็š„ๆŠฅๅ‘Š", reportsResolvedSectionTitle: "ๅทฒ่งฃๅ†ณ็š„ๆŠฅๅ‘Š", reportsInvalidSectionTitle: "ๆ— ๆ•ˆๆŠฅๅ‘Š", - reportsBugTemplateTitle: 'ๅค็Žฐ่ฏฆๆƒ…๏ผˆๆผๆดž๏ผ‰', - reportsFeatureTemplateTitle: '่ฏฆๆƒ…๏ผˆๅŠŸ่ƒฝ๏ผ‰', - reportsAbuseTemplateTitle: '่ฏฆๆƒ…๏ผˆๆปฅ็”จ๏ผ‰', - reportsContentTemplateTitle: '่ฏฆๆƒ…๏ผˆๅ†…ๅฎน้—ฎ้ข˜๏ผ‰', - reportsStepsToReproduceLabel: 'ๅค็Žฐๆญฅ้ชค', - reportsStepsToReproducePlaceholder: '1) ...\n2) ...\n3) ...', - reportsExpectedBehaviorLabel: '้ข„ๆœŸ็ป“ๆžœ', - reportsExpectedBehaviorPlaceholder: 'ๅบ”่ฏฅๅ‘็”Ÿไป€ไนˆ๏ผŸ', - reportsActualBehaviorLabel: 'ๅฎž้™…็ป“ๆžœ', - reportsActualBehaviorPlaceholder: 'ๅฎž้™…ๅ‘็”Ÿไบ†ไป€ไนˆ๏ผŸ', - reportsEnvironmentLabel: '็Žฏๅขƒ', - reportsEnvironmentPlaceholder: '็‰ˆๆœฌใ€่ฎพๅค‡ใ€ๆ“ไฝœ็ณป็ปŸใ€ๆต่งˆๅ™จใ€่ฎพ็ฝฎใ€ๆ—ฅๅฟ—็ญ‰ใ€‚', - reportsReproduceRateLabel: 'ๅค็Žฐ็އ', - reportsReproduceRateAlways: 'ๆ€ปๆ˜ฏ', - reportsReproduceRateOften: '็ปๅธธ', - reportsReproduceRateSometimes: 'ๆœ‰ๆ—ถ', - reportsReproduceRateRarely: 'ๅถๅฐ”', - reportsReproduceRateUnable: 'ๆ— ๆณ•ๅค็Žฐ', - reportsReproduceRateUnknown: 'ๆœชๆŒ‡ๅฎš', - reportsProblemStatementLabel: '้—ฎ้ข˜/้œ€ๆฑ‚', - reportsProblemStatementPlaceholder: 'ๆญคๅŠŸ่ƒฝ่งฃๅ†ณไป€ไนˆ้—ฎ้ข˜๏ผŸ', - reportsUserStoryLabel: 'ๅฑ…ๆฐ‘ๆ•…ไบ‹', - reportsUserStoryPlaceholder: 'ไฝœไธบไธ€ไธช <ๅฑ…ๆฐ‘>๏ผŒๆˆ‘ๅธŒๆœ› <ๅŠจไฝœ>๏ผŒไปฅไพฟ <ๆ”ถ็›Š>ใ€‚', - reportsAcceptanceCriteriaLabel: '้ชŒๆ”ถๆ ‡ๅ‡†', - reportsAcceptanceCriteriaPlaceholder: '- ๅ‡่ฎพ...\n- ๅฝ“...\n- ้‚ฃไนˆ...', - reportsWhatHappenedLabel: 'ๅ‘็”Ÿไบ†ไป€ไนˆ๏ผŸ', - reportsWhatHappenedPlaceholder: 'ๆ่ฟฐไบ‹ไปถ็š„่ƒŒๆ™ฏๅ’Œๅคง่‡ดๆ—ฅๆœŸใ€‚', - reportsReportedUserLabel: '่ขซไธพๆŠฅ็š„ๅฑ…ๆฐ‘/ๅฎžไฝ“', - reportsReportedUserPlaceholder: '@ๅฑ…ๆฐ‘ใ€ๅŠจๆ€ใ€IDใ€้“พๆŽฅ็ญ‰ใ€‚', - reportsEvidenceLinksLabel: '่ฏๆฎ/้“พๆŽฅ', - reportsEvidenceLinksPlaceholder: '็ฒ˜่ดด้“พๆŽฅใ€ๆถˆๆฏ IDใ€ๆˆชๅ›พ๏ผˆๅฆ‚้€‚็”จ๏ผ‰็ญ‰ใ€‚', - reportsContentLocationLabel: 'ๅ†…ๅฎนไฝ็ฝฎ', - reportsContentLocationPlaceholder: '้“พๆŽฅใ€IDใ€้ข‘้“ใ€่ฎจ่ฎบไธฒใ€ไฝœ่€…็ญ‰ใ€‚', - reportsWhyInappropriateLabel: "ไธบไป€ไนˆไธๅˆ้€‚", - reportsWhyInappropriatePlaceholder: '่ฏดๆ˜ŽๅŽŸๅ› ๅ’Œๅฝฑๅ“ใ€‚', - reportsRequestedActionLabel: '่ฏทๆฑ‚็š„ๆ“ไฝœ', - reportsRequestedActionPlaceholder: 'ๅˆ ้™คใ€้š่—ใ€ๆ ‡่ฎฐใ€่ญฆๅ‘Š็ญ‰ใ€‚', tribesTitle: "้ƒจ่ฝ", tribeFilterAll: "ๅ…จ้ƒจ", tribeFilterRecent: "ๆœ€่ฟ‘", @@ -2272,6 +2244,7 @@ module.exports = { voteWellResearched: "็ ”็ฉถๅ……ๅˆ†", voteNeedsSources: "้œ€่ฆๆฅๆบ", voteWrong: "้”™่ฏฏ", + voteOutdated: "ๅทฒ่ฟ‡ๆ—ถ", voteLowQuality: "ไฝŽ่ดจ้‡", voteLove: "ๅ–œ็ˆฑ", voteClear: "ๆธ…ๆ™ฐ", @@ -2516,7 +2489,6 @@ module.exports = { shopNoProducts: "ๆš‚ๆ— ไบงๅ“ใ€‚", shopOutOfStock: "ๅทฒๅ”ฎ็ฝ„", shopBuy: "่ดญไนฐ", - shopBuyEncryptedNote: "ๆ‚จ็š„้…้€ไฟกๆฏๅฐ†ๅŠ ๅฏ†ๅ‘้€๏ผŒไป…ๅบ—ไธปๅฏ่งใ€‚", shopBuyDeliveryAddress: "้…้€ๅœฐๅ€", shopBuyContact: "่”็ณปๆ–นๅผ", shopBuyContactPlaceholder: "้‚ฎ็ฎฑใ€็”ต่ฏ็ญ‰", @@ -3033,6 +3005,16 @@ module.exports = { industryDescriptionLabel: "ๆ่ฟฐ", industryDescriptionPlaceholder: "่ฟ™ๅฎถๅทฅๅŽ‚็”Ÿไบงไป€ไนˆ๏ผŸ", industrySector: "้ƒจ้—จ", + industrySector_software: "่ฝฏไปถ", + industrySector_hardware: "็กฌไปถ", + industrySector_agriculture: "ๅ†œไธš", + industrySector_textile: "็บบ็ป‡", + industrySector_energy: "่ƒฝๆบ", + industrySector_food: "้ฃŸๅ“", + industrySector_construction: "ๅปบ็ญ‘", + industrySector_media: "ๅช’ไฝ“", + industrySector_services: "ๆœๅŠก", + industrySector_other: "ๅ…ถไป–", industryMembershipPolicy: "ๆˆๅ‘˜ๆ”ฟ็ญ–", industryQuorum: "ๆณ•ๅฎšไบบๆ•ฐ", industryMajority: "ๅคšๆ•ฐ", @@ -3234,6 +3216,18 @@ module.exports = { modulesFavoritesDescription: "็ฎก็†ไฝ ๆ”ถ่—ๅ†…ๅฎน็š„ๆจกๅ—ใ€‚", fileTooLargeTitle: "ๆ–‡ไปถ่ฟ‡ๅคง", fileTooLargeMessage: "ๆ–‡ไปถ่ถ…่ฟ‡ไบ†ๅ…่ฎธ็š„ๆœ€ๅคงๅคงๅฐ๏ผˆ75 MB๏ผ‰ใ€‚่ฏท้€‰ๆ‹ฉ่พƒๅฐ็š„ๆ–‡ไปถใ€‚", + fileWrongType: "ๆญคๆ–‡ไปถ็ฑปๅž‹ไธ้€‚็”จไบŽๆญคๆ ็›ฎใ€‚", + mediaLockedAfterOpinions: "ๅทฒๆœ‰่ฏ„ไปท๏ผšๅฏไปฅ็ผ–่พ‘ๅ…ถไป–ๅ†…ๅฎน๏ผŒไฝ†ไธ่ƒฝๆ›ดๆ”นๅทฒ่ขซ่ฏ„ไปท็š„้ƒจๅˆ†ใ€‚", + pdfPrevPage: "ไธŠไธ€้กต", + pdfNextPage: "ไธ‹ไธ€้กต", + pdfPage: "้กต", + pdfZoomIn: "ๆ”พๅคง", + pdfZoomOut: "็ผฉๅฐ", + pdfFullscreen: "ๅ…จๅฑ", + pdfClose: "ๅ…ณ้—ญ", + pdfRotate: "ๆ—‹่ฝฌ", + contentNotAvailableYet: "ๆญคๅ†…ๅฎนๆš‚ไธๅฏ็”จใ€‚", + pdfRetry: "้‡่ฏ•", goBack: "่ฟ”ๅ›ž", errorDismiss: "ๅฅฝ", errorPageTitle: "ๅ‡บ็Žฐไบ†้—ฎ้ข˜", @@ -3276,9 +3270,7 @@ module.exports = { uploadMedia: "ไธŠไผ ๅช’ไฝ“๏ผˆๆœ€ๅคง๏ผš75MB๏ผ‰", feedOpenDiscussion: "ๅ…ฌๅผ€่ฎจ่ฎบ", feedDetailTitle: "ๅŠจๆ€", - feedPostComment: "ๅ‘่กจ่ฏ„่ฎบ", courtsRespondentInvalid: "ๅฟ…้กปๆ˜ฏๆœ‰ๆ•ˆ็š„ SSB ID (@...ed25519)", - noComments: "่ฟ˜ๆฒกๆœ‰่ฏ„่ฎบ", mapsLabel: "ๅœฐๅ›พ", mapTitle: "ๅœฐๅ›พ", mapDescription: "ๅœจๆ‚จ็š„็ฝ‘็ปœไธญ็ฎก็†ๅ’Œๅˆ†ไบซ็ฆป็บฟๅœฐๅ›พใ€‚", @@ -3582,6 +3574,8 @@ module.exports = { podcastTagsPlaceholder: "่พ“ๅ…ฅๆ ‡็ญพ๏ผŒ็”จ้€—ๅทๅˆ†้š”", podcastFilterAll: "ๅ…จ้ƒจ", podcastFilterMine: "ๆˆ‘็š„", + podcastFilterFavorites: "ๆ”ถ่—", + podcastEpisodeNumberLabel: "้›†ๆ•ฐ", podcastFilterRecent: "ๆœ€่ฟ‘", podcastFilterTop: "็ƒญ้—จ", podcastFilterViewers: "ๅฌไผ—", @@ -3800,7 +3794,6 @@ module.exports = { wikiBackToPage: "่ฟ”ๅ›ž้กต้ข", wikiLinkedChip: "ๅทฒๅ…ณ่”", wikiRestoredSummary: "ๅทฒๆขๅค็š„็‰ˆๆœฌ", - wikiExportPdf: "็”Ÿๆˆ้กต้ข", wikiPreview: "้ข„่งˆ", tribeSectionWiki: "็ปดๅŸบ", tribeWikiCreate: "ๅˆ›ๅปบ็ปดๅŸบ", @@ -3868,6 +3861,9 @@ module.exports = { roomRecStop: "ๅœๆญขๅฝ•้Ÿณ", roomStreamFile: "ๆ’ญๆ”พ", roomStreamStop: "ๅœๆญข", + roomScreen: "ๅฑๅน•", + roomScreenSharing: "ๆญฃๅœจๅ…ฑไบซๅฑๅน•", + roomScreenWarning: "ๅœจไฝ ๅœๆญขไน‹ๅ‰๏ผŒๆˆฟ้—ด้‡Œ็š„ๆฏไธชไบบ้ƒฝไผš็œ‹ๅˆฐไฝ ็š„ๆ•ดไธชๅฑๅน•ใ€‚", roomStreaming: "ๆญฃๅœจๆ’ญๆ”พ", liveFullscreen: "ๅ…จๅฑ", roomStreamPickTitle: "้€‰ๆ‹ฉ่ง†้ข‘", @@ -3925,6 +3921,7 @@ module.exports = { inviteAlreadyMember: "ไฝ ๅทฒ็ปๅŠ ๅ…ฅไบ†๏ผŒๅฏไปฅๅœจๅฏนๅบ”ๆจกๅ—ไธญๆ‰“ๅผ€ใ€‚", actionFailed: "ๆ— ๆณ•ๅฎŒๆˆๆญคๆ“ไฝœ๏ผŒ่ฏท้‡่ฏ•ใ€‚", publishTooShort: "ไฝ ็š„ๅธ–ๅญๅคช็Ÿญไบ†ใ€‚", + publishEmpty: "่ฏทๅ†™็‚นๅ†…ๅฎนๆˆ–้™„ๅŠ ไธ€ไธชๆ–‡ไปถใ€‚", walletSendFailed: "ๆ— ๆณ•ๅ‘้€ไป˜ๆฌพ๏ผš{reason}", pixeliaCoordsError: "่ฟ™ไบ›ๅๆ ‡่ถ…ๅ‡บไบ†็ฝ‘ๆ ผ่Œƒๅ›ดใ€‚", courtsErrTitle: "้œ€่ฆๅกซๅ†™ๆ ‡้ข˜ใ€‚", @@ -4436,6 +4433,9 @@ module.exports = { torrentTagsLabel: "ๆ ‡็ญพ", torrentTagsPlaceholder: "่พ“ๅ…ฅๆ ‡็ญพ๏ผŒ็”จ้€—ๅทๅˆ†้š”", torrentSizeLabel: "ๅคงๅฐ", + torrentInfoHash: "ไฟกๆฏๅ“ˆๅธŒ", + torrentFilesLabel: "ๆ–‡ไปถ", + torrentMagnet: "MAGNET", torrentFromContent: "็”Ÿๆˆ็งๅญ", torrentFromContentMissing: "่ฏฅๆ–‡ไปถๅœจๆœฌ่Š‚็‚นๅฐšไธๅฏ็”จ๏ผŒ่ฏท็จๅŽๅ†่ฏ•ใ€‚", torrentDownloads: "ไธ‹่ฝฝ", @@ -4521,9 +4521,6 @@ module.exports = { inhabitantsPendingAccept: "ๆŽฅๅ—", inhabitantsPendingReject: "ๆ‹’็ป", tribeSectionGovernance: "ๆฒป็†", - tribePadInviteRequired: "ๆ‚จๆ— ๆƒ่ฎฟ้—ฎ padใ€‚่ฏทๆฑ‚้‚€่ฏทใ€‚", - tribeChatInviteRequired: "ๆ‚จๆ— ๆƒ่ฎฟ้—ฎ่Šๅคฉใ€‚่ฏทๆฑ‚้‚€่ฏทใ€‚", - tribeGovernanceDesc: "ๆญค้ƒจ่ฝ็š„ๅ†…้ƒจๆฒป็†ใ€‚", tribeGovernanceNoGov: "ๆ— ๆดป่ทƒๆ”ฟๅบœ", tribeGovernanceNoGovDesc: "ๆญค้ƒจ่ฝๅฐšๆœช้€‰ไธพๆ”ฟๅบœใ€‚", tribeGovCardTitle: "ๅฝ“ๅ‰ๆ”ฟๅบœ", @@ -4580,7 +4577,6 @@ module.exports = { logsFilterMonth: "ไธŠๆœˆ", logsFilterWeek: "ไธŠๅ‘จ", logsFilterToday: "ไปŠๅคฉ", - logsComments: "่ฏ„่ฎบ", logsAuthorEntries: "ๆก็›ฎ", logsCreate: "ๅˆ›ๅปบๆก็›ฎ", logsExport: "ๅฏผๅ‡บๆ—ฅๅฟ—", @@ -4736,6 +4732,8 @@ module.exports = { larpWillTestTitle: "WILL ๆต‹่ฏ•", settingsLanDesc: "ๅฎšๆœŸๅ‘ๅŒไธ€ๅฑ€ๅŸŸ็ฝ‘ไธญ็š„ๅ…ถไป– Oasis ๅฎžไพ‹ๅนฟๆ’ญๆญค่Š‚็‚นใ€‚", settingsLanEnable: "ๅฏ็”จๅฑ€ๅŸŸ็ฝ‘ๅนฟๆ’ญ", + settingsConnectionsTitle: "่ฟžๆŽฅ", + settingsConnectionsDesc: "ๆญค่Š‚็‚นๅŒๆ—ถไฟๆŒ่ฟžๆŽฅ็š„่Š‚็‚นๆ•ฐ้‡ใ€‚", settingsLanTitle: "ๅฑ€ๅŸŸ็ฝ‘ๅนฟๆ’ญ", settingsReplicationTitle: "ๅคๅˆถ", settingsReplicationDesc: "้…็ฝฎๅœจๅคๅˆถๅ†…ๅฎนๆ—ถ๏ผŒไฝ ็š„่Š‚็‚นไปŽ่‡ชๅทฑ็š„ไฟกๆฏๆตๅ‡บๅ‘่ทŸ้š็š„่ทณๆ•ฐใ€‚", diff --git a/src/client/gui.js b/src/client/gui.js index bc4374b..4ef9784 100644 --- a/src/client/gui.js +++ b/src/client/gui.js @@ -115,13 +115,13 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false } return new Promise((resolve, reject) => { if (internalSSB) { const { printMetadata, colors } = require('../server/ssb_metadata'); - printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); + printMetadata('OASIS UX', colors.yellow, port, host, isPublic); return resolve(windowLogStream(internalSSB)); } if (internalOpen) { return internalOpen().then((srv) => { const { printMetadata, colors } = require('../server/ssb_metadata'); - printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); + printMetadata('OASIS UX', colors.yellow, port, host, isPublic); resolve(windowLogStream(srv)); }).catch(reject); } @@ -138,7 +138,7 @@ module.exports = ({ offline, port = 3000, host = 'localhost', isPublic = false } reject(new Error("Closing Oasis")); } else { const { printMetadata, colors } = require('../server/ssb_metadata'); - printMetadata('OASIS UX', colors.yellow, port, host, offline, isPublic); + printMetadata('OASIS UX', colors.yellow, port, host, isPublic); resolve(ssb); } }).catch(reject); diff --git a/src/client/middleware.js b/src/client/middleware.js index a804895..488599a 100644 --- a/src/client/middleware.js +++ b/src/client/middleware.js @@ -54,6 +54,19 @@ module.exports = ({ host, port, middleware, allowHost }) => { const { isClearnetPath, isTrustedRequest, buildCsp } = require(path.join(__dirname, "..", "backend", "request_guards")); const isValidRequest = (request) => isTrustedRequest(request, validHosts); + const EMBED_PATHS = ["/pdf/embed"]; + const CLEARNET_EMBED_PATHS = ["/c/pdf/embed"]; + const pageFrames = (ctx, isClearnet) => { + if (ctx.method !== "GET") return {}; + const host = String(ctx.host || ''); + const embeds = /^[A-Za-z0-9.-]+(:\d+)?$/.test(host) ? (isClearnet ? CLEARNET_EMBED_PATHS : EMBED_PATHS).map(p => `${host}${p}`).join(' ') : "'self'"; + const game = /^\/games\/[a-z0-9]+\/?$/.test(ctx.path); + return { + objects: embeds, + frames: game ? "'self'" : embeds, + framed: EMBED_PATHS.includes(ctx.path) || CLEARNET_EMBED_PATHS.includes(ctx.path) + }; + }; const httpDebug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; @@ -92,15 +105,13 @@ module.exports = ({ host, port, middleware, allowHost }) => { gamesStatic.use(koaStatic(join(__dirname, "..", "games"))); app.use(mount("/game-assets", gamesStatic)); - app.use(mount("/js", koaStatic(path.join(__dirname, 'public/js')))); - app.use(koaStatic(path.join(__dirname, 'public'))); app.use(async (ctx, next) => { if (httpDebug) console.log(`[http] ${ctx.method} ${ctx.path}`); const isClearnet = isClearnetPath(ctx.request); - ctx.set("Content-Security-Policy", buildCsp(isClearnet, { frames: ctx.method === "GET" && /^\/games\/[a-z0-9]+\/?$/.test(ctx.path) })); + ctx.set("Content-Security-Policy", buildCsp(isClearnet, pageFrames(ctx, isClearnet))); ctx.set("X-Frame-Options", "SAMEORIGIN"); ctx.set("X-Content-Type-Options", "nosniff"); diff --git a/src/client/oasis_client.js b/src/client/oasis_client.js index 951bdfa..dd72d7e 100644 --- a/src/client/oasis_client.js +++ b/src/client/oasis_client.js @@ -27,7 +27,7 @@ PUB admin commands (require the Oasis Sbot to be running): GUI options (forwarded to the backend): --host= Hostname / IP the web UI listens on (default: localhost). Use 0.0.0.0 to expose on a VPS. - --port= Port for the web UI (default: 3000). + --port= Port for the web UI (default: 3000, or OASIS_PORT). --allow-host= Extra hostname allowed when behind a reverse proxy. --public Public-hosting mode: disables POST and redacts content from people who haven't opted in to public hosting. diff --git a/src/client/public/js/pdf-viewer.js b/src/client/public/js/pdf-viewer.js deleted file mode 100644 index 5dd09a8..0000000 --- a/src/client/public/js/pdf-viewer.js +++ /dev/null @@ -1,131 +0,0 @@ -const showPdfFallback = () => { - document.querySelectorAll('.pdf-viewer-container').forEach(container => { - if (container.dataset.pdfReady) return; - container.dataset.pdfReady = '1'; - container.textContent = ''; - const url = container.getAttribute('data-pdf-url'); - if (url) { - const link = document.createElement('a'); - link.href = url; - link.textContent = '๐Ÿ“„ PDF'; - container.appendChild(link); - } - }); -}; - -const initPdfViewers = async () => { - let pdfjsLib; - try { - pdfjsLib = await import('/js/pdf.min.mjs?v=102'); - pdfjsLib.GlobalWorkerOptions.workerSrc = '/js/pdf.worker.min.mjs?v=102'; - } catch (err) { - showPdfFallback(); - return; - } - - document.querySelectorAll('.pdf-viewer-container').forEach(async container => { - const pdfUrl = container.getAttribute('data-pdf-url'); - if (!pdfUrl || container.dataset.pdfReady) return; - container.dataset.pdfReady = '1'; - - let pdf; - try { - pdf = await pdfjsLib.getDocument(pdfUrl).promise; - } catch (err) { - container.textContent = ''; - const fallback = document.createElement('a'); - fallback.href = pdfUrl; - fallback.textContent = '๐Ÿ“„ PDF'; - container.appendChild(fallback); - return; - } - - let currentPage = 1; - let scale = 1.5; - let rotation = 0; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const canvasWrap = document.createElement('div'); - canvasWrap.className = 'pdf-canvas-wrap'; - canvasWrap.appendChild(canvas); - container.innerHTML = ''; - container.appendChild(canvasWrap); - - const controls = document.createElement('div'); - controls.className = 'pdf-controls'; - controls.innerHTML = ` - - - - - - - - - `; - - container.appendChild(controls); - - const renderPage = async (num) => { - const page = await pdf.getPage(num); - const viewport = page.getViewport({ scale, rotation }); - canvas.width = viewport.width; - canvas.height = viewport.height; - ctx.clearRect(0, 0, canvas.width, canvas.height); - await page.render({ canvasContext: ctx, viewport }).promise; - }; - - const goToPage = (delta) => { - const newPage = currentPage + delta; - if (newPage >= 1 && newPage <= pdf.numPages) { - currentPage = newPage; - renderPage(currentPage); - } - }; - - try { - const firstPage = await pdf.getPage(1); - const baseViewport = firstPage.getViewport({ scale: 1, rotation: 0 }); - const avail = canvasWrap.clientWidth || container.clientWidth; - if (avail > 0 && baseViewport.width > 0) { - scale = Math.min(1.5, Math.max(0.5, (avail - 40) / baseViewport.width)); - } - } catch (e) {} - - renderPage(currentPage); - - controls.querySelector('.pdf-prev').onclick = () => goToPage(-1); - controls.querySelector('.pdf-next').onclick = () => goToPage(1); - controls.querySelector('.pdf-zoom-in').onclick = () => { scale += 0.2; renderPage(currentPage); }; - controls.querySelector('.pdf-zoom-out').onclick = () => { scale = Math.max(0.5, scale - 0.2); renderPage(currentPage); }; - controls.querySelector('.pdf-rotate').onclick = () => { rotation = (rotation + 90) % 360; renderPage(currentPage); }; - controls.querySelector('.pdf-download').onclick = () => { - const a = document.createElement('a'); - a.href = pdfUrl; - a.download = 'document.pdf'; - a.click(); - }; - controls.querySelector('.pdf-fullscreen').onclick = async () => { - const maximized = container.classList.toggle('pdf-maximized'); - document.body.classList.toggle('pdf-maximized-body', maximized); - try { - const p1 = await pdf.getPage(currentPage); - const vw1 = p1.getViewport({ scale: 1, rotation }); - const avail = Math.max(260, (maximized ? window.innerWidth : (canvasWrap.clientWidth || container.clientWidth || 360)) - 24); - scale = maximized ? avail / vw1.width : Math.min(1.5, avail / vw1.width); - await renderPage(currentPage); - } catch (_) {} - }; - controls.querySelector('.pdf-metadata').onclick = async () => { - const info = await pdf.getMetadata(); - alert(`Title: ${info.info.Title || 'N/A'}\nAuthor: ${info.info.Author || 'N/A'}\nPDF Producer: ${info.info.Producer || 'N/A'}`); - }; - }); -}; - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initPdfViewers); -} else { - initPdfViewers(); -} diff --git a/src/client/public/js/pdf.min.mjs b/src/client/public/js/pdf.min.mjs deleted file mode 100644 index 3b3c62f..0000000 --- a/src/client/public/js/pdf.min.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @licstart The following is the entire license notice for the - * JavaScript code in this page - * - * Copyright 2024 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @licend The above is the entire license notice for the - * JavaScript code in this page - */ -/** - * pdfjsVersion = 5.3.31 - * pdfjsBuild = 47ad820d9 - */ -const t=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),e=[.001,0,0,.001,0,0],i=1.35,s=1,n=2,a=4,r=16,o=32,l=64,h=128,d=256,c={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},u={DISABLE:-1,NONE:0,FREETEXT:3,HIGHLIGHT:9,STAMP:13,INK:15,SIGNATURE:101},p={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23,HIGHLIGHT_COLOR:31,HIGHLIGHT_DEFAULT_COLOR:32,HIGHLIGHT_THICKNESS:33,HIGHLIGHT_FREE:34,HIGHLIGHT_SHOW_ALL:35,DRAW_STEP:41},g={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},f=0,m=1,b=2,A=3,w=3,y=4,v={GRAYSCALE_1BPP:1,RGB_24BPP:2,RGBA_32BPP:3},x={TEXT:1,LINK:2,FREETEXT:3,LINE:4,SQUARE:5,CIRCLE:6,POLYGON:7,POLYLINE:8,HIGHLIGHT:9,UNDERLINE:10,SQUIGGLY:11,STRIKEOUT:12,STAMP:13,CARET:14,INK:15,POPUP:16,FILEATTACHMENT:17,SOUND:18,MOVIE:19,WIDGET:20,SCREEN:21,PRINTERMARK:22,TRAPNET:23,WATERMARK:24,THREED:25,REDACT:26},E=1,_=2,S=3,C=4,T=5,M={ERRORS:0,WARNINGS:1,INFOS:5},D={dependency:1,setLineWidth:2,setLineCap:3,setLineJoin:4,setMiterLimit:5,setDash:6,setRenderingIntent:7,setFlatness:8,setGState:9,save:10,restore:11,transform:12,moveTo:13,lineTo:14,curveTo:15,curveTo2:16,curveTo3:17,closePath:18,rectangle:19,stroke:20,closeStroke:21,fill:22,eoFill:23,fillStroke:24,eoFillStroke:25,closeFillStroke:26,closeEOFillStroke:27,endPath:28,clip:29,eoClip:30,beginText:31,endText:32,setCharSpacing:33,setWordSpacing:34,setHScale:35,setLeading:36,setFont:37,setTextRenderingMode:38,setTextRise:39,moveText:40,setLeadingMoveText:41,setTextMatrix:42,nextLine:43,showText:44,showSpacedText:45,nextLineShowText:46,nextLineSetSpacingShowText:47,setCharWidth:48,setCharWidthAndBounds:49,setStrokeColorSpace:50,setFillColorSpace:51,setStrokeColor:52,setStrokeColorN:53,setFillColor:54,setFillColorN:55,setStrokeGray:56,setFillGray:57,setStrokeRGBColor:58,setFillRGBColor:59,setStrokeCMYKColor:60,setFillCMYKColor:61,shadingFill:62,beginInlineImage:63,beginImageData:64,endInlineImage:65,paintXObject:66,markPoint:67,markPointProps:68,beginMarkedContent:69,beginMarkedContentProps:70,endMarkedContent:71,beginCompat:72,endCompat:73,paintFormXObjectBegin:74,paintFormXObjectEnd:75,beginGroup:76,endGroup:77,beginAnnotation:80,endAnnotation:81,paintImageMaskXObject:83,paintImageMaskXObjectGroup:84,paintImageXObject:85,paintInlineImageXObject:86,paintInlineImageXObjectGroup:87,paintImageXObjectRepeat:88,paintImageMaskXObjectRepeat:89,paintSolidColorImageMask:90,constructPath:91,setStrokeTransparent:92,setFillTransparent:93,rawFillPath:94},P=0,k=1,I=2,R=3,F={NEED_PASSWORD:1,INCORRECT_PASSWORD:2};let L=M.WARNINGS;function setVerbosityLevel(t){Number.isInteger(t)&&(L=t)}function getVerbosityLevel(){return L}function info(t){L>=M.INFOS&&console.log(`Info: ${t}`)}function warn(t){L>=M.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function assert(t,e){t||unreachable(e)}function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=function stringToUTF8String(t){return decodeURIComponent(escape(t))}(t)}catch{}}const s=e?URL.parse(t,e):URL.parse(t);return function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s)?s:null}function updateUrlHash(t,e,i=!1){const s=URL.parse(t);if(s){s.hash=e;return s.href}return i&&createValidAbsoluteUrl(t,"http://example.com")?t.split("#",1)[0]+""+(e?`#${e}`:""):""}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const O=function BaseExceptionClosure(){function BaseException(t,e){this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends O{constructor(t,e){super(t,"PasswordException");this.code=e}}class UnknownErrorException extends O{constructor(t,e){super(t,"UnknownErrorException");this.details=e}}class InvalidPDFException extends O{constructor(t){super(t,"InvalidPDFException")}}class ResponseException extends O{constructor(t,e,i){super(t,"ResponseException");this.status=e;this.missing=i}}class FormatError extends O{constructor(t){super(t,"FormatError")}}class AbortException extends O{constructor(t){super(t,"AbortException")}}function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));class Util{static makeHexColor(t,e,i){return`#${N[t]}${N[e]}${N[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[0];e[2]*=t[0];if(t[3]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[1];e[1]=i;i=e[2];e[2]=e[3];e[3]=i;if(t[1]<0){i=e[1];e[1]=e[3];e[3]=i}e[1]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[2];e[2]=i}e[0]*=t[2];e[2]*=t[2]}e[0]+=t[4];e[1]+=t[5];e[2]+=t[4];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e,i=0){const s=t[i],n=t[i+1];t[i]=s*e[0]+n*e[2]+e[4];t[i+1]=s*e[1]+n*e[3]+e[5]}static applyTransformToBezier(t,e,i=0){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5];for(let e=0;e<6;e+=2){const h=t[i+e],d=t[i+e+1];t[i+e]=h*s+d*a+o;t[i+e+1]=h*n+d*r+l}}static applyInverseTransform(t,e){const i=t[0],s=t[1],n=e[0]*e[3]-e[1]*e[2];t[0]=(i*e[3]-s*e[2]+e[2]*e[5]-e[4]*e[3])/n;t[1]=(-i*e[1]+s*e[0]+e[4]*e[1]-e[5]*e[0])/n}static axialAlignedBoundingBox(t,e,i){const s=e[0],n=e[1],a=e[2],r=e[3],o=e[4],l=e[5],h=t[0],d=t[1],c=t[2],u=t[3];let p=s*h+o,g=p,f=s*c+o,m=f,b=r*d+l,A=b,w=r*u+l,y=w;if(0!==n||0!==a){const t=n*h,e=n*c,i=a*d,s=a*u;p+=i;m+=i;f+=s;g+=s;b+=t;y+=t;w+=e;A+=e}i[0]=Math.min(i[0],p,f,g,m);i[1]=Math.min(i[1],b,w,A,y);i[2]=Math.max(i[2],p,f,g,m);i[3]=Math.max(i[3],b,w,A,y)}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t,e){const i=t[0],s=t[1],n=t[2],a=t[3],r=i**2+s**2,o=i*n+s*a,l=n**2+a**2,h=(r+l)/2,d=Math.sqrt(h**2-(r*l-o**2));e[0]=Math.sqrt(h+d||1);e[1]=Math.sqrt(h-d||1)}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static pointBoundingBox(t,e,i){i[0]=Math.min(i[0],t);i[1]=Math.min(i[1],e);i[2]=Math.max(i[2],t);i[3]=Math.max(i[3],e)}static rectBoundingBox(t,e,i,s,n){n[0]=Math.min(n[0],t,i);n[1]=Math.min(n[1],e,s);n[2]=Math.max(n[2],t,i);n[3]=Math.max(n[3],e,s)}static#t(t,e,i,s,n,a,r,o,l,h){if(l<=0||l>=1)return;const d=1-l,c=l*l,u=c*l,p=d*(d*(d*t+3*l*e)+3*c*i)+u*s,g=d*(d*(d*n+3*l*a)+3*c*r)+u*o;h[0]=Math.min(h[0],p);h[1]=Math.min(h[1],g);h[2]=Math.max(h[2],p);h[3]=Math.max(h[3],g)}static#e(t,e,i,s,n,a,r,o,l,h,d,c){if(Math.abs(l)<1e-12){Math.abs(h)>=1e-12&&this.#t(t,e,i,s,n,a,r,o,-d/h,c);return}const u=h**2-4*d*l;if(u<0)return;const p=Math.sqrt(u),g=2*l;this.#t(t,e,i,s,n,a,r,o,(-h+p)/g,c);this.#t(t,e,i,s,n,a,r,o,(-h-p)/g,c)}static bezierBoundingBox(t,e,i,s,n,a,r,o,l){l[0]=Math.min(l[0],t,r);l[1]=Math.min(l[1],e,o);l[2]=Math.max(l[2],t,r);l[3]=Math.max(l[3],e,o);this.#e(t,i,n,r,e,s,a,o,3*(3*(i-n)-t+r),6*(t-2*i+n),3*(i-t),l);this.#e(t,i,n,r,e,s,a,o,3*(3*(s-a)-e+o),6*(e-2*s+a),3*(s-e),l)}}let B=null,H=null;function normalizeUnicode(t){if(!B){B=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;H=new Map([["๏ฌ…","ลฟt"]])}return t.replaceAll(B,((t,e,i)=>e?e.normalize("NFKC"):H.get(i)))}function getUuid(){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);crypto.getRandomValues(t);return bytesToString(t)}const U="pdfjs_internal_id_";function MathClamp(t,e,i){return Math.min(Math.max(t,e),i)}function toBase64Util(t){return Uint8Array.prototype.toBase64?t.toBase64():btoa(bytesToString(t))}"function"!=typeof Promise.try&&(Promise.try=function(t,...e){return new Promise((i=>{i(t(...e))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(t){return t.reduce(((t,e)=>t+e),0)});const z="http://www.w3.org/2000/svg";class PixelsPerInch{static CSS=96;static PDF=72;static PDF_TO_CSS_UNITS=this.CSS/this.PDF}async function fetchData(t,e="text"){if(isValidFetchUrl(t,document.baseURI)){const i=await fetch(t);if(!i.ok)throw new Error(i.statusText);switch(e){case"arraybuffer":return i.arrayBuffer();case"blob":return i.blob();case"json":return i.json()}return i.text()}return new Promise(((i,s)=>{const n=new XMLHttpRequest;n.open("GET",t,!0);n.responseType=e;n.onreadystatechange=()=>{if(n.readyState===XMLHttpRequest.DONE)if(200!==n.status&&0!==n.status)s(new Error(n.statusText));else{switch(e){case"arraybuffer":case"blob":case"json":i(n.response);return}i(n.responseText)}};n.send(null)}))}class PageViewport{constructor({viewBox:t,userUnit:e,scale:i,rotation:s,offsetX:n=0,offsetY:a=0,dontFlip:r=!1}){this.viewBox=t;this.userUnit=e;this.scale=i;this.rotation=s;this.offsetX=n;this.offsetY=a;i*=e;const o=(t[2]+t[0])/2,l=(t[3]+t[1])/2;let h,d,c,u,p,g,f,m;(s%=360)<0&&(s+=360);switch(s){case 180:h=-1;d=0;c=0;u=1;break;case 90:h=0;d=1;c=1;u=0;break;case 270:h=0;d=-1;c=-1;u=0;break;case 0:h=1;d=0;c=0;u=-1;break;default:throw new Error("PageViewport: Invalid rotation, must be a multiple of 90 degrees.")}if(r){c=-c;u=-u}if(0===h){p=Math.abs(l-t[1])*i+n;g=Math.abs(o-t[0])*i+a;f=(t[3]-t[1])*i;m=(t[2]-t[0])*i}else{p=Math.abs(o-t[0])*i+n;g=Math.abs(l-t[1])*i+a;f=(t[2]-t[0])*i;m=(t[3]-t[1])*i}this.transform=[h*i,d*i,c*i,u*i,p-h*i*o-c*i*l,g-d*i*o-u*i*l];this.width=f;this.height=m}get rawDims(){const t=this.viewBox;return shadow(this,"rawDims",{pageWidth:t[2]-t[0],pageHeight:t[3]-t[1],pageX:t[0],pageY:t[1]})}clone({scale:t=this.scale,rotation:e=this.rotation,offsetX:i=this.offsetX,offsetY:s=this.offsetY,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.viewBox.slice(),userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}convertToViewportPoint(t,e){const i=[t,e];Util.applyTransform(i,this.transform);return i}convertToViewportRectangle(t){const e=[t[0],t[1]];Util.applyTransform(e,this.transform);const i=[t[2],t[3]];Util.applyTransform(i,this.transform);return[e[0],e[1],i[0],i[1]]}convertToPdfPoint(t,e){const i=[t,e];Util.applyInverseTransform(i,this.transform);return i}}class RenderingCancelledException extends O{constructor(t,e=0){super(t,"RenderingCancelledException");this.extraDelay=e}}function isDataScheme(t){const e=t.length;let i=0;for(;i=1&&s<=12?s-1:0;let n=parseInt(e[3],10);n=n>=1&&n<=31?n:1;let a=parseInt(e[4],10);a=a>=0&&a<=23?a:0;let r=parseInt(e[5],10);r=r>=0&&r<=59?r:0;let o=parseInt(e[6],10);o=o>=0&&o<=59?o:0;const l=e[7]||"Z";let h=parseInt(e[8],10);h=h>=0&&h<=23?h:0;let d=parseInt(e[9],10)||0;d=d>=0&&d<=59?d:0;if("-"===l){a+=h;r+=d}else if("+"===l){a-=h;r-=d}return new Date(Date.UTC(i,s,n,a,r,o))}}function getXfaPageViewport(t,{scale:e=1,rotation:i=0}){const{width:s,height:n}=t.attributes.style,a=[0,0,parseInt(s),parseInt(n)];return new PageViewport({viewBox:a,userUnit:1,scale:e,rotation:i})}function getRGB(t){if(t.startsWith("#")){const e=parseInt(t.slice(1),16);return[(16711680&e)>>16,(65280&e)>>8,255&e]}if(t.startsWith("rgb("))return t.slice(4,-1).split(",").map((t=>parseInt(t)));if(t.startsWith("rgba("))return t.slice(5,-1).split(",").map((t=>parseInt(t))).slice(0,3);warn(`Not a valid color format: "${t}"`);return[0,0,0]}function getCurrentTransform(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform();return[e,i,s,n,a,r]}function getCurrentTransformInverse(t){const{a:e,b:i,c:s,d:n,e:a,f:r}=t.getTransform().invertSelf();return[e,i,s,n,a,r]}function setLayerDimensions(t,e,i=!1,s=!0){if(e instanceof PageViewport){const{pageWidth:s,pageHeight:n}=e.rawDims,{style:a}=t,r=util_FeatureTest.isCSSRoundSupported,o=`var(--total-scale-factor) * ${s}px`,l=`var(--total-scale-factor) * ${n}px`,h=r?`round(down, ${o}, var(--scale-round-x))`:`calc(${o})`,d=r?`round(down, ${l}, var(--scale-round-y))`:`calc(${l})`;if(i&&e.rotation%180!=0){a.width=d;a.height=h}else{a.width=h;a.height=d}}s&&t.setAttribute("data-main-rotation",e.rotation)}class OutputScale{constructor(){const{pixelRatio:t}=OutputScale;this.sx=t;this.sy=t}get scaled(){return 1!==this.sx||1!==this.sy}get symmetric(){return this.sx===this.sy}limitCanvas(t,e,i,s,n=-1){let a=1/0,r=1/0,o=1/0;(i=OutputScale.capPixels(i,n))>0&&(a=Math.sqrt(i/(t*e)));if(-1!==s){r=s/t;o=s/e}const l=Math.min(a,r,o);if(this.sx>l||this.sy>l){this.sx=l;this.sy=l;return!0}return!1}static get pixelRatio(){return globalThis.devicePixelRatio||1}static capPixels(t,e){if(e>=0){const i=Math.ceil(window.screen.availWidth*window.screen.availHeight*this.pixelRatio**2*(1+e/100));return t>0?Math.min(t,i):i}return t}}const G=["image/apng","image/avif","image/bmp","image/gif","image/jpeg","image/png","image/svg+xml","image/webp","image/x-icon"];class EditorToolbar{#s=null;#n=null;#a;#r=null;#o=null;#l=null;static#h=null;constructor(t){this.#a=t;EditorToolbar.#h||=Object.freeze({freetext:"pdfjs-editor-remove-freetext-button",highlight:"pdfjs-editor-remove-highlight-button",ink:"pdfjs-editor-remove-ink-button",stamp:"pdfjs-editor-remove-stamp-button",signature:"pdfjs-editor-remove-signature-button"})}render(){const t=this.#s=document.createElement("div");t.classList.add("editToolbar","hidden");t.setAttribute("role","toolbar");const e=this.#a._uiManager._signal;t.addEventListener("contextmenu",noContextMenu,{signal:e});t.addEventListener("pointerdown",EditorToolbar.#d,{signal:e});const i=this.#r=document.createElement("div");i.className="buttons";t.append(i);const s=this.#a.toolbarPosition;if(s){const{style:e}=t,i="ltr"===this.#a._uiManager.direction?1-s[0]:s[0];e.insetInlineEnd=100*i+"%";e.top=`calc(${100*s[1]}% + var(--editor-toolbar-vert-offset))`}this.#c();return t}get div(){return this.#s}static#d(t){t.stopPropagation()}#u(t){this.#a._focusEventsAllowed=!1;stopEvent(t)}#p(t){this.#a._focusEventsAllowed=!0;stopEvent(t)}#g(t){const e=this.#a._uiManager._signal;t.addEventListener("focusin",this.#u.bind(this),{capture:!0,signal:e});t.addEventListener("focusout",this.#p.bind(this),{capture:!0,signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e})}hide(){this.#s.classList.add("hidden");this.#n?.hideDropdown()}show(){this.#s.classList.remove("hidden");this.#o?.shown()}#c(){const{editorType:t,_uiManager:e}=this.#a,i=document.createElement("button");i.className="delete";i.tabIndex=0;i.setAttribute("data-l10n-id",EditorToolbar.#h[t]);this.#g(i);i.addEventListener("click",(t=>{e.delete()}),{signal:e._signal});this.#r.append(i)}get#f(){const t=document.createElement("div");t.className="divider";return t}async addAltText(t){const e=await t.render();this.#g(e);this.#r.prepend(e,this.#f);this.#o=t}addColorPicker(t){this.#n=t;const e=t.renderButton();this.#g(e);this.#r.prepend(e,this.#f)}async addEditSignatureButton(t){const e=this.#l=await t.renderEditButton(this.#a);this.#g(e);this.#r.prepend(e,this.#f)}updateEditSignatureButton(t){this.#l&&(this.#l.title=t)}remove(){this.#s.remove();this.#n?.destroy();this.#n=null}}class HighlightToolbar{#r=null;#s=null;#m;constructor(t){this.#m=t}#b(){const t=this.#s=document.createElement("div");t.className="editToolbar";t.setAttribute("role","toolbar");t.addEventListener("contextmenu",noContextMenu,{signal:this.#m._signal});const e=this.#r=document.createElement("div");e.className="buttons";t.append(e);this.#A();return t}#w(t,e){let i=0,s=0;for(const n of t){const t=n.y+n.height;if(ti){s=a;i=t}else e?a>s&&(s=a):a{this.#m.highlightSelection("floating_button")}),{signal:i});this.#r.append(t)}}function bindEvents(t,e,i){for(const s of i)e.addEventListener(s,t[s].bind(t))}class IdManager{#y=0;get id(){return"pdfjs_internal_editor_"+this.#y++}}class ImageManager{#v=getUuid();#y=0;#x=null;static get _isSVGFittingCanvas(){const t=new OffscreenCanvas(1,3).getContext("2d",{willReadFrequently:!0}),e=new Image;e.src='data:image/svg+xml;charset=UTF-8,';return shadow(this,"_isSVGFittingCanvas",e.decode().then((()=>{t.drawImage(e,0,0,1,1,0,0,1,3);return 0===new Uint32Array(t.getImageData(0,0,1,1).data.buffer)[0]})))}async#E(t,e){this.#x||=new Map;let i=this.#x.get(t);if(null===i)return null;if(i?.bitmap){i.refCounter+=1;return i}try{i||={bitmap:null,id:`image_${this.#v}_${this.#y++}`,refCounter:0,isSvg:!1};let t;if("string"==typeof e){i.url=e;t=await fetchData(e,"blob")}else e instanceof File?t=i.file=e:e instanceof Blob&&(t=e);if("image/svg+xml"===t.type){const e=ImageManager._isSVGFittingCanvas,s=new FileReader,n=new Image,a=new Promise(((t,a)=>{n.onload=()=>{i.bitmap=n;i.isSvg=!0;t()};s.onload=async()=>{const t=i.svgUrl=s.result;n.src=await e?`${t}#svgView(preserveAspectRatio(none))`:t};n.onerror=s.onerror=a}));s.readAsDataURL(t);await a}else i.bitmap=await createImageBitmap(t);i.refCounter=1}catch(t){warn(t);i=null}this.#x.set(t,i);i&&this.#x.set(i.id,i);return i}async getFromFile(t){const{lastModified:e,name:i,size:s,type:n}=t;return this.#E(`${e}_${i}_${s}_${n}`,t)}async getFromUrl(t){return this.#E(t,t)}async getFromBlob(t,e){const i=await e;return this.#E(t,i)}async getFromId(t){this.#x||=new Map;const e=this.#x.get(t);if(!e)return null;if(e.bitmap){e.refCounter+=1;return e}if(e.file)return this.getFromFile(e.file);if(e.blobPromise){const{blobPromise:t}=e;delete e.blobPromise;return this.getFromBlob(e.id,t)}return this.getFromUrl(e.url)}getFromCanvas(t,e){this.#x||=new Map;let i=this.#x.get(t);if(i?.bitmap){i.refCounter+=1;return i}const s=new OffscreenCanvas(e.width,e.height);s.getContext("2d").drawImage(e,0,0);i={bitmap:s.transferToImageBitmap(),id:`image_${this.#v}_${this.#y++}`,refCounter:1,isSvg:!1};this.#x.set(t,i);this.#x.set(i.id,i);return i}getSvgUrl(t){const e=this.#x.get(t);return e?.isSvg?e.svgUrl:null}deleteId(t){this.#x||=new Map;const e=this.#x.get(t);if(!e)return;e.refCounter-=1;if(0!==e.refCounter)return;const{bitmap:i}=e;if(!e.url&&!e.file){const t=new OffscreenCanvas(i.width,i.height);t.getContext("bitmaprenderer").transferFromImageBitmap(i);e.blobPromise=t.convertToBlob()}i.close?.();e.bitmap=null}isValidId(t){return t.startsWith(`image_${this.#v}_`)}}class CommandManager{#_=[];#S=!1;#C;#T=-1;constructor(t=128){this.#C=t}add({cmd:t,undo:e,post:i,mustExec:s,type:n=NaN,overwriteIfSameType:a=!1,keepUndo:r=!1}){s&&t();if(this.#S)return;const o={cmd:t,undo:e,post:i,type:n};if(-1===this.#T){this.#_.length>0&&(this.#_.length=0);this.#T=0;this.#_.push(o);return}if(a&&this.#_[this.#T].type===n){r&&(o.undo=this.#_[this.#T].undo);this.#_[this.#T]=o;return}const l=this.#T+1;if(l===this.#C)this.#_.splice(0,1);else{this.#T=l;l=0;e--)if(this.#_[e].type!==t){this.#_.splice(e+1,this.#T-e);this.#T=e;return}this.#_.length=0;this.#T=-1}}destroy(){this.#_=null}}class KeyboardManager{constructor(t){this.buffer=[];this.callbacks=new Map;this.allKeys=new Set;const{isMac:e}=util_FeatureTest.platform;for(const[i,s,n={}]of t)for(const t of i){const i=t.startsWith("mac+");if(e&&i){this.callbacks.set(t.slice(4),{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}else if(!e&&!i){this.callbacks.set(t,{callback:s,options:n});this.allKeys.add(t.split("+").at(-1))}}}#M(t){t.altKey&&this.buffer.push("alt");t.ctrlKey&&this.buffer.push("ctrl");t.metaKey&&this.buffer.push("meta");t.shiftKey&&this.buffer.push("shift");this.buffer.push(t.key);const e=this.buffer.join("+");this.buffer.length=0;return e}exec(t,e){if(!this.allKeys.has(e.key))return;const i=this.callbacks.get(this.#M(e));if(!i)return;const{callback:s,options:{bubbles:n=!1,args:a=[],checker:r=null}}=i;if(!r||r(t,e)){s.bind(t,...a,e)();n||stopEvent(e)}}}class ColorManager{static _colorsMapping=new Map([["CanvasText",[0,0,0]],["Canvas",[255,255,255]]]);get _colors(){const t=new Map([["CanvasText",null],["Canvas",null]]);!function getColorValues(t){const e=document.createElement("span");e.style.visibility="hidden";e.style.colorScheme="only light";document.body.append(e);for(const i of t.keys()){e.style.color=i;const s=window.getComputedStyle(e).color;t.set(i,getRGB(s))}e.remove()}(t);return shadow(this,"_colors",t)}convert(t){const e=getRGB(t);if(!window.matchMedia("(forced-colors: active)").matches)return e;for(const[t,i]of this._colors)if(i.every(((t,i)=>t===e[i])))return ColorManager._colorsMapping.get(t);return e}getHexCode(t){const e=this._colors.get(t);return e?Util.makeHexColor(...e):t}}class AnnotationEditorUIManager{#D=new AbortController;#P=null;#k=new Map;#I=new Map;#R=null;#F=null;#L=null;#O=new CommandManager;#N=null;#B=null;#H=0;#U=new Set;#z=null;#G=null;#$=new Set;_editorUndoBar=null;#W=!1;#V=!1;#j=!1;#q=null;#X=null;#K=null;#Y=null;#Q=!1;#J=null;#Z=new IdManager;#tt=!1;#et=!1;#it=null;#st=null;#nt=null;#at=null;#rt=null;#ot=u.NONE;#lt=new Set;#ht=null;#dt=null;#ct=null;#ut=null;#pt={isEditing:!1,isEmpty:!0,hasSomethingToUndo:!1,hasSomethingToRedo:!1,hasSelectedEditor:!1,hasSelectedText:!1};#gt=[0,0];#ft=null;#mt=null;#bt=null;#At=null;static TRANSLATE_SMALL=1;static TRANSLATE_BIG=10;static get _keyboardManager(){const t=AnnotationEditorUIManager.prototype,arrowChecker=t=>t.#mt.contains(document.activeElement)&&"BUTTON"!==document.activeElement.tagName&&t.hasSomethingToControl(),textInputChecker=(t,{target:e})=>{if(e instanceof HTMLInputElement){const{type:t}=e;return"text"!==t&&"number"!==t}return!0},e=this.TRANSLATE_SMALL,i=this.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+a","mac+meta+a"],t.selectAll,{checker:textInputChecker}],[["ctrl+z","mac+meta+z"],t.undo,{checker:textInputChecker}],[["ctrl+y","ctrl+shift+z","mac+meta+shift+z","ctrl+shift+Z","mac+meta+shift+Z"],t.redo,{checker:textInputChecker}],[["Backspace","alt+Backspace","ctrl+Backspace","shift+Backspace","mac+Backspace","mac+alt+Backspace","mac+ctrl+Backspace","Delete","ctrl+Delete","shift+Delete","mac+Delete"],t.delete,{checker:textInputChecker}],[["Enter","mac+Enter"],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(e)&&!t.isEnterHandled}],[[" ","mac+ "],t.addNewEditorFromKeyboard,{checker:(t,{target:e})=>!(e instanceof HTMLButtonElement)&&t.#mt.contains(document.activeElement)}],[["Escape","mac+Escape"],t.unselectAll],[["ArrowLeft","mac+ArrowLeft"],t.translateSelectedEditors,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t.translateSelectedEditors,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t.translateSelectedEditors,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t.translateSelectedEditors,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t.translateSelectedEditors,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t.translateSelectedEditors,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t.translateSelectedEditors,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t.translateSelectedEditors,{args:[0,i],checker:arrowChecker}]]))}constructor(t,e,i,s,n,a,r,o,l,h,d,c,u,p){const g=this._signal=this.#D.signal;this.#mt=t;this.#bt=e;this.#R=i;this.#dt=s;this._eventBus=n;n._on("editingaction",this.onEditingAction.bind(this),{signal:g});n._on("pagechanging",this.onPageChanging.bind(this),{signal:g});n._on("scalechanging",this.onScaleChanging.bind(this),{signal:g});n._on("rotationchanging",this.onRotationChanging.bind(this),{signal:g});n._on("setpreference",this.onSetPreference.bind(this),{signal:g});n._on("switchannotationeditorparams",(t=>this.updateParams(t.type,t.value)),{signal:g});this.#wt();this.#yt();this.#vt();this.#F=a.annotationStorage;this.#q=a.filterFactory;this.#ct=r;this.#Y=o||null;this.#W=l;this.#V=h;this.#j=d;this.#rt=c||null;this.viewParameters={realScale:PixelsPerInch.PDF_TO_CSS_UNITS,rotation:0};this.isShiftKeyDown=!1;this._editorUndoBar=u||null;this._supportsPinchToZoom=!1!==p}destroy(){this.#At?.resolve();this.#At=null;this.#D?.abort();this.#D=null;this._signal=null;for(const t of this.#I.values())t.destroy();this.#I.clear();this.#k.clear();this.#$.clear();this.#at?.clear();this.#P=null;this.#lt.clear();this.#O.destroy();this.#R?.destroy();this.#dt?.destroy();this.#J?.hide();this.#J=null;this.#nt?.destroy();this.#nt=null;if(this.#X){clearTimeout(this.#X);this.#X=null}if(this.#ft){clearTimeout(this.#ft);this.#ft=null}this._editorUndoBar?.destroy()}combinedSignal(t){return AbortSignal.any([this._signal,t.signal])}get mlManager(){return this.#rt}get useNewAltTextFlow(){return this.#V}get useNewAltTextWhenAddingImage(){return this.#j}get hcmFilter(){return shadow(this,"hcmFilter",this.#ct?this.#q.addHCMFilter(this.#ct.foreground,this.#ct.background):"none")}get direction(){return shadow(this,"direction",getComputedStyle(this.#mt).direction)}get highlightColors(){return shadow(this,"highlightColors",this.#Y?new Map(this.#Y.split(",").map((t=>t.split("=").map((t=>t.trim()))))):null)}get highlightColorNames(){return shadow(this,"highlightColorNames",this.highlightColors?new Map(Array.from(this.highlightColors,(t=>t.reverse()))):null)}setCurrentDrawingSession(t){if(t){this.unselectAll();this.disableUserSelect(!0)}else this.disableUserSelect(!1);this.#B=t}setMainHighlightColorPicker(t){this.#nt=t}editAltText(t,e=!1){this.#R?.editAltText(this,t,e)}getSignature(t){this.#dt?.getSignature({uiManager:this,editor:t})}get signatureManager(){return this.#dt}switchToMode(t,e){this._eventBus.on("annotationeditormodechanged",e,{once:!0,signal:this._signal});this._eventBus.dispatch("showannotationeditorui",{source:this,mode:t})}setPreference(t,e){this._eventBus.dispatch("setpreference",{source:this,name:t,value:e})}onSetPreference({name:t,value:e}){if("enableNewAltTextWhenAddingImage"===t)this.#j=e}onPageChanging({pageNumber:t}){this.#H=t-1}focusMainContainer(){this.#mt.focus()}findParent(t,e){for(const i of this.#I.values()){const{x:s,y:n,width:a,height:r}=i.div.getBoundingClientRect();if(t>=s&&t<=s+a&&e>=n&&e<=n+r)return i}return null}disableUserSelect(t=!1){this.#bt.classList.toggle("noUserSelect",t)}addShouldRescale(t){this.#$.add(t)}removeShouldRescale(t){this.#$.delete(t)}onScaleChanging({scale:t}){this.commitOrRemove();this.viewParameters.realScale=t*PixelsPerInch.PDF_TO_CSS_UNITS;for(const t of this.#$)t.onScaleChanging();this.#B?.onScaleChanging()}onRotationChanging({pagesRotation:t}){this.commitOrRemove();this.viewParameters.rotation=t}#xt({anchorNode:t}){return t.nodeType===Node.TEXT_NODE?t.parentElement:t}#Et(t){const{currentLayer:e}=this;if(e.hasTextLayer(t))return e;for(const e of this.#I.values())if(e.hasTextLayer(t))return e;return null}highlightSelection(t=""){const e=document.getSelection();if(!e||e.isCollapsed)return;const{anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a}=e,r=e.toString(),o=this.#xt(e).closest(".textLayer"),l=this.getSelectionBoxes(o);if(!l)return;e.empty();const h=this.#Et(o),d=this.#ot===u.NONE,callback=()=>{h?.createAndAddNewEditor({x:0,y:0},!1,{methodOfCreation:t,boxes:l,anchorNode:i,anchorOffset:s,focusNode:n,focusOffset:a,text:r});d&&this.showAllEditors("highlight",!0,!0)};d?this.switchToMode(u.HIGHLIGHT,callback):callback()}#_t(){const t=document.getSelection();if(!t||t.isCollapsed)return;const e=this.#xt(t).closest(".textLayer"),i=this.getSelectionBoxes(e);if(i){this.#J||=new HighlightToolbar(this);this.#J.show(e,i,"ltr"===this.direction)}}addToAnnotationStorage(t){t.isEmpty()||!this.#F||this.#F.has(t.id)||this.#F.setValue(t.id,t)}#St(){const t=document.getSelection();if(!t||t.isCollapsed){if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}return}const{anchorNode:e}=t;if(e===this.#ht)return;const i=this.#xt(t).closest(".textLayer");if(i){this.#J?.hide();this.#ht=e;this.#Ct({hasSelectedText:!0});if(this.#ot===u.HIGHLIGHT||this.#ot===u.NONE){this.#ot===u.HIGHLIGHT&&this.showAllEditors("highlight",!0,!0);this.#Q=this.isShiftKeyDown;if(!this.isShiftKeyDown){const t=this.#ot===u.HIGHLIGHT?this.#Et(i):null;t?.toggleDrawing();const e=new AbortController,s=this.combinedSignal(e),pointerup=i=>{if("pointerup"!==i.type||0===i.button){e.abort();t?.toggleDrawing(!0);"pointerup"===i.type&&this.#Tt("main_toolbar")}};window.addEventListener("pointerup",pointerup,{signal:s});window.addEventListener("blur",pointerup,{signal:s})}}}else if(this.#ht){this.#J?.hide();this.#ht=null;this.#Ct({hasSelectedText:!1})}}#Tt(t=""){this.#ot===u.HIGHLIGHT?this.highlightSelection(t):this.#W&&this.#_t()}#wt(){document.addEventListener("selectionchange",this.#St.bind(this),{signal:this._signal})}#Mt(){if(this.#K)return;this.#K=new AbortController;const t=this.combinedSignal(this.#K);window.addEventListener("focus",this.focus.bind(this),{signal:t});window.addEventListener("blur",this.blur.bind(this),{signal:t})}#Dt(){this.#K?.abort();this.#K=null}blur(){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}if(!this.hasSelection)return;const{activeElement:t}=document;for(const e of this.#lt)if(e.div.contains(t)){this.#st=[e,t];e._focusEventsAllowed=!1;break}}focus(){if(!this.#st)return;const[t,e]=this.#st;this.#st=null;e.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this._signal});e.focus()}#vt(){if(this.#it)return;this.#it=new AbortController;const t=this.combinedSignal(this.#it);window.addEventListener("keydown",this.keydown.bind(this),{signal:t});window.addEventListener("keyup",this.keyup.bind(this),{signal:t})}#Pt(){this.#it?.abort();this.#it=null}#kt(){if(this.#N)return;this.#N=new AbortController;const t=this.combinedSignal(this.#N);document.addEventListener("copy",this.copy.bind(this),{signal:t});document.addEventListener("cut",this.cut.bind(this),{signal:t});document.addEventListener("paste",this.paste.bind(this),{signal:t})}#It(){this.#N?.abort();this.#N=null}#yt(){const t=this._signal;document.addEventListener("dragover",this.dragOver.bind(this),{signal:t});document.addEventListener("drop",this.drop.bind(this),{signal:t})}addEditListeners(){this.#vt();this.#kt()}removeEditListeners(){this.#Pt();this.#It()}dragOver(t){for(const{type:e}of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e)){t.dataTransfer.dropEffect="copy";t.preventDefault();return}}drop(t){for(const e of t.dataTransfer.items)for(const i of this.#G)if(i.isHandlingMimeForPasting(e.type)){i.paste(e,this.currentLayer);t.preventDefault();return}}copy(t){t.preventDefault();this.#P?.commitOrRemove();if(!this.hasSelection)return;const e=[];for(const t of this.#lt){const i=t.serialize(!0);i&&e.push(i)}0!==e.length&&t.clipboardData.setData("application/pdfjs",JSON.stringify(e))}cut(t){this.copy(t);this.delete()}async paste(t){t.preventDefault();const{clipboardData:e}=t;for(const t of e.items)for(const e of this.#G)if(e.isHandlingMimeForPasting(t.type)){e.paste(t,this.currentLayer);return}let i=e.getData("application/pdfjs");if(!i)return;try{i=JSON.parse(i)}catch(t){warn(`paste: "${t.message}".`);return}if(!Array.isArray(i))return;this.unselectAll();const s=this.currentLayer;try{const t=[];for(const e of i){const i=await s.deserialize(e);if(!i)return;t.push(i)}const cmd=()=>{for(const e of t)this.#Rt(e);this.#Ft(t)},undo=()=>{for(const e of t)e.remove()};this.addCommands({cmd,undo,mustExec:!0})}catch(t){warn(`paste: "${t.message}".`)}}keydown(t){this.isShiftKeyDown||"Shift"!==t.key||(this.isShiftKeyDown=!0);this.#ot===u.NONE||this.isEditorHandlingKeyboard||AnnotationEditorUIManager._keyboardManager.exec(this,t)}keyup(t){if(this.isShiftKeyDown&&"Shift"===t.key){this.isShiftKeyDown=!1;if(this.#Q){this.#Q=!1;this.#Tt("main_toolbar")}}}onEditingAction({name:t}){switch(t){case"undo":case"redo":case"delete":case"selectAll":this[t]();break;case"highlightSelection":this.highlightSelection("context_menu")}}#Ct(t){if(Object.entries(t).some((([t,e])=>this.#pt[t]!==e))){this._eventBus.dispatch("annotationeditorstateschanged",{source:this,details:Object.assign(this.#pt,t)});this.#ot===u.HIGHLIGHT&&!1===t.hasSelectedEditor&&this.#Lt([[p.HIGHLIGHT_FREE,!0]])}}#Lt(t){this._eventBus.dispatch("annotationeditorparamschanged",{source:this,details:t})}setEditingState(t){if(t){this.#Mt();this.#kt();this.#Ct({isEditing:this.#ot!==u.NONE,isEmpty:this.#Ot(),hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:this.#O.hasSomethingToRedo(),hasSelectedEditor:!1})}else{this.#Dt();this.#It();this.#Ct({isEditing:!1});this.disableUserSelect(!1)}}registerEditorTypes(t){if(!this.#G){this.#G=t;for(const t of this.#G)this.#Lt(t.defaultPropertiesToUpdate)}}getId(){return this.#Z.id}get currentLayer(){return this.#I.get(this.#H)}getLayer(t){return this.#I.get(t)}get currentPageIndex(){return this.#H}addLayer(t){this.#I.set(t.pageIndex,t);this.#tt?t.enable():t.disable()}removeLayer(t){this.#I.delete(t.pageIndex)}async updateMode(t,e=null,i=!1){if(this.#ot!==t){if(this.#At){await this.#At.promise;if(!this.#At)return}this.#At=Promise.withResolvers();this.#B?.commitOrRemove();this.#ot=t;if(t!==u.NONE){t===u.SIGNATURE&&await(this.#dt?.loadSignatures());this.setEditingState(!0);await this.#Nt();this.unselectAll();for(const e of this.#I.values())e.updateMode(t);if(e){for(const t of this.#k.values())if(t.annotationElementId===e||t.id===e){this.setSelected(t);t.enterInEditMode()}else t.unselect();this.#At.resolve()}else{i&&this.addNewEditorFromKeyboard();this.#At.resolve()}}else{this.setEditingState(!1);this.#Bt();this._editorUndoBar?.hide();this.#At.resolve()}}}addNewEditorFromKeyboard(){this.currentLayer.canCreateNewEmptyEditor()&&this.currentLayer.addNewEditor()}updateToolbar(t){t.mode!==this.#ot&&this._eventBus.dispatch("switchannotationeditormode",{source:this,...t})}updateParams(t,e){if(this.#G){switch(t){case p.CREATE:this.currentLayer.addNewEditor(e);return;case p.HIGHLIGHT_DEFAULT_COLOR:this.#nt?.updateColor(e);break;case p.HIGHLIGHT_SHOW_ALL:this._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:{type:"highlight",action:"toggle_visibility"}}});(this.#ut||=new Map).set(t,e);this.showAllEditors("highlight",e)}for(const i of this.#lt)i.updateParams(t,e);for(const i of this.#G)i.updateDefaultParams(t,e)}}showAllEditors(t,e,i=!1){for(const i of this.#k.values())i.editorType===t&&i.show(e);(this.#ut?.get(p.HIGHLIGHT_SHOW_ALL)??!0)!==e&&this.#Lt([[p.HIGHLIGHT_SHOW_ALL,e]])}enableWaiting(t=!1){if(this.#et!==t){this.#et=t;for(const e of this.#I.values()){t?e.disableClick():e.enableClick();e.div.classList.toggle("waiting",t)}}}async#Nt(){if(!this.#tt){this.#tt=!0;const t=[];for(const e of this.#I.values())t.push(e.enable());await Promise.all(t);for(const t of this.#k.values())t.enable()}}#Bt(){this.unselectAll();if(this.#tt){this.#tt=!1;for(const t of this.#I.values())t.disable();for(const t of this.#k.values())t.disable()}}getEditors(t){const e=[];for(const i of this.#k.values())i.pageIndex===t&&e.push(i);return e}getEditor(t){return this.#k.get(t)}addEditor(t){this.#k.set(t.id,t)}removeEditor(t){if(t.div.contains(document.activeElement)){this.#X&&clearTimeout(this.#X);this.#X=setTimeout((()=>{this.focusMainContainer();this.#X=null}),0)}this.#k.delete(t.id);t.annotationElementId&&this.#at?.delete(t.annotationElementId);this.unselect(t);t.annotationElementId&&this.#U.has(t.annotationElementId)||this.#F?.remove(t.id)}addDeletedAnnotationElement(t){this.#U.add(t.annotationElementId);this.addChangedExistingAnnotation(t);t.deleted=!0}isDeletedAnnotationElement(t){return this.#U.has(t)}removeDeletedAnnotationElement(t){this.#U.delete(t.annotationElementId);this.removeChangedExistingAnnotation(t);t.deleted=!1}#Rt(t){const e=this.#I.get(t.pageIndex);if(e)e.addOrRebuild(t);else{this.addEditor(t);this.addToAnnotationStorage(t)}}setActiveEditor(t){if(this.#P!==t){this.#P=t;t&&this.#Lt(t.propertiesToUpdate)}}get#Ht(){let t=null;for(t of this.#lt);return t}updateUI(t){this.#Ht===t&&this.#Lt(t.propertiesToUpdate)}updateUIForDefaultProperties(t){this.#Lt(t.defaultPropertiesToUpdate)}toggleSelected(t){if(this.#lt.has(t)){this.#lt.delete(t);t.unselect();this.#Ct({hasSelectedEditor:this.hasSelection})}else{this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}}setSelected(t){this.#B?.commitOrRemove();for(const e of this.#lt)e!==t&&e.unselect();this.#lt.clear();this.#lt.add(t);t.select();this.#Lt(t.propertiesToUpdate);this.#Ct({hasSelectedEditor:!0})}isSelected(t){return this.#lt.has(t)}get firstSelectedEditor(){return this.#lt.values().next().value}unselect(t){t.unselect();this.#lt.delete(t);this.#Ct({hasSelectedEditor:this.hasSelection})}get hasSelection(){return 0!==this.#lt.size}get isEnterHandled(){return 1===this.#lt.size&&this.firstSelectedEditor.isEnterHandled}undo(){this.#O.undo();this.#Ct({hasSomethingToUndo:this.#O.hasSomethingToUndo(),hasSomethingToRedo:!0,isEmpty:this.#Ot()});this._editorUndoBar?.hide()}redo(){this.#O.redo();this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:this.#O.hasSomethingToRedo(),isEmpty:this.#Ot()})}addCommands(t){this.#O.add(t);this.#Ct({hasSomethingToUndo:!0,hasSomethingToRedo:!1,isEmpty:this.#Ot()})}cleanUndoStack(t){this.#O.cleanType(t)}#Ot(){if(0===this.#k.size)return!0;if(1===this.#k.size)for(const t of this.#k.values())return t.isEmpty();return!1}delete(){this.commitOrRemove();const t=this.currentLayer?.endDrawingSession(!0);if(!this.hasSelection&&!t)return;const e=t?[t]:[...this.#lt],undo=()=>{for(const t of e)this.#Rt(t)};this.addCommands({cmd:()=>{this._editorUndoBar?.show(undo,1===e.length?e[0].editorType:e.length);for(const t of e)t.remove()},undo,mustExec:!0})}commitOrRemove(){this.#P?.commitOrRemove()}hasSomethingToControl(){return this.#P||this.hasSelection}#Ft(t){for(const t of this.#lt)t.unselect();this.#lt.clear();for(const e of t)if(!e.isEmpty()){this.#lt.add(e);e.select()}this.#Ct({hasSelectedEditor:this.hasSelection})}selectAll(){for(const t of this.#lt)t.commit();this.#Ft(this.#k.values())}unselectAll(){if(this.#P){this.#P.commitOrRemove();if(this.#ot!==u.NONE)return}if(!this.#B?.commitOrRemove()&&this.hasSelection){for(const t of this.#lt)t.unselect();this.#lt.clear();this.#Ct({hasSelectedEditor:!1})}}translateSelectedEditors(t,e,i=!1){i||this.commitOrRemove();if(!this.hasSelection)return;this.#gt[0]+=t;this.#gt[1]+=e;const[s,n]=this.#gt,a=[...this.#lt];this.#ft&&clearTimeout(this.#ft);this.#ft=setTimeout((()=>{this.#ft=null;this.#gt[0]=this.#gt[1]=0;this.addCommands({cmd:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(s,n);t.translationDone()}},undo:()=>{for(const t of a)if(this.#k.has(t.id)){t.translateInPage(-s,-n);t.translationDone()}},mustExec:!1})}),1e3);for(const i of a){i.translateInPage(t,e);i.translationDone()}}setUpDragSession(){if(this.hasSelection){this.disableUserSelect(!0);this.#z=new Map;for(const t of this.#lt)this.#z.set(t,{savedX:t.x,savedY:t.y,savedPageIndex:t.pageIndex,newX:0,newY:0,newPageIndex:-1})}}endDragSession(){if(!this.#z)return!1;this.disableUserSelect(!1);const t=this.#z;this.#z=null;let e=!1;for(const[{x:i,y:s,pageIndex:n},a]of t){a.newX=i;a.newY=s;a.newPageIndex=n;e||=i!==a.savedX||s!==a.savedY||n!==a.savedPageIndex}if(!e)return!1;const move=(t,e,i,s)=>{if(this.#k.has(t.id)){const n=this.#I.get(s);if(n)t._setParentAndPosition(n,e,i);else{t.pageIndex=s;t.x=e;t.y=i}}};this.addCommands({cmd:()=>{for(const[e,{newX:i,newY:s,newPageIndex:n}]of t)move(e,i,s,n)},undo:()=>{for(const[e,{savedX:i,savedY:s,savedPageIndex:n}]of t)move(e,i,s,n)},mustExec:!0});return!0}dragSelectedEditors(t,e){if(this.#z)for(const i of this.#z.keys())i.drag(t,e)}rebuild(t){if(null===t.parent){const e=this.getLayer(t.pageIndex);if(e){e.changeParent(t);e.addOrRebuild(t)}else{this.addEditor(t);this.addToAnnotationStorage(t);t.rebuild()}}else t.parent.addOrRebuild(t)}get isEditorHandlingKeyboard(){return this.getActive()?.shouldGetKeyboardEvents()||1===this.#lt.size&&this.firstSelectedEditor.shouldGetKeyboardEvents()}isActive(t){return this.#P===t}getActive(){return this.#P}getMode(){return this.#ot}get imageManager(){return shadow(this,"imageManager",new ImageManager)}getSelectionBoxes(t){if(!t)return null;const e=document.getSelection();for(let i=0,s=e.rangeCount;i({x:(e-s)/a,y:1-(t+r-i)/n,width:o/a,height:r/n});break;case"180":r=(t,e,r,o)=>({x:1-(t+r-i)/n,y:1-(e+o-s)/a,width:r/n,height:o/a});break;case"270":r=(t,e,r,o)=>({x:1-(e+o-s)/a,y:(t-i)/n,width:o/a,height:r/n});break;default:r=(t,e,r,o)=>({x:(t-i)/n,y:(e-s)/a,width:r/n,height:o/a})}const o=[];for(let t=0,i=e.rangeCount;tt.stopPropagation()),{signal:i});const onClick=t=>{t.preventDefault();this.#a._uiManager.editAltText(this.#a);this.#Kt&&this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_clicked",data:{label:this.#Qt}})};t.addEventListener("click",onClick,{capture:!0,signal:i});t.addEventListener("keydown",(e=>{if(e.target===t&&"Enter"===e.key){this.#Vt=!0;onClick(e)}}),{signal:i});await this.#Jt();return t}get#Qt(){return(this.#o?"added":null===this.#o&&this.guessedText&&"review")||"missing"}finish(){if(this.#zt){this.#zt.focus({focusVisible:this.#Vt});this.#Vt=!1}}isEmpty(){return this.#Kt?null===this.#o:!this.#o&&!this.#Ut}hasData(){return this.#Kt?null!==this.#o||!!this.#qt:this.isEmpty()}get guessedText(){return this.#qt}async setGuessedText(t){if(null===this.#o){this.#qt=t;this.#Xt=await AltText._l10n.get("pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",{generatedAltText:t});this.#Jt()}}toggleAltTextBadge(t=!1){if(this.#Kt&&!this.#o){if(!this.#jt){const t=this.#jt=document.createElement("div");t.className="noAltTextBadge";this.#a.div.append(t)}this.#jt.classList.toggle("hidden",!t)}else{this.#jt?.remove();this.#jt=null}}serialize(t){let e=this.#o;t||this.#qt!==e||(e=this.#Xt);return{altText:e,decorative:this.#Ut,guessedText:this.#qt,textWithDisclaimer:this.#Xt}}get data(){return{altText:this.#o,decorative:this.#Ut}}set data({altText:t,decorative:e,guessedText:i,textWithDisclaimer:s,cancel:n=!1}){if(i){this.#qt=i;this.#Xt=s}if(this.#o!==t||this.#Ut!==e){if(!n){this.#o=t;this.#Ut=e}this.#Jt()}}toggle(t=!1){if(this.#zt){if(!t&&this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#zt.disabled=!t}}shown(){this.#a._reportTelemetry({action:"pdfjs.image.alt_text.image_status_label_displayed",data:{label:this.#Qt}})}destroy(){this.#zt?.remove();this.#zt=null;this.#Gt=null;this.#$t=null;this.#jt?.remove();this.#jt=null}async#Jt(){const t=this.#zt;if(!t)return;if(this.#Kt){t.classList.toggle("done",!!this.#o);t.setAttribute("data-l10n-id",AltText.#Yt[this.#Qt]);this.#Gt?.setAttribute("data-l10n-id",AltText.#Yt[`${this.#Qt}-label`]);if(!this.#o){this.#$t?.remove();return}}else{if(!this.#o&&!this.#Ut){t.classList.remove("done");this.#$t?.remove();return}t.classList.add("done");t.setAttribute("data-l10n-id","pdfjs-editor-alt-text-edit-button")}let e=this.#$t;if(!e){this.#$t=e=document.createElement("span");e.className="tooltip";e.setAttribute("role","tooltip");e.id=`alt-text-tooltip-${this.#a.id}`;const i=100,s=this.#a._uiManager._signal;s.addEventListener("abort",(()=>{clearTimeout(this.#Wt);this.#Wt=null}),{once:!0});t.addEventListener("mouseenter",(()=>{this.#Wt=setTimeout((()=>{this.#Wt=null;this.#$t.classList.add("show");this.#a._reportTelemetry({action:"alt_text_tooltip"})}),i)}),{signal:s});t.addEventListener("mouseleave",(()=>{if(this.#Wt){clearTimeout(this.#Wt);this.#Wt=null}this.#$t?.classList.remove("show")}),{signal:s})}if(this.#Ut)e.setAttribute("data-l10n-id","pdfjs-editor-alt-text-decorative-tooltip");else{e.removeAttribute("data-l10n-id");e.textContent=this.#o}e.parentNode||t.append(e);const i=this.#a.getElementForAltText();i?.setAttribute("aria-describedby",e.id)}}class TouchManager{#mt;#Zt=!1;#te=null;#ee;#ie;#se;#ne;#ae=null;#re;#oe=null;#le;#he=null;constructor({container:t,isPinchingDisabled:e=null,isPinchingStopped:i=null,onPinchStart:s=null,onPinching:n=null,onPinchEnd:a=null,signal:r}){this.#mt=t;this.#te=i;this.#ee=e;this.#ie=s;this.#se=n;this.#ne=a;this.#le=new AbortController;this.#re=AbortSignal.any([r,this.#le.signal]);t.addEventListener("touchstart",this.#de.bind(this),{passive:!1,signal:this.#re})}get MIN_TOUCH_DISTANCE_TO_PINCH(){return 35/OutputScale.pixelRatio}#de(t){if(this.#ee?.())return;if(1===t.touches.length){if(this.#ae)return;const t=this.#ae=new AbortController,e=AbortSignal.any([this.#re,t.signal]),i=this.#mt,s={capture:!0,signal:e,passive:!1},cancelPointerDown=t=>{if("touch"===t.pointerType){this.#ae?.abort();this.#ae=null}};i.addEventListener("pointerdown",(t=>{if("touch"===t.pointerType){stopEvent(t);cancelPointerDown(t)}}),s);i.addEventListener("pointerup",cancelPointerDown,s);i.addEventListener("pointercancel",cancelPointerDown,s);return}if(!this.#he){this.#he=new AbortController;const t=AbortSignal.any([this.#re,this.#he.signal]),e=this.#mt,i={signal:t,capture:!1,passive:!1};e.addEventListener("touchmove",this.#ce.bind(this),i);const s=this.#ue.bind(this);e.addEventListener("touchend",s,i);e.addEventListener("touchcancel",s,i);i.capture=!0;e.addEventListener("pointerdown",stopEvent,i);e.addEventListener("pointermove",stopEvent,i);e.addEventListener("pointercancel",stopEvent,i);e.addEventListener("pointerup",stopEvent,i);this.#ie?.()}stopEvent(t);if(2!==t.touches.length||this.#te?.()){this.#oe=null;return}let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);this.#oe={touch0X:e.screenX,touch0Y:e.screenY,touch1X:i.screenX,touch1Y:i.screenY}}#ce(t){if(!this.#oe||2!==t.touches.length)return;stopEvent(t);let[e,i]=t.touches;e.identifier>i.identifier&&([e,i]=[i,e]);const{screenX:s,screenY:n}=e,{screenX:a,screenY:r}=i,o=this.#oe,{touch0X:l,touch0Y:h,touch1X:d,touch1Y:c}=o,u=d-l,p=c-h,g=a-s,f=r-n,m=Math.hypot(g,f)||1,b=Math.hypot(u,p)||1;if(!this.#Zt&&Math.abs(b-m)<=TouchManager.MIN_TOUCH_DISTANCE_TO_PINCH)return;o.touch0X=s;o.touch0Y=n;o.touch1X=a;o.touch1Y=r;if(!this.#Zt){this.#Zt=!0;return}const A=[(s+a)/2,(n+r)/2];this.#se?.(A,b,m)}#ue(t){if(!(t.touches.length>=2)){if(this.#he){this.#he.abort();this.#he=null;this.#ne?.()}if(this.#oe){stopEvent(t);this.#oe=null;this.#Zt=!1}}}destroy(){this.#le?.abort();this.#le=null;this.#ae?.abort();this.#ae=null}}class AnnotationEditor{#pe=null;#ge=null;#o=null;#fe=!1;#me=null;#be="";#Ae=!1;#we=null;#ye=null;#ve=null;#xe=null;#Ee="";#_e=!1;#Se=null;#Ce=!1;#Te=!1;#Me=!1;#De=null;#Pe=0;#ke=0;#Ie=null;#Re=null;isSelected=!1;_isCopy=!1;_editToolbar=null;_initialOptions=Object.create(null);_initialData=null;_isVisible=!0;_uiManager=null;_focusEventsAllowed=!0;static _l10n=null;static _l10nResizer=null;#Fe=!1;#Le=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new ColorManager;static _zIndex=1;static _telemetryTimeout=1e3;static get _resizerKeyboardManager(){const t=AnnotationEditor.prototype._resizeWithKeyboard,e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_resizerKeyboardManager",new KeyboardManager([[["ArrowLeft","mac+ArrowLeft"],t,{args:[-e,0]}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t,{args:[-i,0]}],[["ArrowRight","mac+ArrowRight"],t,{args:[e,0]}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t,{args:[i,0]}],[["ArrowUp","mac+ArrowUp"],t,{args:[0,-e]}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t,{args:[0,-i]}],[["ArrowDown","mac+ArrowDown"],t,{args:[0,e]}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t,{args:[0,i]}],[["Escape","mac+Escape"],AnnotationEditor.prototype._stopResizingWithKeyboard]]))}constructor(t){this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:n,pageY:a}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[n,a];const[r,o]=this.parentDimensions;this.x=t.x/r;this.y=t.y/o;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get isDrawer(){return!1}static get _defaultLineColor(){return shadow(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e){AnnotationEditor._l10n??=t;AnnotationEditor._l10nResizer||=Object.freeze({topLeft:"pdfjs-editor-resizer-top-left",topMiddle:"pdfjs-editor-resizer-top-middle",topRight:"pdfjs-editor-resizer-top-right",middleRight:"pdfjs-editor-resizer-middle-right",bottomRight:"pdfjs-editor-resizer-bottom-right",bottomMiddle:"pdfjs-editor-resizer-bottom-middle",bottomLeft:"pdfjs-editor-resizer-bottom-left",middleLeft:"pdfjs-editor-resizer-middle-left"});if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){unreachable("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#Fe}set _isDraggable(t){this.#Fe=t;this.div?.classList.toggle("draggable",t)}get isEnterHandled(){return!0}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#Le}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}else this.#Oe();this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#_e?this.#_e=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}_moveAfterPaste(t,e){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s);this._onTranslated()}#Ne([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this._onTranslating(this.x,this.y);this.fixAndSetPosition()}translate(t,e){this.#Ne(this.parentDimensions,t,e)}translateInPage(t,e){this.#Se||=[this.x,this.y,this.width,this.height];this.#Ne(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}translationDone(){this._onTranslated(this.x,this.y)}drag(t,e){this.#Se||=[this.x,this.y,this.width,this.height];const{div:i,parentDimensions:[s,n]}=this;this.x+=t/s;this.y+=e/n;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:a,y:r}=this;const[o,l]=this.getBaseTranslation();a+=o;r+=l;const{style:h}=i;h.left=`${(100*a).toFixed(2)}%`;h.top=`${(100*r).toFixed(2)}%`;this._onTranslating(a,r);i.scrollIntoView({block:"nearest"})}_onTranslating(t,e){}_onTranslated(t,e){}get _hasBeenMoved(){return!!this.#Se&&(this.#Se[0]!==this.x||this.#Se[1]!==this.y)}get _hasBeenResized(){return!!this.#Se&&(this.#Se[2]!==this.width||this.#Se[3]!==this.height)}getBaseTranslation(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}get _mustFixPosition(){return!0}fixAndSetPosition(t=this.rotation){const{div:{style:e},pageDimensions:[i,s]}=this;let{x:n,y:a,width:r,height:o}=this;r*=i;o*=s;n*=i;a*=s;if(this._mustFixPosition)switch(t){case 0:n=MathClamp(n,0,i-r);a=MathClamp(a,0,s-o);break;case 90:n=MathClamp(n,0,i-o);a=MathClamp(a,r,s);break;case 180:n=MathClamp(n,r,i);a=MathClamp(a,o,s);break;case 270:n=MathClamp(n,o,i);a=MathClamp(a,0,s-r)}this.x=n/=i;this.y=a/=s;const[l,h]=this.getBaseTranslation();n+=l;a+=h;e.left=`${(100*n).toFixed(2)}%`;e.top=`${(100*a).toFixed(2)}%`;this.moveInDOM()}static#Be(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#Be(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#Be(t,e,360-this.parentRotation)}#He(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this;return[e*t,i*t]}setDims(t,e){const[i,s]=this.parentDimensions,{style:n}=this.div;n.width=`${(100*t/i).toFixed(2)}%`;this.#Ae||(n.height=`${(100*e/s).toFixed(2)}%`)}fixDims(){const{style:t}=this.div,{height:e,width:i}=t,s=i.endsWith("%"),n=!this.#Ae&&e.endsWith("%");if(s&&n)return;const[a,r]=this.parentDimensions;s||(t.width=`${(100*parseFloat(i)/a).toFixed(2)}%`);this.#Ae||n||(t.height=`${(100*parseFloat(e)/r).toFixed(2)}%`)}getInitialTranslation(){return[0,0]}#Ue(){if(this.#we)return;this.#we=document.createElement("div");this.#we.classList.add("resizers");const t=this._willKeepAspectRatio?["topLeft","topRight","bottomRight","bottomLeft"]:["topLeft","topMiddle","topRight","middleRight","bottomRight","bottomMiddle","bottomLeft","middleLeft"],e=this._uiManager._signal;for(const i of t){const t=document.createElement("div");this.#we.append(t);t.classList.add("resizer",i);t.setAttribute("data-resizer-name",i);t.addEventListener("pointerdown",this.#ze.bind(this,i),{signal:e});t.addEventListener("contextmenu",noContextMenu,{signal:e});t.tabIndex=-1}this.div.prepend(this.#we)}#ze(t,e){e.preventDefault();const{isMac:i}=util_FeatureTest.platform;if(0!==e.button||e.ctrlKey&&i)return;this.#o?.toggle(!1);const s=this._isDraggable;this._isDraggable=!1;this.#ye=[e.screenX,e.screenY];const n=new AbortController,a=this._uiManager.combinedSignal(n);this.parent.togglePointerEvents(!1);window.addEventListener("pointermove",this.#Ge.bind(this,t),{passive:!0,capture:!0,signal:a});window.addEventListener("touchmove",stopEvent,{passive:!1,signal:a});window.addEventListener("contextmenu",noContextMenu,{signal:a});this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const r=this.parent.div.style.cursor,o=this.div.style.cursor;this.div.style.cursor=this.parent.div.style.cursor=window.getComputedStyle(e.target).cursor;const pointerUpCallback=()=>{n.abort();this.parent.togglePointerEvents(!0);this.#o?.toggle(!0);this._isDraggable=s;this.parent.div.style.cursor=r;this.div.style.cursor=o;this.#$e()};window.addEventListener("pointerup",pointerUpCallback,{signal:a});window.addEventListener("blur",pointerUpCallback,{signal:a})}#We(t,e,i,s){this.width=i;this.height=s;this.x=t;this.y=e;const[n,a]=this.parentDimensions;this.setDims(n*i,a*s);this.fixAndSetPosition();this._onResized()}_onResized(){}#$e(){if(!this.#ve)return;const{savedX:t,savedY:e,savedWidth:i,savedHeight:s}=this.#ve;this.#ve=null;const n=this.x,a=this.y,r=this.width,o=this.height;n===t&&a===e&&r===i&&o===s||this.addCommands({cmd:this.#We.bind(this,n,a,r,o),undo:this.#We.bind(this,t,e,i,s),mustExec:!0})}static _round(t){return Math.round(1e4*t)/1e4}#Ge(t,e){const[i,s]=this.parentDimensions,n=this.x,a=this.y,r=this.width,o=this.height,l=AnnotationEditor.MIN_SIZE/i,h=AnnotationEditor.MIN_SIZE/s,d=this.#He(this.rotation),transf=(t,e)=>[d[0]*t+d[2]*e,d[1]*t+d[3]*e],c=this.#He(360-this.rotation);let u,p,g=!1,f=!1;switch(t){case"topLeft":g=!0;u=(t,e)=>[0,0];p=(t,e)=>[t,e];break;case"topMiddle":u=(t,e)=>[t/2,0];p=(t,e)=>[t/2,e];break;case"topRight":g=!0;u=(t,e)=>[t,0];p=(t,e)=>[0,e];break;case"middleRight":f=!0;u=(t,e)=>[t,e/2];p=(t,e)=>[0,e/2];break;case"bottomRight":g=!0;u=(t,e)=>[t,e];p=(t,e)=>[0,0];break;case"bottomMiddle":u=(t,e)=>[t/2,e];p=(t,e)=>[t/2,0];break;case"bottomLeft":g=!0;u=(t,e)=>[0,e];p=(t,e)=>[t,0];break;case"middleLeft":f=!0;u=(t,e)=>[0,e/2];p=(t,e)=>[t,e/2]}const m=u(r,o),b=p(r,o);let A=transf(...b);const w=AnnotationEditor._round(n+A[0]),y=AnnotationEditor._round(a+A[1]);let v,x,E=1,_=1;if(e.fromKeyboard)({deltaX:v,deltaY:x}=e);else{const{screenX:t,screenY:i}=e,[s,n]=this.#ye;[v,x]=this.screenToPageTranslation(t-s,i-n);this.#ye[0]=t;this.#ye[1]=i}[v,x]=(S=v/i,C=x/s,[c[0]*S+c[2]*C,c[1]*S+c[3]*C]);var S,C;if(g){const t=Math.hypot(r,o);E=_=Math.max(Math.min(Math.hypot(b[0]-m[0]-v,b[1]-m[1]-x)/t,1/r,1/o),l/r,h/o)}else f?E=MathClamp(Math.abs(b[0]-m[0]-v),l,1)/r:_=MathClamp(Math.abs(b[1]-m[1]-x),h,1)/o;const T=AnnotationEditor._round(r*E),M=AnnotationEditor._round(o*_);A=transf(...p(T,M));const D=w-A[0],P=y-A[1];this.#Se||=[this.x,this.y,this.width,this.height];this.width=T;this.height=M;this.x=D;this.y=P;this.setDims(i*T,s*M);this.fixAndSetPosition();this._onResizing()}_onResizing(){}altTextFinish(){this.#o?.finish()}async addEditToolbar(){if(this._editToolbar||this.#Te)return this._editToolbar;this._editToolbar=new EditorToolbar(this);this.div.append(this._editToolbar.render());this.#o&&await this._editToolbar.addAltText(this.#o);return this._editToolbar}removeEditToolbar(){if(this._editToolbar){this._editToolbar.remove();this._editToolbar=null;this.#o?.destroy()}}addContainer(t){const e=this._editToolbar?.div;e?e.before(t):this.div.append(t)}getClientDimensions(){return this.div.getBoundingClientRect()}async addAltTextButton(){if(!this.#o){AltText.initialize(AnnotationEditor._l10n);this.#o=new AltText(this);if(this.#pe){this.#o.data=this.#pe;this.#pe=null}await this.addEditToolbar()}}get altTextData(){return this.#o?.data}set altTextData(t){this.#o&&(this.#o.data=t)}get guessedAltText(){return this.#o?.guessedText}async setGuessedAltText(t){await(this.#o?.setGuessedText(t))}serializeAltText(t){return this.#o?.serialize(t)}hasAltText(){return!!this.#o&&!this.#o.isEmpty()}hasAltTextData(){return this.#o?.hasData()??!1}render(){const t=this.div=document.createElement("div");t.setAttribute("data-editor-rotation",(360-this.rotation)%360);t.className=this.name;t.setAttribute("id",this.id);t.tabIndex=this.#fe?-1:0;t.setAttribute("role","application");this.defaultL10nId&&t.setAttribute("data-l10n-id",this.defaultL10nId);this._isVisible||t.classList.add("hidden");this.setInForeground();this.#Ve();const[e,i]=this.parentDimensions;if(this.parentRotation%180!=0){t.style.maxWidth=`${(100*i/e).toFixed(2)}%`;t.style.maxHeight=`${(100*e/i).toFixed(2)}%`}const[s,n]=this.getInitialTranslation();this.translate(s,n);bindEvents(this,t,["keydown","pointerdown","dblclick"]);this.isResizable&&this._uiManager._supportsPinchToZoom&&(this.#Re||=new TouchManager({container:t,isPinchingDisabled:()=>!this.isSelected,onPinchStart:this.#je.bind(this),onPinching:this.#qe.bind(this),onPinchEnd:this.#Xe.bind(this),signal:this._uiManager._signal}));this._uiManager._editorUndoBar?.hide();return t}#je(){this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};this.#o?.toggle(!1);this.parent.togglePointerEvents(!1)}#qe(t,e,i){let s=i/e*.7+1-.7;if(1===s)return;const n=this.#He(this.rotation),transf=(t,e)=>[n[0]*t+n[2]*e,n[1]*t+n[3]*e],[a,r]=this.parentDimensions,o=this.x,l=this.y,h=this.width,d=this.height,c=AnnotationEditor.MIN_SIZE/a,u=AnnotationEditor.MIN_SIZE/r;s=Math.max(Math.min(s,1/h,1/d),c/h,u/d);const p=AnnotationEditor._round(h*s),g=AnnotationEditor._round(d*s);if(p===h&&g===d)return;this.#Se||=[o,l,h,d];const f=transf(h/2,d/2),m=AnnotationEditor._round(o+f[0]),b=AnnotationEditor._round(l+f[1]),A=transf(p/2,g/2);this.x=m-A[0];this.y=b-A[1];this.width=p;this.height=g;this.setDims(a*p,r*g);this.fixAndSetPosition();this._onResizing()}#Xe(){this.#o?.toggle(!0);this.parent.togglePointerEvents(!0);this.#$e()}pointerdown(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)t.preventDefault();else{this.#_e=!0;this._isDraggable?this.#Ke(t):this.#Ye(t)}}#Ye(t){const{isMac:e}=util_FeatureTest.platform;t.ctrlKey&&!e||t.shiftKey||t.metaKey&&e?this.parent.toggleSelected(this):this.parent.setSelected(this)}#Ke(t){const{isSelected:e}=this;this._uiManager.setUpDragSession();let i=!1;const s=new AbortController,n=this._uiManager.combinedSignal(s),a={capture:!0,passive:!1,signal:n},cancelDrag=t=>{s.abort();this.#me=null;this.#_e=!1;this._uiManager.endDragSession()||this.#Ye(t);i&&this._onStopDragging()};if(e){this.#Pe=t.clientX;this.#ke=t.clientY;this.#me=t.pointerId;this.#be=t.pointerType;window.addEventListener("pointermove",(t=>{if(!i){i=!0;this._onStartDragging()}const{clientX:e,clientY:s,pointerId:n}=t;if(n!==this.#me){stopEvent(t);return}const[a,r]=this.screenToPageTranslation(e-this.#Pe,s-this.#ke);this.#Pe=e;this.#ke=s;this._uiManager.dragSelectedEditors(a,r)}),a);window.addEventListener("touchmove",stopEvent,a);window.addEventListener("pointerdown",(t=>{t.pointerType===this.#be&&(this.#Re||t.isPrimary)&&cancelDrag(t);stopEvent(t)}),a)}const pointerUpCallback=t=>{this.#me&&this.#me!==t.pointerId?stopEvent(t):cancelDrag(t)};window.addEventListener("pointerup",pointerUpCallback,{signal:n});window.addEventListener("blur",pointerUpCallback,{signal:n})}_onStartDragging(){}_onStopDragging(){}moveInDOM(){this.#De&&clearTimeout(this.#De);this.#De=setTimeout((()=>{this.#De=null;this.parent?.moveEditorInDOM(this)}),0)}_setParentAndPosition(t,e,i){t.changeParent(this);this.x=e;this.y=i;this.fixAndSetPosition();this._onTranslated()}getRect(t,e,i=this.rotation){const s=this.parentScale,[n,a]=this.pageDimensions,[r,o]=this.pageTranslation,l=t/s,h=e/s,d=this.x*n,c=this.y*a,u=this.width*n,p=this.height*a;switch(i){case 0:return[d+l+r,a-c-h-p+o,d+l+u+r,a-c-h+o];case 90:return[d+h+r,a-c+l+o,d+h+p+r,a-c+l+u+o];case 180:return[d-l-u+r,a-c+h+o,d-l+r,a-c+h+p+o];case 270:return[d-h-p+r,a-c-l-u+o,d-h+r,a-c-l+o];default:throw new Error("Invalid rotation")}}getRectInCurrentCoords(t,e){const[i,s,n,a]=t,r=n-i,o=a-s;switch(this.rotation){case 0:return[i,e-a,r,o];case 90:return[i,e-s,o,r];case 180:return[n,e-s,r,o];case 270:return[n,e-a,o,r];default:throw new Error("Invalid rotation")}}onceAdded(t){}isEmpty(){return!1}enableEditMode(){if(this.isInEditMode())return!1;this.parent.setEditingState(!1);this.#Te=!0;return!0}disableEditMode(){if(!this.isInEditMode())return!1;this.parent.setEditingState(!0);this.#Te=!1;return!0}isInEditMode(){return this.#Te}shouldGetKeyboardEvents(){return this.#Me}needsToBeRebuilt(){return this.div&&!this.isAttachedToDOM}get isOnScreen(){const{top:t,left:e,bottom:i,right:s}=this.getClientDimensions(),{innerHeight:n,innerWidth:a}=window;return e0&&t0}#Ve(){if(this.#xe||!this.div)return;this.#xe=new AbortController;const t=this._uiManager.combinedSignal(this.#xe);this.div.addEventListener("focusin",this.focusin.bind(this),{signal:t});this.div.addEventListener("focusout",this.focusout.bind(this),{signal:t})}rebuild(){this.#Ve()}rotate(t){}resize(){}serializeDeleted(){return{id:this.annotationElementId,deleted:!0,pageIndex:this.pageIndex,popupRef:this._initialData?.popupRef||""}}serialize(t=!1,e=null){unreachable("An editor must be serializable")}static async deserialize(t,e,i){const s=new this.prototype.constructor({parent:e,id:e.getNextId(),uiManager:i});s.rotation=t.rotation;s.#pe=t.accessibilityData;s._isCopy=t.isCopy||!1;const[n,a]=s.pageDimensions,[r,o,l,h]=s.getRectInCurrentCoords(t.rect,a);s.x=r/n;s.y=o/a;s.width=l/n;s.height=h/a;return s}get hasBeenModified(){return!!this.annotationElementId&&(this.deleted||null!==this.serialize())}remove(){this.#xe?.abort();this.#xe=null;this.isEmpty()||this.commit();this.parent?this.parent.remove(this):this._uiManager.removeEditor(this);if(this.#De){clearTimeout(this.#De);this.#De=null}this.#Oe();this.removeEditToolbar();if(this.#Ie){for(const t of this.#Ie.values())clearTimeout(t);this.#Ie=null}this.parent=null;this.#Re?.destroy();this.#Re=null}get isResizable(){return!1}makeResizable(){if(this.isResizable){this.#Ue();this.#we.classList.remove("hidden")}}get toolbarPosition(){return null}keydown(t){if(!this.isResizable||t.target!==this.div||"Enter"!==t.key)return;this._uiManager.setSelected(this);this.#ve={savedX:this.x,savedY:this.y,savedWidth:this.width,savedHeight:this.height};const e=this.#we.children;if(!this.#ge){this.#ge=Array.from(e);const t=this.#Qe.bind(this),i=this.#Je.bind(this),s=this._uiManager._signal;for(const e of this.#ge){const n=e.getAttribute("data-resizer-name");e.setAttribute("role","spinbutton");e.addEventListener("keydown",t,{signal:s});e.addEventListener("blur",i,{signal:s});e.addEventListener("focus",this.#Ze.bind(this,n),{signal:s});e.setAttribute("data-l10n-id",AnnotationEditor._l10nResizer[n])}}const i=this.#ge[0];let s=0;for(const t of e){if(t===i)break;s++}const n=(360-this.rotation+this.parentRotation)%360/90*(this.#ge.length/4);if(n!==s){if(ns)for(let t=0;t{this.div?.classList.contains("selectedEditor")&&this._editToolbar?.show()}))}}unselect(){if(this.isSelected){this.isSelected=!1;this.#we?.classList.add("hidden");this.div?.classList.remove("selectedEditor");this.div?.contains(document.activeElement)&&this._uiManager.currentLayer.div.focus({preventScroll:!0});this._editToolbar?.hide();this.#o?.toggleAltTextBadge(!0)}}updateParams(t,e){}disableEditing(){}enableEditing(){}get canChangeContent(){return!1}enterInEditMode(){if(this.canChangeContent){this.enableEditMode();this.div.focus()}}dblclick(t){this.enterInEditMode();this.parent.updateToolbar({mode:this.constructor._editorType,editId:this.id})}getElementForAltText(){return this.div}get contentDiv(){return this.div}get isEditing(){return this.#Ce}set isEditing(t){this.#Ce=t;if(this.parent)if(t){this.parent.setSelected(this);this.parent.setActiveEditor(this)}else this.parent.setActiveEditor(null)}setAspectRatio(t,e){this.#Ae=!0;const i=t/e,{style:s}=this.div;s.aspectRatio=i;s.height="auto"}static get MIN_SIZE(){return 16}static canCreateNewEmptyEditor(){return!0}get telemetryInitialData(){return{action:"added"}}get telemetryFinalData(){return null}_reportTelemetry(t,e=!1){if(e){this.#Ie||=new Map;const{action:e}=t;let i=this.#Ie.get(e);i&&clearTimeout(i);i=setTimeout((()=>{this._reportTelemetry(t);this.#Ie.delete(e);0===this.#Ie.size&&(this.#Ie=null)}),AnnotationEditor._telemetryTimeout);this.#Ie.set(e,i)}else{t.type||=this.editorType;this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",data:t}})}}show(t=this._isVisible){this.div.classList.toggle("hidden",!t);this._isVisible=t}enable(){this.div&&(this.div.tabIndex=0);this.#fe=!1}disable(){this.div&&(this.div.tabIndex=-1);this.#fe=!0}renderAnnotationElement(t){let e=t.container.querySelector(".annotationContent");if(e){if("CANVAS"===e.nodeName){const t=e;e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.before(e)}}else{e=document.createElement("div");e.classList.add("annotationContent",this.editorType);t.container.prepend(e)}return e}resetAnnotationElement(t){const{firstChild:e}=t.container;"DIV"===e?.nodeName&&e.classList.contains("annotationContent")&&e.remove()}}class FakeEditor extends AnnotationEditor{constructor(t){super(t);this.annotationElementId=t.annotationElementId;this.deleted=!0}serialize(){return this.serializeDeleted()}}const $=3285377520,W=4294901760,V=65535;class MurmurHash3_64{constructor(t){this.h1=t?4294967295&t:$;this.h2=t?4294967295&t:$}update(t){let e,i;if("string"==typeof t){e=new Uint8Array(2*t.length);i=0;for(let s=0,n=t.length;s>>8;e[i++]=255&n}}}else{if(!ArrayBuffer.isView(t))throw new Error("Invalid data format, must be a string or TypedArray.");e=t.slice();i=e.byteLength}const s=i>>2,n=i-4*s,a=new Uint32Array(e.buffer,0,s);let r=0,o=0,l=this.h1,h=this.h2;const d=3432918353,c=461845907,u=11601,p=13715;for(let t=0;t>>17;r=r*c&W|r*p&V;l^=r;l=l<<13|l>>>19;l=5*l+3864292196}else{o=a[t];o=o*d&W|o*u&V;o=o<<15|o>>>17;o=o*c&W|o*p&V;h^=o;h=h<<13|h>>>19;h=5*h+3864292196}r=0;switch(n){case 3:r^=e[4*s+2]<<16;case 2:r^=e[4*s+1]<<8;case 1:r^=e[4*s];r=r*d&W|r*u&V;r=r<<15|r>>>17;r=r*c&W|r*p&V;1&s?l^=r:h^=r}this.h1=l;this.h2=h}hexdigest(){let t=this.h1,e=this.h2;t^=e>>>1;t=3981806797*t&W|36045*t&V;e=4283543511*e&W|(2950163797*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;t=444984403*t&W|60499*t&V;e=3301882366*e&W|(3120437893*(e<<16|t>>>16)&W)>>>16;t^=e>>>1;return(t>>>0).toString(16).padStart(8,"0")+(e>>>0).toString(16).padStart(8,"0")}}const j=Object.freeze({map:null,hash:"",transfer:void 0});class AnnotationStorage{#ei=!1;#ii=null;#si=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#si.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#si.get(t)}remove(t){this.#si.delete(t);0===this.#si.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#si.values())if(t instanceof AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#si.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#si.set(t,e)}s&&this.#ni();e instanceof AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#si.has(t)}get size(){return this.#si.size}#ni(){if(!this.#ei){this.#ei=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#ei){this.#ei=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#si.size)return j;const t=new Map,e=new MurmurHash3_64,i=[],s=Object.create(null);let n=!1;for(const[i,a]of this.#si){const r=a instanceof AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);n||=!!r.bitmap}}if(n)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfer:i}:j}get editorStats(){let t=null;const e=new Map;for(const i of this.#si.values()){if(!(i instanceof AnnotationEditor))continue;const s=i.telemetryFinalData;if(!s)continue;const{type:n}=s;e.has(n)||e.set(n,Object.getPrototypeOf(i).constructor);t||=Object.create(null);const a=t[n]||=new Map;for(const[t,e]of Object.entries(s)){if("type"===t)continue;let i=a.get(t);if(!i){i=new Map;a.set(t,i)}const s=i.get(e)??0;i.set(e,s+1)}}for(const[i,s]of e)t[i]=s.computeTelemetryFinalData(t[i]);return t}resetModifiedIds(){this.#ii=null}get modifiedIds(){if(this.#ii)return this.#ii;const t=[];for(const e of this.#si.values())e instanceof AnnotationEditor&&e.annotationElementId&&e.serialize()&&t.push(e.annotationElementId);return this.#ii={ids:new Set(t),hash:t.join(",")}}[Symbol.iterator](){return this.#si.entries()}}class PrintAnnotationStorage extends AnnotationStorage{#ai;constructor(t){super();const{map:e,hash:i,transfer:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#ai={map:n,hash:i,transfer:s}}get print(){unreachable("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#ai}get modifiedIds(){return shadow(this,"modifiedIds",{ids:new Set,hash:""})}}class FontLoader{#ri=new Set;constructor({ownerDocument:t=globalThis.document,styleElement:e=null}){this._document=t;this.nativeFontFaces=new Set;this.styleElement=null;this.loadingRequests=[];this.loadTestFontId=0}addNativeFontFace(t){this.nativeFontFaces.add(t);this._document.fonts.add(t)}removeNativeFontFace(t){this.nativeFontFaces.delete(t);this._document.fonts.delete(t)}insertRule(t){if(!this.styleElement){this.styleElement=this._document.createElement("style");this._document.documentElement.getElementsByTagName("head")[0].append(this.styleElement)}const e=this.styleElement.sheet;e.insertRule(t,e.cssRules.length)}clear(){for(const t of this.nativeFontFaces)this._document.fonts.delete(t);this.nativeFontFaces.clear();this.#ri.clear();if(this.styleElement){this.styleElement.remove();this.styleElement=null}}async loadSystemFont({systemFontInfo:t,disableFontFace:e,_inspectFont:i}){if(t&&!this.#ri.has(t.loadedName)){assert(!e,"loadSystemFont shouldn't be called when `disableFontFace` is set.");if(this.isFontLoadingAPISupported){const{loadedName:e,src:s,style:n}=t,a=new FontFace(e,s,n);this.addNativeFontFace(a);try{await a.load();this.#ri.add(e);i?.(t)}catch{warn(`Cannot load system font: ${t.baseFontName}, installing it could help to improve PDF rendering.`);this.removeNativeFontFace(a)}}else unreachable("Not implemented: loadSystemFont without the Font Loading API.")}}async bind(t){if(t.attached||t.missingFile&&!t.systemFontInfo)return;t.attached=!0;if(t.systemFontInfo){await this.loadSystemFont(t);return}if(this.isFontLoadingAPISupported){const e=t.createNativeFontFace();if(e){this.addNativeFontFace(e);try{await e.loaded}catch(i){warn(`Failed to load font '${e.family}': '${i}'.`);t.disableFontFace=!0;throw i}}return}const e=t.createFontFaceRule();if(e){this.insertRule(e);if(this.isSyncFontLoadingSupported)return;await new Promise((e=>{const i=this._queueLoadingCallback(e);this._prepareFontLoadEvent(t,i)}))}}get isFontLoadingAPISupported(){return shadow(this,"isFontLoadingAPISupported",!!this._document?.fonts)}get isSyncFontLoadingSupported(){return shadow(this,"isSyncFontLoadingSupported",t||util_FeatureTest.platform.isFirefox)}_queueLoadingCallback(t){const{loadingRequests:e}=this,i={done:!1,complete:function completeRequest(){assert(!i.done,"completeRequest() cannot be called twice.");i.done=!0;for(;e.length>0&&e[0].done;){const t=e.shift();setTimeout(t.callback,0)}},callback:t};e.push(i);return i}get _loadTestFont(){return shadow(this,"_loadTestFont",atob("T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=="))}_prepareFontLoadEvent(t,e){function int32(t,e){return t.charCodeAt(e)<<24|t.charCodeAt(e+1)<<16|t.charCodeAt(e+2)<<8|255&t.charCodeAt(e+3)}function spliceString(t,e,i,s){return t.substring(0,e)+s+t.substring(e+i)}let i,s;const n=this._document.createElement("canvas");n.width=1;n.height=1;const a=n.getContext("2d");let r=0;const o=`lt${Date.now()}${this.loadTestFontId++}`;let l=this._loadTestFont;l=spliceString(l,976,o.length,o);const h=1482184792;let d=int32(l,16);for(i=0,s=o.length-3;i>24&255,t>>16&255,t>>8&255,255&t)}(d));const c=`@font-face {font-family:"${o}";src:${`url(data:font/opentype;base64,${btoa(l)});`}}`;this.insertRule(c);const u=this._document.createElement("div");u.style.visibility="hidden";u.style.width=u.style.height="10px";u.style.position="absolute";u.style.top=u.style.left="0px";for(const e of[t.loadedName,o]){const t=this._document.createElement("span");t.textContent="Hi";t.style.fontFamily=e;u.append(t)}this._document.body.append(u);!function isFontReady(t,e){if(++r>30){warn("Load test font never loaded.");e();return}a.font="30px "+t;a.fillText(".",0,20);a.getImageData(0,0,1,1).data[3]>0?e():setTimeout(isFontReady.bind(null,t,e))}(o,(()=>{u.remove();e.complete()}))}}class FontFaceObject{constructor(t,e=null){this.compiledGlyphs=Object.create(null);for(const e in t)this[e]=t[e];this._inspectFont=e}createNativeFontFace(){if(!this.data||this.disableFontFace)return null;let t;if(this.cssFontInfo){const e={weight:this.cssFontInfo.fontWeight};this.cssFontInfo.italicAngle&&(e.style=`oblique ${this.cssFontInfo.italicAngle}deg`);t=new FontFace(this.cssFontInfo.fontFamily,this.data,e)}else t=new FontFace(this.loadedName,this.data,{});this._inspectFont?.(this);return t}createFontFaceRule(){if(!this.data||this.disableFontFace)return null;const t=`url(data:${this.mimetype};base64,${toBase64Util(this.data)});`;let e;if(this.cssFontInfo){let i=`font-weight: ${this.cssFontInfo.fontWeight};`;this.cssFontInfo.italicAngle&&(i+=`font-style: oblique ${this.cssFontInfo.italicAngle}deg;`);e=`@font-face {font-family:"${this.cssFontInfo.fontFamily}";${i}src:${t}}`}else e=`@font-face {font-family:"${this.loadedName}";src:${t}}`;this._inspectFont?.(this,t);return e}getPathGenerator(t,e){if(void 0!==this.compiledGlyphs[e])return this.compiledGlyphs[e];const i=this.loadedName+"_path_"+e;let s;try{s=t.get(i)}catch(t){warn(`getPathGenerator - ignoring character: "${t}".`)}const n=new Path2D(s||"");this.fontExtraProperties||t.delete(i);return this.compiledGlyphs[e]=n}}function getFactoryUrlProp(t){if("string"!=typeof t)return null;if(t.endsWith("/"))return t;throw new Error(`Invalid factory url: "${t}" must include trailing slash.`)}const isRefProxy=t=>"object"==typeof t&&Number.isInteger(t?.num)&&t.num>=0&&Number.isInteger(t?.gen)&&t.gen>=0,q=function _isValidExplicitDest(t,e,i){if(!Array.isArray(i)||i.length<2)return!1;const[s,n,...a]=i;if(!t(s)&&!Number.isInteger(s))return!1;if(!e(n))return!1;const r=a.length;let o=!0;switch(n.name){case"XYZ":if(r<2||r>3)return!1;break;case"Fit":case"FitB":return 0===r;case"FitH":case"FitBH":case"FitV":case"FitBV":if(r>1)return!1;break;case"FitR":if(4!==r)return!1;o=!1;break;default:return!1}for(const t of a)if(!("number"==typeof t||o&&null===t))return!1;return!0}.bind(null,isRefProxy,(t=>"object"==typeof t&&"string"==typeof t?.name));class LoopbackPort{#oi=new Map;#li=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#li.then((()=>{for(const[t]of this.#oi)t.call(this,i)}))}addEventListener(t,e,i=null){let s=null;if(i?.signal instanceof AbortSignal){const{signal:n}=i;if(n.aborted){warn("LoopbackPort - cannot use an `aborted` signal.");return}const onAbort=()=>this.removeEventListener(t,e);s=()=>n.removeEventListener("abort",onAbort);n.addEventListener("abort",onAbort)}this.#oi.set(e,s)}removeEventListener(t,e){const i=this.#oi.get(e);i?.();this.#oi.delete(e)}terminate(){for(const[,t]of this.#oi)t?.();this.#oi.clear()}}const X=1,K=2,Y=1,Q=2,J=3,Z=4,tt=5,et=6,it=7,st=8;function onFn(){}function wrapReason(t){if(t instanceof AbortException||t instanceof InvalidPDFException||t instanceof PasswordException||t instanceof ResponseException||t instanceof UnknownErrorException)return t;t instanceof Error||"object"==typeof t&&null!==t||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new AbortException(t.message);case"InvalidPDFException":return new InvalidPDFException(t.message);case"PasswordException":return new PasswordException(t.message,t.code);case"ResponseException":return new ResponseException(t.message,t.status,t.missing);case"UnknownErrorException":return new UnknownErrorException(t.message,t.details)}return new UnknownErrorException(t.message,t.toString())}class MessageHandler{#hi=new AbortController;constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);i.addEventListener("message",this.#di.bind(this),{signal:this.#hi.signal})}#di({data:t}){if(t.targetName!==this.sourceName)return;if(t.stream){this.#ci(t);return}if(t.callback){const e=t.callbackId,i=this.callbackCapabilities[e];if(!i)throw new Error(`Cannot resolve callback ${e}`);delete this.callbackCapabilities[e];if(t.callback===X)i.resolve(t.data);else{if(t.callback!==K)throw new Error("Unexpected callback case");i.reject(wrapReason(t.reason))}return}const e=this.actionHandler[t.action];if(!e)throw new Error(`Unknown action from worker: ${t.action}`);if(t.callbackId){const i=this.sourceName,s=t.sourceName,n=this.comObj;Promise.try(e,t.data).then((function(e){n.postMessage({sourceName:i,targetName:s,callback:X,callbackId:t.callbackId,data:e})}),(function(e){n.postMessage({sourceName:i,targetName:s,callback:K,callbackId:t.callbackId,reason:wrapReason(e)})}))}else t.streamId?this.#ui(t):e(t.data)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const s=this.callbackId++,n=Promise.withResolvers();this.callbackCapabilities[s]=n;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:s,data:e},i)}catch(t){n.reject(t)}return n.promise}sendWithStream(t,e,i,s){const n=this.streamId++,a=this.sourceName,r=this.targetName,o=this.comObj;return new ReadableStream({start:i=>{const l=Promise.withResolvers();this.streamControllers[n]={controller:i,startCall:l,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:a,targetName:r,action:t,streamId:n,data:e,desiredSize:i.desiredSize},s);return l.promise},pull:t=>{const e=Promise.withResolvers();this.streamControllers[n].pullCall=e;o.postMessage({sourceName:a,targetName:r,stream:et,streamId:n,desiredSize:t.desiredSize});return e.promise},cancel:t=>{assert(t instanceof Error,"cancel must have a valid reason");const e=Promise.withResolvers();this.streamControllers[n].cancelCall=e;this.streamControllers[n].isClosed=!0;o.postMessage({sourceName:a,targetName:r,stream:Y,streamId:n,reason:wrapReason(t)});return e.promise}},i)}#ui(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this,r=this.actionHandler[t.action],o={enqueue(t,a=1,r){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=a;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}n.postMessage({sourceName:i,targetName:s,stream:Z,streamId:e,chunk:t},r)},close(){if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:J,streamId:e});delete a.streamSinks[e]}},error(t){assert(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;n.postMessage({sourceName:i,targetName:s,stream:tt,streamId:e,reason:wrapReason(t)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[e]=o;Promise.try(r,t.data,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:st,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:st,streamId:e,reason:wrapReason(t)})}))}#ci(t){const e=t.streamId,i=this.sourceName,s=t.sourceName,n=this.comObj,a=this.streamControllers[e],r=this.streamSinks[e];switch(t.stream){case st:t.success?a.startCall.resolve():a.startCall.reject(wrapReason(t.reason));break;case it:t.success?a.pullCall.resolve():a.pullCall.reject(wrapReason(t.reason));break;case et:if(!r){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,success:!0});break}r.desiredSize<=0&&t.desiredSize>0&&r.sinkCapability.resolve();r.desiredSize=t.desiredSize;Promise.try(r.onPull||onFn).then((function(){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:it,streamId:e,reason:wrapReason(t)})}));break;case Z:assert(a,"enqueue should have stream controller");if(a.isClosed)break;a.controller.enqueue(t.chunk);break;case J:assert(a,"close should have stream controller");if(a.isClosed)break;a.isClosed=!0;a.controller.close();this.#pi(a,e);break;case tt:assert(a,"error should have stream controller");a.controller.error(wrapReason(t.reason));this.#pi(a,e);break;case Q:t.success?a.cancelCall.resolve():a.cancelCall.reject(wrapReason(t.reason));this.#pi(a,e);break;case Y:if(!r)break;const o=wrapReason(t.reason);Promise.try(r.onCancel||onFn,o).then((function(){n.postMessage({sourceName:i,targetName:s,stream:Q,streamId:e,success:!0})}),(function(t){n.postMessage({sourceName:i,targetName:s,stream:Q,streamId:e,reason:wrapReason(t)})}));r.sinkCapability.reject(o);r.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#pi(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.#hi?.abort();this.#hi=null}}class BaseCanvasFactory{#gi=!1;constructor({enableHWA:t=!1}){this.#gi=t}create(t,e){if(t<=0||e<=0)throw new Error("Invalid canvas size");const i=this._createCanvas(t,e);return{canvas:i,context:i.getContext("2d",{willReadFrequently:!this.#gi})}}reset(t,e,i){if(!t.canvas)throw new Error("Canvas is not specified");if(e<=0||i<=0)throw new Error("Invalid canvas size");t.canvas.width=e;t.canvas.height=i}destroy(t){if(!t.canvas)throw new Error("Canvas is not specified");t.canvas.width=0;t.canvas.height=0;t.canvas=null;t.context=null}_createCanvas(t,e){unreachable("Abstract method `_createCanvas` called.")}}class DOMCanvasFactory extends BaseCanvasFactory{constructor({ownerDocument:t=globalThis.document,enableHWA:e=!1}){super({enableHWA:e});this._document=t}_createCanvas(t,e){const i=this._document.createElement("canvas");i.width=t;i.height=e;return i}}class BaseCMapReaderFactory{constructor({baseUrl:t=null,isCompressed:e=!0}){this.baseUrl=t;this.isCompressed=e}async fetch({name:t}){if(!this.baseUrl)throw new Error("Ensure that the `cMapUrl` and `cMapPacked` API parameters are provided.");if(!t)throw new Error("CMap name must be specified.");const e=this.baseUrl+t+(this.isCompressed?".bcmap":"");return this._fetch(e).then((t=>({cMapData:t,isCompressed:this.isCompressed}))).catch((t=>{throw new Error(`Unable to load ${this.isCompressed?"binary ":""}CMap at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){const e=await fetchData(t,this.isCompressed?"arraybuffer":"text");return e instanceof ArrayBuffer?new Uint8Array(e):stringToBytes(e)}}class BaseFilterFactory{addFilter(t){return"none"}addHCMFilter(t,e){return"none"}addAlphaFilter(t){return"none"}addLuminosityFilter(t){return"none"}addHighlightHCMFilter(t,e,i,s,n){return"none"}destroy(t=!1){}}class DOMFilterFactory extends BaseFilterFactory{#fi;#mi;#bi;#Ai;#wi;#yi;#y=0;constructor({docId:t,ownerDocument:e=globalThis.document}){super();this.#Ai=t;this.#wi=e}get#x(){return this.#mi||=new Map}get#vi(){return this.#yi||=new Map}get#xi(){if(!this.#bi){const t=this.#wi.createElement("div"),{style:e}=t;e.visibility="hidden";e.contain="strict";e.width=e.height=0;e.position="absolute";e.top=e.left=0;e.zIndex=-1;const i=this.#wi.createElementNS(z,"svg");i.setAttribute("width",0);i.setAttribute("height",0);this.#bi=this.#wi.createElementNS(z,"defs");t.append(i);i.append(this.#bi);this.#wi.body.append(t)}return this.#bi}#Ei(t){if(1===t.length){const e=t[0],i=new Array(256);for(let t=0;t<256;t++)i[t]=e[t]/255;const s=i.join(",");return[s,s,s]}const[e,i,s]=t,n=new Array(256),a=new Array(256),r=new Array(256);for(let t=0;t<256;t++){n[t]=e[t]/255;a[t]=i[t]/255;r[t]=s[t]/255}return[n.join(","),a.join(","),r.join(",")]}#_i(t){if(void 0===this.#fi){this.#fi="";const t=this.#wi.URL;t!==this.#wi.baseURI&&(isDataScheme(t)?warn('#createUrl: ignore "data:"-URL for performance reasons.'):this.#fi=updateUrlHash(t,""))}return`url(${this.#fi}#${t})`}addFilter(t){if(!t)return"none";let e=this.#x.get(t);if(e)return e;const[i,s,n]=this.#Ei(t),a=1===t.length?i:`${i}${s}${n}`;e=this.#x.get(a);if(e){this.#x.set(t,e);return e}const r=`g_${this.#Ai}_transfer_map_${this.#y++}`,o=this.#_i(r);this.#x.set(t,o);this.#x.set(a,o);const l=this.#Si(r);this.#Ci(i,s,n,l);return o}addHCMFilter(t,e){const i=`${t}-${e}`,s="base";let n=this.#vi.get(s);if(n?.key===i)return n.url;if(n){n.filter?.remove();n.key=i;n.url="none";n.filter=null}else{n={key:i,url:"none",filter:null};this.#vi.set(s,n)}if(!t||!e)return n.url;const a=this.#Ti(t);t=Util.makeHexColor(...a);const r=this.#Ti(e);e=Util.makeHexColor(...r);this.#xi.style.color="";if("#000000"===t&&"#ffffff"===e||t===e)return n.url;const o=new Array(256);for(let t=0;t<=255;t++){const e=t/255;o[t]=e<=.03928?e/12.92:((e+.055)/1.055)**2.4}const l=o.join(","),h=`g_${this.#Ai}_hcm_filter`,d=n.filter=this.#Si(h);this.#Ci(l,l,l,d);this.#Mi(d);const getSteps=(t,e)=>{const i=a[t]/255,s=r[t]/255,n=new Array(e+1);for(let t=0;t<=e;t++)n[t]=i+t/e*(s-i);return n.join(",")};this.#Ci(getSteps(0,5),getSteps(1,5),getSteps(2,5),d);n.url=this.#_i(h);return n.url}addAlphaFilter(t){let e=this.#x.get(t);if(e)return e;const[i]=this.#Ei([t]),s=`alpha_${i}`;e=this.#x.get(s);if(e){this.#x.set(t,e);return e}const n=`g_${this.#Ai}_alpha_map_${this.#y++}`,a=this.#_i(n);this.#x.set(t,a);this.#x.set(s,a);const r=this.#Si(n);this.#Di(i,r);return a}addLuminosityFilter(t){let e,i,s=this.#x.get(t||"luminosity");if(s)return s;if(t){[e]=this.#Ei([t]);i=`luminosity_${e}`}else i="luminosity";s=this.#x.get(i);if(s){this.#x.set(t,s);return s}const n=`g_${this.#Ai}_luminosity_map_${this.#y++}`,a=this.#_i(n);this.#x.set(t,a);this.#x.set(i,a);const r=this.#Si(n);this.#Pi(r);t&&this.#Di(e,r);return a}addHighlightHCMFilter(t,e,i,s,n){const a=`${e}-${i}-${s}-${n}`;let r=this.#vi.get(t);if(r?.key===a)return r.url;if(r){r.filter?.remove();r.key=a;r.url="none";r.filter=null}else{r={key:a,url:"none",filter:null};this.#vi.set(t,r)}if(!e||!i)return r.url;const[o,l]=[e,i].map(this.#Ti.bind(this));let h=Math.round(.2126*o[0]+.7152*o[1]+.0722*o[2]),d=Math.round(.2126*l[0]+.7152*l[1]+.0722*l[2]),[c,u]=[s,n].map(this.#Ti.bind(this));d{const s=new Array(256),n=(d-h)/i,a=t/255,r=(e-t)/(255*i);let o=0;for(let t=0;t<=i;t++){const e=Math.round(h+t*n),i=a+t*r;for(let t=o;t<=e;t++)s[t]=i;o=e+1}for(let t=o;t<256;t++)s[t]=s[o-1];return s.join(",")},p=`g_${this.#Ai}_hcm_${t}_filter`,g=r.filter=this.#Si(p);this.#Mi(g);this.#Ci(getSteps(c[0],u[0],5),getSteps(c[1],u[1],5),getSteps(c[2],u[2],5),g);r.url=this.#_i(p);return r.url}destroy(t=!1){if(!t||!this.#yi?.size){this.#bi?.parentNode.parentNode.remove();this.#bi=null;this.#mi?.clear();this.#mi=null;this.#yi?.clear();this.#yi=null;this.#y=0}}#Pi(t){const e=this.#wi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0.59 0.11 0 0");t.append(e)}#Mi(t){const e=this.#wi.createElementNS(z,"feColorMatrix");e.setAttribute("type","matrix");e.setAttribute("values","0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");t.append(e)}#Si(t){const e=this.#wi.createElementNS(z,"filter");e.setAttribute("color-interpolation-filters","sRGB");e.setAttribute("id",t);this.#xi.append(e);return e}#ki(t,e,i){const s=this.#wi.createElementNS(z,e);s.setAttribute("type","discrete");s.setAttribute("tableValues",i);t.append(s)}#Ci(t,e,i,s){const n=this.#wi.createElementNS(z,"feComponentTransfer");s.append(n);this.#ki(n,"feFuncR",t);this.#ki(n,"feFuncG",e);this.#ki(n,"feFuncB",i)}#Di(t,e){const i=this.#wi.createElementNS(z,"feComponentTransfer");e.append(i);this.#ki(i,"feFuncA",t)}#Ti(t){this.#xi.style.color=t;return getRGB(getComputedStyle(this.#xi).getPropertyValue("color"))}}class BaseStandardFontDataFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `standardFontDataUrl` API parameter is provided.");if(!t)throw new Error("Font filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load font data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}class BaseWasmFactory{constructor({baseUrl:t=null}){this.baseUrl=t}async fetch({filename:t}){if(!this.baseUrl)throw new Error("Ensure that the `wasmUrl` API parameter is provided.");if(!t)throw new Error("Wasm filename must be specified.");const e=`${this.baseUrl}${t}`;return this._fetch(e).catch((t=>{throw new Error(`Unable to load wasm data at: ${e}`)}))}async _fetch(t){unreachable("Abstract method `_fetch` called.")}}class DOMWasmFactory extends BaseWasmFactory{async _fetch(t){const e=await fetchData(t,"arraybuffer");return new Uint8Array(e)}}t&&warn("Please use the `legacy` build in Node.js environments.");async function node_utils_fetchData(t){const e=process.getBuiltinModule("fs"),i=await e.promises.readFile(t);return new Uint8Array(i)}class NodeFilterFactory extends BaseFilterFactory{}class NodeCanvasFactory extends BaseCanvasFactory{_createCanvas(t,e){return process.getBuiltinModule("module").createRequire(import.meta.url)("@napi-rs/canvas").createCanvas(t,e)}}class NodeCMapReaderFactory extends BaseCMapReaderFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeStandardFontDataFactory extends BaseStandardFontDataFactory{async _fetch(t){return node_utils_fetchData(t)}}class NodeWasmFactory extends BaseWasmFactory{async _fetch(t){return node_utils_fetchData(t)}}const nt="Fill",at="Stroke",rt="Shading";function applyBoundingBox(t,e){if(!e)return;const i=e[2]-e[0],s=e[3]-e[1],n=new Path2D;n.rect(e[0],e[1],i,s);t.clip(n)}class BaseShadingPattern{isModifyingCurrentTransform(){return!1}getPattern(){unreachable("Abstract method `getPattern` called.")}}class RadialAxialShadingPattern extends BaseShadingPattern{constructor(t){super();this._type=t[1];this._bbox=t[2];this._colorStops=t[3];this._p0=t[4];this._p1=t[5];this._r0=t[6];this._r1=t[7];this.matrix=null}_createGradient(t){let e;"axial"===this._type?e=t.createLinearGradient(this._p0[0],this._p0[1],this._p1[0],this._p1[1]):"radial"===this._type&&(e=t.createRadialGradient(this._p0[0],this._p0[1],this._r0,this._p1[0],this._p1[1],this._r1));for(const t of this._colorStops)e.addColorStop(t[0],t[1]);return e}getPattern(t,e,i,s){let n;if(s===at||s===nt){const a=e.current.getClippedPathBoundingBox(s,getCurrentTransform(t))||[0,0,0,0],r=Math.ceil(a[2]-a[0])||1,o=Math.ceil(a[3]-a[1])||1,l=e.cachedCanvases.getCanvas("pattern",r,o),h=l.context;h.clearRect(0,0,h.canvas.width,h.canvas.height);h.beginPath();h.rect(0,0,h.canvas.width,h.canvas.height);h.translate(-a[0],-a[1]);i=Util.transform(i,[1,0,0,1,a[0],a[1]]);h.transform(...e.baseTransform);this.matrix&&h.transform(...this.matrix);applyBoundingBox(h,this._bbox);h.fillStyle=this._createGradient(h);h.fill();n=t.createPattern(l.canvas,"no-repeat");const d=new DOMMatrix(i);n.setTransform(d)}else{applyBoundingBox(t,this._bbox);n=this._createGradient(t)}return n}}function drawTriangle(t,e,i,s,n,a,r,o){const l=e.coords,h=e.colors,d=t.data,c=4*t.width;let u;if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}if(l[s+1]>l[n+1]){u=s;s=n;n=u;u=r;r=o;o=u}if(l[i+1]>l[s+1]){u=i;i=s;s=u;u=a;a=r;r=u}const p=(l[i]+e.offsetX)*e.scaleX,g=(l[i+1]+e.offsetY)*e.scaleY,f=(l[s]+e.offsetX)*e.scaleX,m=(l[s+1]+e.offsetY)*e.scaleY,b=(l[n]+e.offsetX)*e.scaleX,A=(l[n+1]+e.offsetY)*e.scaleY;if(g>=A)return;const w=h[a],y=h[a+1],v=h[a+2],x=h[r],E=h[r+1],_=h[r+2],S=h[o],C=h[o+1],T=h[o+2],M=Math.round(g),D=Math.round(A);let P,k,I,R,F,L,O,N;for(let t=M;t<=D;t++){if(tA?1:m===A?0:(m-t)/(m-A);P=f-(f-b)*e;k=x-(x-S)*e;I=E-(E-C)*e;R=_-(_-T)*e}let e;e=tA?1:(g-t)/(g-A);F=p-(p-b)*e;L=w-(w-S)*e;O=y-(y-C)*e;N=v-(v-T)*e;const i=Math.round(Math.min(P,F)),s=Math.round(Math.max(P,F));let n=c*t+4*i;for(let t=i;t<=s;t++){e=(P-t)/(P-F);e<0?e=0:e>1&&(e=1);d[n++]=k-(k-L)*e|0;d[n++]=I-(I-O)*e|0;d[n++]=R-(R-N)*e|0;d[n++]=255}}}function drawFigure(t,e,i){const s=e.coords,n=e.colors;let a,r;switch(e.type){case"lattice":const o=e.verticesPerRow,l=Math.floor(s.length/o)-1,h=o-1;for(a=0;a=Math.ceil(p*A)?y=o:x=!0;S>=Math.ceil(g*w)?v=l:E=!0;const C=this.getSizeAndScale(y,this.ctx.canvas.width,A),T=this.getSizeAndScale(v,this.ctx.canvas.height,w),M=t.cachedCanvases.getCanvas("pattern",C.size,T.size),D=M.context,P=r.createCanvasGraphics(D);P.groupLevel=t.groupLevel;this.setFillAndStrokeStyleToContext(P,s,a);D.translate(-C.scale*h,-T.scale*d);P.transform(C.scale,0,0,T.scale,0,0);D.save();this.clipBbox(P,h,d,c,u);P.baseTransform=getCurrentTransform(P.ctx);P.executeOperatorList(i);P.endDrawing();D.restore();if(x||E){const e=M.canvas;x&&(y=o);E&&(v=l);const i=this.getSizeAndScale(y,this.ctx.canvas.width,A),s=this.getSizeAndScale(v,this.ctx.canvas.height,w),n=i.size,a=s.size,r=t.cachedCanvases.getCanvas("pattern-workaround",n,a),c=r.context,u=x?Math.floor(p/o):0,f=E?Math.floor(g/l):0;for(let t=0;t<=u;t++)for(let i=0;i<=f;i++)c.drawImage(e,n*t,a*i,n,a,0,0,n,a);return{canvas:r.canvas,scaleX:i.scale,scaleY:s.scale,offsetX:h,offsetY:d}}return{canvas:M.canvas,scaleX:C.scale,scaleY:T.scale,offsetX:h,offsetY:d}}getSizeAndScale(t,e,i){const s=Math.max(TilingPattern.MAX_PATTERN_SIZE,e);let n=Math.ceil(t*i);n>=s?n=s:i=n/t;return{scale:i,size:n}}clipBbox(t,e,i,s,n){const a=s-e,r=n-i;t.ctx.rect(e,i,a,r);Util.axialAlignedBoundingBox([e,i,s,n],getCurrentTransform(t.ctx),t.current.minMax);t.clip();t.endPath()}setFillAndStrokeStyleToContext(t,e,i){const s=t.ctx,n=t.current;switch(e){case ot:const{fillStyle:t,strokeStyle:a}=this.ctx;s.fillStyle=n.fillColor=t;s.strokeStyle=n.strokeColor=a;break;case lt:s.fillStyle=s.strokeStyle=i;n.fillColor=n.strokeColor=i;break;default:throw new FormatError(`Unsupported paint type: ${e}`)}}isModifyingCurrentTransform(){return!1}getPattern(t,e,i,s){let n=i;if(s!==rt){n=Util.transform(n,e.baseTransform);this.matrix&&(n=Util.transform(n,this.matrix))}const a=this.createPatternCanvas(e);let r=new DOMMatrix(n);r=r.translate(a.offsetX,a.offsetY);r=r.scale(1/a.scaleX,1/a.scaleY);const o=t.createPattern(a.canvas,"repeat");o.setTransform(r);return o}}function convertBlackAndWhiteToRGBA({src:t,srcPos:e=0,dest:i,width:s,height:n,nonBlackColor:a=4294967295,inverseDecode:r=!1}){const o=util_FeatureTest.isLittleEndian?4278190080:255,[l,h]=r?[a,o]:[o,a],d=s>>3,c=7&s,u=t.length;i=new Uint32Array(i.buffer);let p=0;for(let s=0;s>2),f=i.length,m=s+7>>3,b=4294967295,A=util_FeatureTest.isLittleEndian?4278190080:255;for(u=0;um?s:8*t-7,r=-8&a;let o=0,c=0;for(;n>=1}}for(;l=a){g=n;f=s*g}l=0;for(p=f;p--;){c[l++]=d[h++];c[l++]=d[h++];c[l++]=d[h++];c[l++]=255}t.putImageData(o,0,u*ht)}}}function putBinaryImageMask(t,e){if(e.bitmap){t.drawImage(e.bitmap,0,0);return}const i=e.height,s=e.width,n=i%ht,a=(i-n)/ht,r=0===n?a:a+1,o=t.createImageData(s,ht);let l=0;const h=e.data,d=o.data;for(let e=0;e10&&"function"==typeof i,h=l?Date.now()+15:0;let d=0;const c=this.commonObjs,u=this.objs;let p;for(;;){if(void 0!==s&&r===s.nextBreakPoint){s.breakIt(r,i);return r}p=a[r];if(p!==D.dependency)this[p].apply(this,n[r]);else for(const t of n[r]){const e=t.startsWith("g_")?c:u;if(!e.has(t)){e.get(t,i);return r}}r++;if(r===o)return r;if(l&&++d>10){if(Date.now()>h){i();return r}d=0}}}#Ii(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.current.activeSMask=null;this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#Ii();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#Ri()}#Ri(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width??t.displayWidth,s=t.height??t.displayHeight;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,d="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(d,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;d="prescale1"===d?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:s}=t,n=this.current.fillColor,a=this.current.patternFill,r=getCurrentTransform(e);let o,l,h,d;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;l=JSON.stringify(a?r:[r.slice(0,4),n]);o=this._cachedBitmapsMap.get(e);if(!o){o=new Map;this._cachedBitmapsMap.set(e,o)}const i=o.get(l);if(i&&!a){return{canvas:i,offsetX:Math.round(Math.min(r[0],r[2])+r[4]),offsetY:Math.round(Math.min(r[1],r[3])+r[5])}}h=i}if(!h){d=this.cachedCanvases.getCanvas("maskCanvas",i,s);putBinaryImageMask(d.context,t)}let c=Util.transform(r,[1/i,0,0,-1/s,0,0]);c=Util.transform(c,[1,0,0,1,0,-s]);const u=ut.slice();Util.axialAlignedBoundingBox([0,0,i,s],c,u);const[p,g,f,m]=u,b=Math.round(f-p)||1,A=Math.round(m-g)||1,w=this.cachedCanvases.getCanvas("fillCanvas",b,A),y=w.context,v=p,x=g;y.translate(-v,-x);y.transform(...c);if(!h){h=this._scaleImage(d.canvas,getCurrentTransformInverse(y));h=h.img;o&&a&&o.set(l,h)}y.imageSmoothingEnabled=getImageSmoothingEnabled(getCurrentTransform(y),t.interpolate);drawImageAtIntegerCoords(y,h,0,0,h.width,h.height,0,0,i,s);y.globalCompositeOperation="source-in";const E=Util.transform(getCurrentTransformInverse(y),[1,0,0,1,-v,-x]);y.fillStyle=a?n.getPattern(e,this,E,nt):n;y.fillRect(0,0,i,s);if(o&&!a){this.cachedCanvases.delete("fillCanvas");o.set(l,w.canvas)}return{canvas:w.canvas,offsetX:Math.round(v),offsetY:Math.round(x)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=pt[t]}setLineJoin(t){this.ctx.lineJoin=gt[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.ctx.globalAlpha=this.current.fillAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;const n=this.ctx=s.context;n.setTransform(this.suspendedCtx.getTransform());copyCtxState(this.suspendedCtx,n);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function(){e.save();this.__originalSave()};t.restore=function(){e.restore();this.__originalRestore()};t.translate=function(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function(){e.resetTransform();this.__originalResetTransform()};t.rotate=function(t){e.rotate(t);this.__originalRotate(t)};t.clip=function(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(n,this.suspendedCtx);this.setGState([["BM","source-over"]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask,i=this.suspendedCtx;this.composeSMask(i,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){this.genericComposeSMask(e.context,i,r,o,e.subtype,e.backdrop,e.transferMap,n,a,e.offsetX,e.offsetY);t.save();t.globalAlpha=1;t.globalCompositeOperation="source-over";t.setTransform(1,0,0,1,0,0);t.drawImage(i.canvas,0,0);t.restore()}}genericComposeSMask(t,e,i,s,n,a,r,o,l,h,d){let c=t.canvas,u=o-h,p=l-d;if(a)if(u<0||p<0||u+i>c.width||p+s>c.height){const t=this.cachedCanvases.getCanvas("maskExtension",i,s),e=t.context;e.drawImage(c,-u,-p);e.globalCompositeOperation="destination-atop";e.fillStyle=a;e.fillRect(0,0,i,s);e.globalCompositeOperation="source-over";c=t.canvas;u=p=0}else{t.save();t.globalAlpha=1;t.setTransform(1,0,0,1,0,0);const e=new Path2D;e.rect(u,p,i,s);t.clip(e);t.globalCompositeOperation="destination-atop";t.fillStyle=a;t.fillRect(u,p,i,s);t.restore()}e.save();e.globalAlpha=1;e.setTransform(1,0,0,1,0,0);"Alpha"===n&&r?e.filter=this.filterFactory.addAlphaFilter(r):"Luminosity"===n&&(e.filter=this.filterFactory.addLuminosityFilter(r));const g=new Path2D;g.rect(o,l,i,s);e.clip(g);e.globalCompositeOperation="destination-in";e.drawImage(c,u,p,i,s,o,l,i,s);e.restore()}save(){this.inSMaskMode&©CtxState(this.ctx,this.suspendedCtx);this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){if(0!==this.stateStack.length){this.current=this.stateStack.pop();this.ctx.restore();this.inSMaskMode&©CtxState(this.suspendedCtx,this.ctx);this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}else this.inSMaskMode&&this.endSMaskMode()}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){let[s]=e;if(i){if(!(s instanceof Path2D)){const t=e[0]=new Path2D;for(let e=0,i=s.length;e100&&(h=100);this.current.fontSizeScale=i/h;this.ctx.font=`${l} ${o} ${h}px ${r}`}setTextRenderingMode(t){this.current.textRenderingMode=t}setTextRise(t){this.current.textRise=t}moveText(t,e){this.current.x=this.current.lineX+=t;this.current.y=this.current.lineY+=e}setLeadingMoveText(t,e){this.setLeading(-e);this.moveText(t,e)}setTextMatrix(t){const{current:e}=this;e.textMatrix=t;e.textMatrixScale=Math.hypot(t[0],t[1]);e.x=e.lineX=0;e.y=e.lineY=0}nextLine(){this.moveText(0,this.current.leading)}#Fi(t,e,i){const s=new Path2D;s.addPath(t,new DOMMatrix(i).invertSelf().multiplySelf(e));return s}paintChar(t,e,i,s,n){const a=this.ctx,r=this.current,o=r.font,l=r.textRenderingMode,h=r.fontSize/r.fontSizeScale,d=l&w,c=!!(l&y),u=r.patternFill&&!o.missingFile,p=r.patternStroke&&!o.missingFile;let g;(o.disableFontFace||c||u||p)&&(g=o.getPathGenerator(this.commonObjs,t));if(o.disableFontFace||u||p){a.save();a.translate(e,i);a.scale(h,-h);let t;if(d===f||d===b)if(s){t=a.getTransform();a.setTransform(...s);a.fill(this.#Fi(g,t,s))}else a.fill(g);if(d===m||d===b)if(n){t||=a.getTransform();a.setTransform(...n);const{a:e,b:i,c:s,d:r}=t,o=Util.inverseTransform(n),l=Util.transform([e,i,s,r,0,0],o);Util.singularValueDecompose2dScale(l,ct);a.lineWidth*=Math.max(ct[0],ct[1])/h;a.stroke(this.#Fi(g,t,n))}else{a.lineWidth/=h;a.stroke(g)}a.restore()}else{d!==f&&d!==b||a.fillText(t,e,i);d!==m&&d!==b||a.strokeText(t,e,i)}if(c){(this.pendingTextPaths||=[]).push({transform:getCurrentTransform(a),x:e,y:i,fontSize:h,path:g})}}get isFontSubpixelAAEnabled(){const{context:t}=this.cachedCanvases.getCanvas("isFontSubpixelAAEnabled",10,10);t.scale(1.5,1);t.fillText("I",0,10);const e=t.getImageData(0,0,10,10).data;let i=!1;for(let t=3;t0&&e[t]<255){i=!0;break}return shadow(this,"isFontSubpixelAAEnabled",i)}showText(t){const e=this.current,i=e.font;if(i.isType3Font)return this.showType3Text(t);const s=e.fontSize;if(0===s)return;const n=this.ctx,a=e.fontSizeScale,r=e.charSpacing,o=e.wordSpacing,l=e.fontDirection,h=e.textHScale*l,d=t.length,c=i.vertical,u=c?1:-1,p=i.defaultVMetrics,g=s*e.fontMatrix[0],A=e.textRenderingMode===f&&!i.disableFontFace&&!e.patternFill;n.save();e.textMatrix&&n.transform(...e.textMatrix);n.translate(e.x,e.y+e.textRise);l>0?n.scale(h,-1):n.scale(h,1);let y,v;if(e.patternFill){n.save();const t=e.fillColor.getPattern(n,this,getCurrentTransformInverse(n),nt);y=getCurrentTransform(n);n.restore();n.fillStyle=t}if(e.patternStroke){n.save();const t=e.strokeColor.getPattern(n,this,getCurrentTransformInverse(n),at);v=getCurrentTransform(n);n.restore();n.strokeStyle=t}let x=e.lineWidth;const E=e.textMatrixScale;if(0===E||0===x){const t=e.textRenderingMode&w;t!==m&&t!==b||(x=this.getSinglePixelWidth())}else x/=E;if(1!==a){n.scale(a,a);x/=a}n.lineWidth=x;if(i.isInvalidPDFjsFont){const i=[];let s=0;for(const e of t){i.push(e.unicode);s+=e.width}n.fillText(i.join(""),0,0);e.x+=s*g*h;n.restore();this.compose();return}let _,S=0;for(_=0;_0){const t=1e3*n.measureText(f).width/s*a;if(xnew CanvasGraphics(t,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:this.optionalContentConfig,markedContentStack:this.markedContentStack})};e=new TilingPattern(t,this.ctx,s,i)}else e=this._getPattern(t[1],t[2]);return e}setStrokeColorN(){this.current.strokeColor=this.getColorN_Pattern(arguments);this.current.patternStroke=!0}setFillColorN(){this.current.fillColor=this.getColorN_Pattern(arguments);this.current.patternFill=!0}setStrokeRGBColor(t){this.ctx.strokeStyle=this.current.strokeColor=t;this.current.patternStroke=!1}setStrokeTransparent(){this.ctx.strokeStyle=this.current.strokeColor="transparent";this.current.patternStroke=!1}setFillRGBColor(t){this.ctx.fillStyle=this.current.fillColor=t;this.current.patternFill=!1}setFillTransparent(){this.ctx.fillStyle=this.current.fillColor="transparent";this.current.patternFill=!1}_getPattern(t,e=null){let i;if(this.cachedPatterns.has(t))i=this.cachedPatterns.get(t);else{i=function getShadingPattern(t){switch(t[0]){case"RadialAxial":return new RadialAxialShadingPattern(t);case"Mesh":return new MeshShadingPattern(t);case"Dummy":return new DummyShadingPattern}throw new Error(`Unknown IR type: ${t[0]}`)}(this.getObject(t));this.cachedPatterns.set(t,i)}e&&(i.matrix=e);return i}shadingFill(t){if(!this.contentVisible)return;const e=this.ctx;this.save();const i=this._getPattern(t);e.fillStyle=i.getPattern(e,this,getCurrentTransformInverse(e),rt);const s=getCurrentTransformInverse(e);if(s){const{width:t,height:i}=e.canvas,n=ut.slice();Util.axialAlignedBoundingBox([0,0,t,i],s,n);const[a,r,o,l]=n;this.ctx.fillRect(a,r,o-a,l-r)}else this.ctx.fillRect(-1e10,-1e10,2e10,2e10);this.compose(this.current.getClippedPathBoundingBox());this.restore()}beginInlineImage(){unreachable("Should not call beginInlineImage")}beginImageData(){unreachable("Should not call beginImageData")}paintFormXObjectBegin(t,e){if(this.contentVisible){this.save();this.baseTransformStack.push(this.baseTransform);t&&this.transform(...t);this.baseTransform=getCurrentTransform(this.ctx);if(e){Util.axialAlignedBoundingBox(e,this.baseTransform,this.current.minMax);const[t,i,s,n]=e,a=new Path2D;a.rect(t,i,s-t,n-i);this.ctx.clip(a);this.endPath()}}}paintFormXObjectEnd(){if(this.contentVisible){this.restore();this.baseTransform=this.baseTransformStack.pop()}}beginGroup(t){if(!this.contentVisible)return;this.save();if(this.inSMaskMode){this.endSMaskMode();this.current.activeSMask=null}const e=this.ctx;t.isolated||info("TODO: Support non-isolated groups.");t.knockout&&warn("Knockout groups not supported.");const i=getCurrentTransform(e);t.matrix&&e.transform(...t.matrix);if(!t.bbox)throw new Error("Bounding box is required.");let s=ut.slice();Util.axialAlignedBoundingBox(t.bbox,getCurrentTransform(e),s);const n=[0,0,e.canvas.width,e.canvas.height];s=Util.intersect(s,n)||[0,0,0,0];const a=Math.floor(s[0]),r=Math.floor(s[1]),o=Math.max(Math.ceil(s[2])-a,1),l=Math.max(Math.ceil(s[3])-r,1);this.current.startNewPathAndClipBox([0,0,o,l]);let h="groupAt"+this.groupLevel;t.smask&&(h+="_smask_"+this.smaskCounter++%2);const d=this.cachedCanvases.getCanvas(h,o,l),c=d.context;c.translate(-a,-r);c.transform(...i);let u=new Path2D;const[p,g,f,m]=t.bbox;u.rect(p,g,f-p,m-g);if(t.matrix){const e=new Path2D;e.addPath(u,new DOMMatrix(t.matrix));u=e}c.clip(u);if(t.smask)this.smaskStack.push({canvas:d.canvas,context:c,offsetX:a,offsetY:r,subtype:t.smask.subtype,backdrop:t.smask.backdrop,transferMap:t.smask.transferMap||null,startTransformInverse:null});else{e.setTransform(1,0,0,1,0,0);e.translate(a,r);e.save()}copyCtxState(e,c);this.ctx=c;this.setGState([["BM","source-over"],["ca",1],["CA",1]]);this.groupStack.push(e);this.groupLevel++}endGroup(t){if(!this.contentVisible)return;this.groupLevel--;const e=this.ctx,i=this.groupStack.pop();this.ctx=i;this.ctx.imageSmoothingEnabled=!1;if(t.smask){this.tempSMask=this.smaskStack.pop();this.restore()}else{this.ctx.restore();const t=getCurrentTransform(this.ctx);this.restore();this.ctx.save();this.ctx.setTransform(...t);const i=ut.slice();Util.axialAlignedBoundingBox([0,0,e.canvas.width,e.canvas.height],t,i);this.ctx.drawImage(e.canvas,0,0);this.ctx.restore();this.compose(i)}}beginAnnotation(t,e,i,s,n){this.#Ii();resetCtxToDefault(this.ctx);this.ctx.save();this.save();this.baseTransform&&this.ctx.setTransform(...this.baseTransform);if(e){const s=e[2]-e[0],a=e[3]-e[1];if(n&&this.annotationCanvasMap){(i=i.slice())[4]-=e[0];i[5]-=e[1];(e=e.slice())[0]=e[1]=0;e[2]=s;e[3]=a;Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx),ct);const{viewportScale:n}=this,r=Math.ceil(s*this.outputScaleX*n),o=Math.ceil(a*this.outputScaleY*n);this.annotationCanvas=this.canvasFactory.create(r,o);const{canvas:l,context:h}=this.annotationCanvas;this.annotationCanvasMap.set(t,l);this.annotationCanvas.savedCtx=this.ctx;this.ctx=h;this.ctx.save();this.ctx.setTransform(ct[0],0,0,-ct[1],0,a*ct[1]);resetCtxToDefault(this.ctx)}else{resetCtxToDefault(this.ctx);this.endPath();const t=new Path2D;t.rect(e[0],e[1],s,a);this.ctx.clip(t)}}this.current=new CanvasExtraState(this.ctx.canvas.width,this.ctx.canvas.height);this.transform(...i);this.transform(...s)}endAnnotation(){if(this.annotationCanvas){this.ctx.restore();this.#Ri();this.ctx=this.annotationCanvas.savedCtx;delete this.annotationCanvas.savedCtx;delete this.annotationCanvas}}paintImageMaskXObject(t){if(!this.contentVisible)return;const e=t.count;(t=this.getObject(t.data,t)).count=e;const i=this.ctx,s=this._createMaskCanvas(t),n=s.canvas;i.save();i.setTransform(1,0,0,1,0,0);i.drawImage(n,s.offsetX,s.offsetY);i.restore();this.compose()}paintImageMaskXObjectRepeat(t,e,i=0,s=0,n,a){if(!this.contentVisible)return;t=this.getObject(t.data,t);const r=this.ctx;r.save();const o=getCurrentTransform(r);r.transform(e,i,s,n,0,0);const l=this._createMaskCanvas(t);r.setTransform(1,0,0,1,l.offsetX-o[4],l.offsetY-o[5]);for(let t=0,h=a.length;te?h/e:1;r=l>e?l/e:1}}this._cachedScaleForStroking[0]=a;this._cachedScaleForStroking[1]=r}return this._cachedScaleForStroking}rescaleAndStroke(t,e){const{ctx:i,current:{lineWidth:s}}=this,[n,a]=this.getScaleForStroking();if(n===a){i.lineWidth=(s||1)*n;i.stroke(t);return}const r=i.getLineDash();e&&i.save();i.scale(n,a);dt.a=1/n;dt.d=1/a;const o=new Path2D;o.addPath(t,dt);if(r.length>0){const t=Math.max(n,a);i.setLineDash(r.map((e=>e/t)));i.lineDashOffset/=t}i.lineWidth=s||1;i.stroke(o);e&&i.restore()}isContentVisible(){for(let t=this.markedContentStack.length-1;t>=0;t--)if(!this.markedContentStack[t].visible)return!1;return!0}}for(const t in D)void 0!==CanvasGraphics.prototype[t]&&(CanvasGraphics.prototype[D[t]]=CanvasGraphics.prototype[t]);class GlobalWorkerOptions{static#Li=null;static#Oi="";static get workerPort(){return this.#Li}static set workerPort(t){if(!("undefined"!=typeof Worker&&t instanceof Worker)&&null!==t)throw new Error("Invalid `workerPort` type.");this.#Li=t}static get workerSrc(){return this.#Oi}static set workerSrc(t){if("string"!=typeof t)throw new Error("Invalid `workerSrc` type.");this.#Oi=t}}class Metadata{#Ni;#Bi;constructor({parsedData:t,rawData:e}){this.#Ni=t;this.#Bi=e}getRaw(){return this.#Bi}get(t){return this.#Ni.get(t)??null}[Symbol.iterator](){return this.#Ni.entries()}}const bt=Symbol("INTERNAL");class OptionalContentGroup{#Hi=!1;#Ui=!1;#zi=!1;#Gi=!0;constructor(t,{name:e,intent:i,usage:s,rbGroups:r}){this.#Hi=!!(t&n);this.#Ui=!!(t&a);this.name=e;this.intent=i;this.usage=s;this.rbGroups=r}get visible(){if(this.#zi)return this.#Gi;if(!this.#Gi)return!1;const{print:t,view:e}=this.usage;return this.#Hi?"OFF"!==e?.viewState:!this.#Ui||"OFF"!==t?.printState}_setVisible(t,e,i=!1){t!==bt&&unreachable("Internal method `_setVisible` called.");this.#zi=i;this.#Gi=e}}class OptionalContentConfig{#$i=null;#Wi=new Map;#Vi=null;#ji=null;constructor(t,e=n){this.renderingIntent=e;this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#ji=t.order;for(const i of t.groups)this.#Wi.set(i.id,new OptionalContentGroup(e,i));if("OFF"===t.baseState)for(const t of this.#Wi.values())t._setVisible(bt,!1);for(const e of t.on)this.#Wi.get(e)._setVisible(bt,!0);for(const e of t.off)this.#Wi.get(e)._setVisible(bt,!1);this.#Vi=this.getHash()}}#qi(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let s=1;s0){const t=n instanceof Uint8Array&&n.byteLength===n.buffer.byteLength?n.buffer:new Uint8Array(n).buffer;this._queuedChunks.push(t)}this._pdfDataRangeTransport=t;this._isStreamingSupported=!i;this._isRangeSupported=!e;this._contentLength=s;this._fullRequestReader=null;this._rangeReaders=[];t.addRangeListener(((t,e)=>{this._onReceiveData({begin:t,chunk:e})}));t.addProgressListener(((t,e)=>{this._onProgress({loaded:t,total:e})}));t.addProgressiveReadListener((t=>{this._onReceiveData({chunk:t})}));t.addProgressiveDoneListener((()=>{this._onProgressiveDone()}));t.transportReady()}_onReceiveData({begin:t,chunk:e}){const i=e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength?e.buffer:new Uint8Array(e).buffer;if(void 0===t)this._fullRequestReader?this._fullRequestReader._enqueue(i):this._queuedChunks.push(i);else{assert(this._rangeReaders.some((function(e){if(e._begin!==t)return!1;e._enqueue(i);return!0})),"_onReceiveData - no `PDFDataTransportStreamRangeReader` instance found.")}}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}_onProgress(t){void 0===t.total?this._rangeReaders[0]?.onProgress?.({loaded:t.loaded}):this._fullRequestReader?.onProgress?.({loaded:t.loaded,total:t.total})}_onProgressiveDone(){this._fullRequestReader?.progressiveDone();this._progressiveDone=!0}_removeRangeReader(t){const e=this._rangeReaders.indexOf(t);e>=0&&this._rangeReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFDataTransportStream.getFullReader can only be called once.");const t=this._queuedChunks;this._queuedChunks=null;return new PDFDataTransportStreamReader(this,t,this._progressiveDone,this._contentDispositionFilename)}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFDataTransportStreamRangeReader(this,t,e);this._pdfDataRangeTransport.requestDataRange(t,e);this._rangeReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeReaders.slice(0))e.cancel(t);this._pdfDataRangeTransport.abort()}}class PDFDataTransportStreamReader{constructor(t,e,i=!1,s=null){this._stream=t;this._done=i||!1;this._filename=isPdfFile(s)?s:null;this._queuedChunks=e||[];this._loaded=0;for(const t of this._queuedChunks)this._loaded+=t.byteLength;this._requests=[];this._headersReady=Promise.resolve();t._fullRequestReader=this;this.onProgress=null}_enqueue(t){if(!this._done){if(this._requests.length>0){this._requests.shift().resolve({value:t,done:!1})}else this._queuedChunks.push(t);this._loaded+=t.byteLength}}get headersReady(){return this._headersReady}get filename(){return this._filename}get isRangeSupported(){return this._stream._isRangeSupported}get isStreamingSupported(){return this._stream._isStreamingSupported}get contentLength(){return this._stream._contentLength}async read(){if(this._queuedChunks.length>0){return{value:this._queuedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}progressiveDone(){this._done||(this._done=!0)}}class PDFDataTransportStreamRangeReader{constructor(t,e,i){this._stream=t;this._begin=e;this._end=i;this._queuedChunk=null;this._requests=[];this._done=!1;this.onProgress=null}_enqueue(t){if(!this._done){if(0===this._requests.length)this._queuedChunk=t;else{this._requests.shift().resolve({value:t,done:!1});for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}this._done=!0;this._stream._removeRangeReader(this)}}get isStreamingSupported(){return!1}async read(){if(this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._stream._removeRangeReader(this)}}function createHeaders(t,e){const i=new Headers;if(!t||!e||"object"!=typeof e)return i;for(const t in e){const s=e[t];void 0!==s&&i.append(t,s)}return i}function getResponseOrigin(t){return URL.parse(t)?.origin??null}function validateRangeRequestCapabilities({responseHeaders:t,isHttp:e,rangeChunkSize:i,disableRange:s}){const n={allowRangeRequests:!1,suggestedLength:void 0},a=parseInt(t.get("Content-Length"),10);if(!Number.isInteger(a))return n;n.suggestedLength=a;if(a<=2*i)return n;if(s||!e)return n;if("bytes"!==t.get("Accept-Ranges"))return n;if("identity"!==(t.get("Content-Encoding")||"identity"))return n;n.allowRangeRequests=!0;return n}function extractFilenameFromHeader(t){const e=t.get("Content-Disposition");if(e){let t=function getFilenameFromContentDispositionHeader(t){let e=!0,i=toParamRegExp("filename\\*","i").exec(t);if(i){i=i[1];let t=rfc2616unquote(i);t=unescape(t);t=rfc5987decode(t);t=rfc2047decode(t);return fixupEncoding(t)}i=function rfc2231getparam(t){const e=[];let i;const s=toParamRegExp("filename\\*((?!0\\d)\\d+)(\\*?)","ig");for(;null!==(i=s.exec(t));){let[,t,s,n]=i;t=parseInt(t,10);if(t in e){if(0===t)break}else e[t]=[s,n]}const n=[];for(let t=0;t{t._responseOrigin=getResponseOrigin(e.url);if(!validateResponseStatus(e.status))throw createResponseError(e.status,s);this._reader=e.body.getReader();this._headersCapability.resolve();const i=e.headers,{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:i,isHttp:t.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});this._isRangeSupported=n;this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(i);!this._isStreamingSupported&&this._isRangeSupported&&this.cancel(new AbortException("Streaming is disabled."))})).catch(this._headersCapability.reject);this.onProgress=null}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._headersCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class PDFFetchStreamRangeReader{constructor(t,e,i){this._stream=t;this._reader=null;this._loaded=0;const s=t.source;this._withCredentials=s.withCredentials||!1;this._readCapability=Promise.withResolvers();this._isStreamingSupported=!s.disableStream;this._abortController=new AbortController;const n=new Headers(t.headers);n.append("Range",`bytes=${e}-${i-1}`);const a=s.url;fetch(a,createFetchOptions(n,this._withCredentials,this._abortController)).then((e=>{const i=getResponseOrigin(e.url);if(i!==t._responseOrigin)throw new Error(`Expected range response-origin "${i}" to match "${t._responseOrigin}".`);if(!validateResponseStatus(e.status))throw createResponseError(e.status,a);this._readCapability.resolve();this._reader=e.body.getReader()})).catch(this._readCapability.reject);this.onProgress=null}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;const{value:t,done:e}=await this._reader.read();if(e)return{value:t,done:e};this._loaded+=t.byteLength;this.onProgress?.({loaded:this._loaded});return{value:getArrayBuffer(t),done:!1}}cancel(t){this._reader?.cancel(t);this._abortController.abort()}}class NetworkManager{_responseOrigin=null;constructor({url:t,httpHeaders:e,withCredentials:i}){this.url=t;this.isHttp=/^https?:/i.test(t);this.headers=createHeaders(this.isHttp,e);this.withCredentials=i||!1;this.currXhrId=0;this.pendingRequests=Object.create(null)}request(t){const e=new XMLHttpRequest,i=this.currXhrId++,s=this.pendingRequests[i]={xhr:e};e.open("GET",this.url);e.withCredentials=this.withCredentials;for(const[t,i]of this.headers)e.setRequestHeader(t,i);if(this.isHttp&&"begin"in t&&"end"in t){e.setRequestHeader("Range",`bytes=${t.begin}-${t.end-1}`);s.expectedStatus=206}else s.expectedStatus=200;e.responseType="arraybuffer";assert(t.onError,"Expected `onError` callback to be provided.");e.onerror=()=>{t.onError(e.status)};e.onreadystatechange=this.onStateChange.bind(this,i);e.onprogress=this.onProgress.bind(this,i);s.onHeadersReceived=t.onHeadersReceived;s.onDone=t.onDone;s.onError=t.onError;s.onProgress=t.onProgress;e.send(null);return i}onProgress(t,e){const i=this.pendingRequests[t];i&&i.onProgress?.(e)}onStateChange(t,e){const i=this.pendingRequests[t];if(!i)return;const s=i.xhr;if(s.readyState>=2&&i.onHeadersReceived){i.onHeadersReceived();delete i.onHeadersReceived}if(4!==s.readyState)return;if(!(t in this.pendingRequests))return;delete this.pendingRequests[t];if(0===s.status&&this.isHttp){i.onError(s.status);return}const n=s.status||200;if(!(200===n&&206===i.expectedStatus)&&n!==i.expectedStatus){i.onError(s.status);return}const a=function network_getArrayBuffer(t){const e=t.response;return"string"!=typeof e?e:stringToBytes(e).buffer}(s);if(206===n){const t=s.getResponseHeader("Content-Range"),e=/bytes (\d+)-(\d+)\/(\d+)/.exec(t);if(e)i.onDone({begin:parseInt(e[1],10),chunk:a});else{warn('Missing or invalid "Content-Range" header.');i.onError(0)}}else a?i.onDone({begin:0,chunk:a}):i.onError(s.status)}getRequestXhr(t){return this.pendingRequests[t].xhr}isPendingRequest(t){return t in this.pendingRequests}abortRequest(t){const e=this.pendingRequests[t].xhr;delete this.pendingRequests[t];e.abort()}}class PDFNetworkStream{constructor(t){this._source=t;this._manager=new NetworkManager(t);this._rangeChunkSize=t.rangeChunkSize;this._fullRequestReader=null;this._rangeRequestReaders=[]}_onRangeRequestReaderClosed(t){const e=this._rangeRequestReaders.indexOf(t);e>=0&&this._rangeRequestReaders.splice(e,1)}getFullReader(){assert(!this._fullRequestReader,"PDFNetworkStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNetworkStreamFullRequestReader(this._manager,this._source);return this._fullRequestReader}getRangeReader(t,e){const i=new PDFNetworkStreamRangeRequestReader(this._manager,t,e);i.onClosed=this._onRangeRequestReaderClosed.bind(this);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNetworkStreamFullRequestReader{constructor(t,e){this._manager=t;this._url=e.url;this._fullRequestId=t.request({onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._headersCapability=Promise.withResolvers();this._disableRange=e.disableRange||!1;this._contentLength=e.length;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!1;this._isRangeSupported=!1;this._cachedChunks=[];this._requests=[];this._done=!1;this._storedError=void 0;this._filename=null;this.onProgress=null}_onHeadersReceived(){const t=this._fullRequestId,e=this._manager.getRequestXhr(t);this._manager._responseOrigin=getResponseOrigin(e.responseURL);const i=e.getAllResponseHeaders(),s=new Headers(i?i.trimStart().replace(/[^\S ]+$/,"").split(/[\r\n]+/).map((t=>{const[e,...i]=t.split(": ");return[e,i.join(": ")]})):[]),{allowRangeRequests:n,suggestedLength:a}=validateRangeRequestCapabilities({responseHeaders:s,isHttp:this._manager.isHttp,rangeChunkSize:this._rangeChunkSize,disableRange:this._disableRange});n&&(this._isRangeSupported=!0);this._contentLength=a||this._contentLength;this._filename=extractFilenameFromHeader(s);this._isRangeSupported&&this._manager.abortRequest(t);this._headersCapability.resolve()}_onDone(t){if(t)if(this._requests.length>0){this._requests.shift().resolve({value:t.chunk,done:!1})}else this._cachedChunks.push(t.chunk);this._done=!0;if(!(this._cachedChunks.length>0)){for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0}}_onError(t){this._storedError=createResponseError(t,this._url);this._headersCapability.reject(this._storedError);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._cachedChunks.length=0}_onProgress(t){this.onProgress?.({loaded:t.loaded,total:t.lengthComputable?t.total:this._contentLength})}get filename(){return this._filename}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}get contentLength(){return this._contentLength}get headersReady(){return this._headersCapability.promise}async read(){await this._headersCapability.promise;if(this._storedError)throw this._storedError;if(this._cachedChunks.length>0){return{value:this._cachedChunks.shift(),done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;this._headersCapability.reject(t);for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._fullRequestId)&&this._manager.abortRequest(this._fullRequestId);this._fullRequestReader=null}}class PDFNetworkStreamRangeRequestReader{constructor(t,e,i){this._manager=t;this._url=t.url;this._requestId=t.request({begin:e,end:i,onHeadersReceived:this._onHeadersReceived.bind(this),onDone:this._onDone.bind(this),onError:this._onError.bind(this),onProgress:this._onProgress.bind(this)});this._requests=[];this._queuedChunk=null;this._done=!1;this._storedError=void 0;this.onProgress=null;this.onClosed=null}_onHeadersReceived(){const t=getResponseOrigin(this._manager.getRequestXhr(this._requestId)?.responseURL);if(t!==this._manager._responseOrigin){this._storedError=new Error(`Expected range response-origin "${t}" to match "${this._manager._responseOrigin}".`);this._onError(0)}}_close(){this.onClosed?.(this)}_onDone(t){const e=t.chunk;if(this._requests.length>0){this._requests.shift().resolve({value:e,done:!1})}else this._queuedChunk=e;this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._close()}_onError(t){this._storedError??=createResponseError(t,this._url);for(const t of this._requests)t.reject(this._storedError);this._requests.length=0;this._queuedChunk=null}_onProgress(t){this.isStreamingSupported||this.onProgress?.({loaded:t.loaded})}get isStreamingSupported(){return!1}async read(){if(this._storedError)throw this._storedError;if(null!==this._queuedChunk){const t=this._queuedChunk;this._queuedChunk=null;return{value:t,done:!1}}if(this._done)return{value:void 0,done:!0};const t=Promise.withResolvers();this._requests.push(t);return t.promise}cancel(t){this._done=!0;for(const t of this._requests)t.resolve({value:void 0,done:!0});this._requests.length=0;this._manager.isPendingRequest(this._requestId)&&this._manager.abortRequest(this._requestId);this._close()}}const At=/^[a-z][a-z0-9\-+.]+:/i;class PDFNodeStream{constructor(t){this.source=t;this.url=function parseUrlOrPath(t){if(At.test(t))return new URL(t);const e=process.getBuiltinModule("url");return new URL(e.pathToFileURL(t))}(t.url);assert("file:"===this.url.protocol,"PDFNodeStream only supports file:// URLs.");this._fullRequestReader=null;this._rangeRequestReaders=[]}get _progressiveDataLength(){return this._fullRequestReader?._loaded??0}getFullReader(){assert(!this._fullRequestReader,"PDFNodeStream.getFullReader can only be called once.");this._fullRequestReader=new PDFNodeStreamFsFullReader(this);return this._fullRequestReader}getRangeReader(t,e){if(e<=this._progressiveDataLength)return null;const i=new PDFNodeStreamFsRangeReader(this,t,e);this._rangeRequestReaders.push(i);return i}cancelAllRequests(t){this._fullRequestReader?.cancel(t);for(const e of this._rangeRequestReaders.slice(0))e.cancel(t)}}class PDFNodeStreamFsFullReader{constructor(t){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;const e=t.source;this._contentLength=e.length;this._loaded=0;this._filename=null;this._disableRange=e.disableRange||!1;this._rangeChunkSize=e.rangeChunkSize;this._rangeChunkSize||this._disableRange||(this._disableRange=!0);this._isStreamingSupported=!e.disableStream;this._isRangeSupported=!e.disableRange;this._readableStream=null;this._readCapability=Promise.withResolvers();this._headersCapability=Promise.withResolvers();const i=process.getBuiltinModule("fs");i.promises.lstat(this._url).then((t=>{this._contentLength=t.size;this._setReadableStream(i.createReadStream(this._url));this._headersCapability.resolve()}),(t=>{"ENOENT"===t.code&&(t=createResponseError(0,this._url.href));this._storedError=t;this._headersCapability.reject(t)}))}get headersReady(){return this._headersCapability.promise}get filename(){return this._filename}get contentLength(){return this._contentLength}get isRangeSupported(){return this._isRangeSupported}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded,total:this._contentLength});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));!this._isStreamingSupported&&this._isRangeSupported&&this._error(new AbortException("streaming is disabled"));this._storedError&&this._readableStream.destroy(this._storedError)}}class PDFNodeStreamFsRangeReader{constructor(t,e,i){this._url=t.url;this._done=!1;this._storedError=null;this.onProgress=null;this._loaded=0;this._readableStream=null;this._readCapability=Promise.withResolvers();const s=t.source;this._isStreamingSupported=!s.disableStream;const n=process.getBuiltinModule("fs");this._setReadableStream(n.createReadStream(this._url,{start:e,end:i-1}))}get isStreamingSupported(){return this._isStreamingSupported}async read(){await this._readCapability.promise;if(this._done)return{value:void 0,done:!0};if(this._storedError)throw this._storedError;const t=this._readableStream.read();if(null===t){this._readCapability=Promise.withResolvers();return this.read()}this._loaded+=t.length;this.onProgress?.({loaded:this._loaded});return{value:new Uint8Array(t).buffer,done:!1}}cancel(t){this._readableStream?this._readableStream.destroy(t):this._error(t)}_error(t){this._storedError=t;this._readCapability.resolve()}_setReadableStream(t){this._readableStream=t;t.on("readable",(()=>{this._readCapability.resolve()}));t.on("end",(()=>{t.destroy();this._done=!0;this._readCapability.resolve()}));t.on("error",(t=>{this._error(t)}));this._storedError&&this._readableStream.destroy(this._storedError)}}const wt=Symbol("INITIAL_DATA");class PDFObjects{#Xi=Object.create(null);#Ki(t){return this.#Xi[t]||={...Promise.withResolvers(),data:wt}}get(t,e=null){if(e){const i=this.#Ki(t);i.promise.then((()=>e(i.data)));return null}const i=this.#Xi[t];if(!i||i.data===wt)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#Xi[t];return!!e&&e.data!==wt}delete(t){const e=this.#Xi[t];if(!e||e.data===wt)return!1;delete this.#Xi[t];return!0}resolve(t,e=null){const i=this.#Ki(t);i.data=e;i.resolve()}clear(){for(const t in this.#Xi){const{data:e}=this.#Xi[t];e?.bitmap?.close()}this.#Xi=Object.create(null)}*[Symbol.iterator](){for(const t in this.#Xi){const{data:e}=this.#Xi[t];e!==wt&&(yield[t,e])}}}class TextLayer{#Yi=Promise.withResolvers();#mt=null;#Qi=!1;#Ji=!!globalThis.FontInspector?.enabled;#Zi=null;#ts=null;#es=0;#is=0;#ss=null;#ns=null;#as=0;#rs=0;#os=Object.create(null);#ls=[];#hs=null;#ds=[];#cs=new WeakMap;#us=null;static#ps=new Map;static#gs=new Map;static#fs=new WeakMap;static#ms=null;static#bs=new Set;constructor({textContentSource:t,container:e,viewport:i}){if(t instanceof ReadableStream)this.#hs=t;else{if("object"!=typeof t)throw new Error('No "textContentSource" parameter specified.');this.#hs=new ReadableStream({start(e){e.enqueue(t);e.close()}})}this.#mt=this.#ns=e;this.#rs=i.scale*OutputScale.pixelRatio;this.#as=i.rotation;this.#ts={div:null,properties:null,ctx:null};const{pageWidth:s,pageHeight:n,pageX:a,pageY:r}=i.rawDims;this.#us=[1,0,0,-1,-a,r+n];this.#is=s;this.#es=n;TextLayer.#As();setLayerDimensions(e,i);this.#Yi.promise.finally((()=>{TextLayer.#bs.delete(this);this.#ts=null;this.#os=null})).catch((()=>{}))}static get fontFamilyMap(){const{isWindows:t,isFirefox:e}=util_FeatureTest.platform;return shadow(this,"fontFamilyMap",new Map([["sans-serif",(t&&e?"Calibri, ":"")+"sans-serif"],["monospace",(t&&e?"Lucida Console, ":"")+"monospace"]]))}render(){const pump=()=>{this.#ss.read().then((({value:t,done:e})=>{if(e)this.#Yi.resolve();else{this.#Zi??=t.lang;Object.assign(this.#os,t.styles);this.#ws(t.items);pump()}}),this.#Yi.reject)};this.#ss=this.#hs.getReader();TextLayer.#bs.add(this);pump();return this.#Yi.promise}update({viewport:t,onBefore:e=null}){const i=t.scale*OutputScale.pixelRatio,s=t.rotation;if(s!==this.#as){e?.();this.#as=s;setLayerDimensions(this.#ns,{rotation:s})}if(i!==this.#rs){e?.();this.#rs=i;const t={div:null,properties:null,ctx:TextLayer.#ys(this.#Zi)};for(const e of this.#ds){t.properties=this.#cs.get(e);t.div=e;this.#vs(t)}}}cancel(){const t=new AbortException("TextLayer task cancelled.");this.#ss?.cancel(t).catch((()=>{}));this.#ss=null;this.#Yi.reject(t)}get textDivs(){return this.#ds}get textContentItemsStr(){return this.#ls}#ws(t){if(this.#Qi)return;this.#ts.ctx??=TextLayer.#ys(this.#Zi);const e=this.#ds,i=this.#ls;for(const s of t){if(e.length>1e5){warn("Ignoring additional textDivs for performance reasons.");this.#Qi=!0;return}if(void 0!==s.str){i.push(s.str);this.#xs(s)}else if("beginMarkedContentProps"===s.type||"beginMarkedContent"===s.type){const t=this.#mt;this.#mt=document.createElement("span");this.#mt.classList.add("markedContent");null!==s.id&&this.#mt.setAttribute("id",`${s.id}`);t.append(this.#mt)}else"endMarkedContent"===s.type&&(this.#mt=this.#mt.parentNode)}}#xs(t){const e=document.createElement("span"),i={angle:0,canvasWidth:0,hasText:""!==t.str,hasEOL:t.hasEOL,fontSize:0};this.#ds.push(e);const s=Util.transform(this.#us,t.transform);let n=Math.atan2(s[1],s[0]);const a=this.#os[t.fontName];a.vertical&&(n+=Math.PI/2);let r=this.#Ji&&a.fontSubstitution||a.fontFamily;r=TextLayer.fontFamilyMap.get(r)||r;const o=Math.hypot(s[2],s[3]),l=o*TextLayer.#Es(r,a,this.#Zi);let h,d;if(0===n){h=s[4];d=s[5]-l}else{h=s[4]+l*Math.sin(n);d=s[5]-l*Math.cos(n)}const c="calc(var(--total-scale-factor) *",u=e.style;if(this.#mt===this.#ns){u.left=`${(100*h/this.#is).toFixed(2)}%`;u.top=`${(100*d/this.#es).toFixed(2)}%`}else{u.left=`${c}${h.toFixed(2)}px)`;u.top=`${c}${d.toFixed(2)}px)`}u.fontSize=`${c}${(TextLayer.#ms*o).toFixed(2)}px)`;u.fontFamily=r;i.fontSize=o;e.setAttribute("role","presentation");e.textContent=t.str;e.dir=t.dir;this.#Ji&&(e.dataset.fontName=a.fontSubstitutionLoadedName||t.fontName);0!==n&&(i.angle=n*(180/Math.PI));let p=!1;if(t.str.length>1)p=!0;else if(" "!==t.str&&t.transform[0]!==t.transform[3]){const e=Math.abs(t.transform[0]),i=Math.abs(t.transform[3]);e!==i&&Math.max(e,i)/Math.min(e,i)>1.5&&(p=!0)}p&&(i.canvasWidth=a.vertical?t.height:t.width);this.#cs.set(e,i);this.#ts.div=e;this.#ts.properties=i;this.#vs(this.#ts);i.hasText&&this.#mt.append(e);if(i.hasEOL){const t=document.createElement("br");t.setAttribute("role","presentation");this.#mt.append(t)}}#vs(t){const{div:e,properties:i,ctx:s}=t,{style:n}=e;let a="";TextLayer.#ms>1&&(a=`scale(${1/TextLayer.#ms})`);if(0!==i.canvasWidth&&i.hasText){const{fontFamily:t}=n,{canvasWidth:r,fontSize:o}=i;TextLayer.#_s(s,o*this.#rs,t);const{width:l}=s.measureText(e.textContent);l>0&&(a=`scaleX(${r*this.#rs/l}) ${a}`)}0!==i.angle&&(a=`rotate(${i.angle}deg) ${a}`);a.length>0&&(n.transform=a)}static cleanup(){if(!(this.#bs.size>0)){this.#ps.clear();for(const{canvas:t}of this.#gs.values())t.remove();this.#gs.clear()}}static#ys(t=null){let e=this.#gs.get(t||="");if(!e){const i=document.createElement("canvas");i.className="hiddenCanvasElement";i.lang=t;document.body.append(i);e=i.getContext("2d",{alpha:!1,willReadFrequently:!0});this.#gs.set(t,e);this.#fs.set(e,{size:0,family:""})}return e}static#_s(t,e,i){const s=this.#fs.get(t);if(e!==s.size||i!==s.family){t.font=`${e}px ${i}`;s.size=e;s.family=i}}static#As(){if(null!==this.#ms)return;const t=document.createElement("div");t.style.opacity=0;t.style.lineHeight=1;t.style.fontSize="1px";t.style.position="absolute";t.textContent="X";document.body.append(t);this.#ms=t.getBoundingClientRect().height;t.remove()}static#Es(t,e,i){const s=this.#ps.get(t);if(s)return s;const n=this.#ys(i);n.canvas.width=n.canvas.height=30;this.#_s(n,30,t);const a=n.measureText(""),r=a.fontBoundingBoxAscent,o=Math.abs(a.fontBoundingBoxDescent);n.canvas.width=n.canvas.height=0;let l=.8;if(r)l=r/(r+o);else{util_FeatureTest.platform.isFirefox&&warn("Enable the `dom.textMetrics.fontBoundingBox.enabled` preference in `about:config` to improve TextLayer rendering.");e.ascent?l=e.ascent:e.descent&&(l=1+e.descent)}this.#ps.set(t,l);return l}}class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}function getDocument(e={}){"string"==typeof e||e instanceof URL?e={url:e}:(e instanceof ArrayBuffer||ArrayBuffer.isView(e))&&(e={data:e});const i=new PDFDocumentLoadingTask,{docId:s}=i,n=e.url?function getUrlProp(e){if(e instanceof URL)return e.href;if("string"==typeof e){if(t)return e;const i=URL.parse(e,window.location);if(i)return i.href}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}(e.url):null,a=e.data?function getDataProp(e){if(t&&"undefined"!=typeof Buffer&&e instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(e instanceof Uint8Array&&e.byteLength===e.buffer.byteLength)return e;if("string"==typeof e)return stringToBytes(e);if(e instanceof ArrayBuffer||ArrayBuffer.isView(e)||"object"==typeof e&&!isNaN(e?.length))return new Uint8Array(e);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}(e.data):null,r=e.httpHeaders||null,o=!0===e.withCredentials,l=e.password??null,h=e.range instanceof PDFDataRangeTransport?e.range:null,d=Number.isInteger(e.rangeChunkSize)&&e.rangeChunkSize>0?e.rangeChunkSize:65536;let c=e.worker instanceof PDFWorker?e.worker:null;const u=e.verbosity,p="string"!=typeof e.docBaseUrl||isDataScheme(e.docBaseUrl)?null:e.docBaseUrl,g=getFactoryUrlProp(e.cMapUrl),f=!1!==e.cMapPacked,m=e.CMapReaderFactory||(t?NodeCMapReaderFactory:DOMCMapReaderFactory),b=getFactoryUrlProp(e.iccUrl),A=getFactoryUrlProp(e.standardFontDataUrl),w=e.StandardFontDataFactory||(t?NodeStandardFontDataFactory:DOMStandardFontDataFactory),y=getFactoryUrlProp(e.wasmUrl),v=e.WasmFactory||(t?NodeWasmFactory:DOMWasmFactory),x=!0!==e.stopAtErrors,E=Number.isInteger(e.maxImageSize)&&e.maxImageSize>-1?e.maxImageSize:-1,_=!1!==e.isEvalSupported,S="boolean"==typeof e.isOffscreenCanvasSupported?e.isOffscreenCanvasSupported:!t,C="boolean"==typeof e.isImageDecoderSupported?e.isImageDecoderSupported:!t&&(util_FeatureTest.platform.isFirefox||!globalThis.chrome),T=Number.isInteger(e.canvasMaxAreaInBytes)?e.canvasMaxAreaInBytes:-1,M="boolean"==typeof e.disableFontFace?e.disableFontFace:t,D=!0===e.fontExtraProperties,P=!0===e.enableXfa,k=e.ownerDocument||globalThis.document,I=!0===e.disableRange,R=!0===e.disableStream,F=!0===e.disableAutoFetch,L=!0===e.pdfBug,O=e.CanvasFactory||(t?NodeCanvasFactory:DOMCanvasFactory),N=e.FilterFactory||(t?NodeFilterFactory:DOMFilterFactory),B=!0===e.enableHWA,H=!1!==e.useWasm,U=h?h.length:e.length??NaN,z="boolean"==typeof e.useSystemFonts?e.useSystemFonts:!t&&!M,G="boolean"==typeof e.useWorkerFetch?e.useWorkerFetch:!!(m===DOMCMapReaderFactory&&w===DOMStandardFontDataFactory&&v===DOMWasmFactory&&g&&A&&y&&isValidFetchUrl(g,document.baseURI)&&isValidFetchUrl(A,document.baseURI)&&isValidFetchUrl(y,document.baseURI));setVerbosityLevel(u);const $={canvasFactory:new O({ownerDocument:k,enableHWA:B}),filterFactory:new N({docId:s,ownerDocument:k}),cMapReaderFactory:G?null:new m({baseUrl:g,isCompressed:f}),standardFontDataFactory:G?null:new w({baseUrl:A}),wasmFactory:G?null:new v({baseUrl:y})};if(!c){c=PDFWorker.create({verbosity:u,port:GlobalWorkerOptions.workerPort});i._worker=c}const W={docId:s,apiVersion:"5.3.31",data:a,password:l,disableAutoFetch:F,rangeChunkSize:d,length:U,docBaseUrl:p,enableXfa:P,evaluatorOptions:{maxImageSize:E,disableFontFace:M,ignoreErrors:x,isEvalSupported:_,isOffscreenCanvasSupported:S,isImageDecoderSupported:C,canvasMaxAreaInBytes:T,fontExtraProperties:D,useSystemFonts:z,useWasm:H,useWorkerFetch:G,cMapUrl:g,iccUrl:b,standardFontDataUrl:A,wasmUrl:y}},V={ownerDocument:k,pdfBug:L,styleElement:null,loadingParams:{disableAutoFetch:F,enableXfa:P}};c.promise.then((function(){if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=c.messageHandler.sendWithPromise("GetDocRequest",W,a?[a.buffer]:null);let l;if(h)l=new PDFDataTransportStream(h,{disableRange:I,disableStream:R});else if(!a){if(!n)throw new Error("getDocument - no `url` parameter provided.");const e=isValidFetchUrl(n)?PDFFetchStream:t?PDFNodeStream:PDFNetworkStream;l=new e({url:n,length:U,httpHeaders:r,withCredentials:o,rangeChunkSize:d,disableRange:I,disableStream:R})}return e.then((t=>{if(i.destroyed)throw new Error("Loading aborted");if(c.destroyed)throw new Error("Worker was destroyed");const e=new MessageHandler(s,t,c.port),n=new WorkerTransport(e,i,l,V,$);i._transport=n;e.send("Ready",null)}))})).catch(i._capability.reject);return i}class PDFDocumentLoadingTask{static#Ai=0;_capability=Promise.withResolvers();_transport=null;_worker=null;docId="d"+PDFDocumentLoadingTask.#Ai++;destroyed=!1;onPassword=null;onProgress=null;get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;this._worker?.destroy();this._worker=null}async getData(){return this._transport.getData()}}class PDFDataRangeTransport{#Yi=Promise.withResolvers();#Ss=[];#Cs=[];#Ts=[];#Ms=[];constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s}addRangeListener(t){this.#Ms.push(t)}addProgressListener(t){this.#Ts.push(t)}addProgressiveReadListener(t){this.#Cs.push(t)}addProgressiveDoneListener(t){this.#Ss.push(t)}onDataRange(t,e){for(const i of this.#Ms)i(t,e)}onDataProgress(t,e){this.#Yi.promise.then((()=>{for(const i of this.#Ts)i(t,e)}))}onDataProgressiveRead(t){this.#Yi.promise.then((()=>{for(const e of this.#Cs)e(t)}))}onDataProgressiveDone(){this.#Yi.promise.then((()=>{for(const t of this.#Ss)t()}))}transportReady(){this.#Yi.resolve()}requestDataRange(t,e){unreachable("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e}get annotationStorage(){return this._transport.annotationStorage}get canvasFactory(){return this._transport.canvasFactory}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getOptionalContentConfig(e)}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}cachedPageNumber(t){return this._transport.cachedPageNumber(t)}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}class PDFPageProxy{#Ds=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new PageViewport({viewBox:this.view,userUnit:this.userUnit,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const{renderingIntent:e}=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return shadow(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=c.ENABLE,transform:n=null,background:r=null,optionalContentConfigPromise:o=null,annotationCanvasMap:l=null,pageColors:h=null,printAnnotationStorage:d=null,isEditing:u=!1}){this._stats?.time("Overall");const p=this._transport.getRenderingIntent(i,s,d,u),{renderingIntent:g,cacheKey:f}=p;this.#Ds=!1;o||=this._transport.getOptionalContentConfig(g);let m=this._intentStates.get(f);if(!m){m=Object.create(null);this._intentStates.set(f,m)}if(m.streamReaderCancelTimeout){clearTimeout(m.streamReaderCancelTimeout);m.streamReaderCancelTimeout=null}const b=!!(g&a);if(!m.displayReadyCapability){m.displayReadyCapability=Promise.withResolvers();m.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(p)}const complete=t=>{m.renderTasks.delete(A);b&&(this.#Ds=!0);this.#Ps();if(t){A.capability.reject(t);this._abortOperatorList({intentState:m,reason:t instanceof Error?t:new Error(t)})}else A.capability.resolve();if(this._stats){this._stats.timeEnd("Rendering");this._stats.timeEnd("Overall");globalThis.Stats?.enabled&&globalThis.Stats.add(this.pageNumber,this._stats)}},A=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:r},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:l,operatorList:m.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!b,pdfBug:this._pdfBug,pageColors:h});(m.renderTasks||=new Set).add(A);const w=A.task;Promise.all([m.displayReadyCapability.promise,o]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");if(!(e.renderingIntent&g))throw new Error("Must use the same `intent`-argument when calling the `PDFPageProxy.render` and `PDFDocumentProxy.getOptionalContentConfig` methods.");A.initializeGraphics({transparency:t,optionalContentConfig:e});A.operatorListChanged()}})).catch(complete);return w}getOperatorList({intent:t="display",annotationMode:e=c.ENABLE,printAnnotationStorage:i=null,isEditing:s=!1}={}){const n=this._transport.getRenderingIntent(t,e,i,s,!0);let a,r=this._intentStates.get(n.cacheKey);if(!r){r=Object.create(null);this._intentStates.set(n.cacheKey,r)}if(!r.opListReadCapability){a=Object.create(null);a.operatorListChanged=function operatorListChanged(){if(r.operatorList.lastChunk){r.opListReadCapability.resolve(r.operatorList);r.renderTasks.delete(a)}};r.opListReadCapability=Promise.withResolvers();(r.renderTasks||=new Set).add(a);r.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(n)}return r.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null),lang:null};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{n.lang??=e.lang;Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#Ds=!1;return Promise.all(t)}cleanup(t=!1){this.#Ds=!0;const e=this.#Ps();t&&e&&(this._stats&&=new StatTimer);return e}#Ps(){if(!this.#Ds||this.destroyed)return!1;for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#Ds=!1;return!0}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i{r.read().then((({value:t,done:e})=>{if(e)o.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,o);pump()}}),(t=>{o.streamReader=null;if(!this._transport.destroyed){if(o.operatorList){o.operatorList.lastChunk=!0;for(const t of o.renderTasks)t.operatorListChanged();this.#Ps()}if(o.displayReadyCapability)o.displayReadyCapability.reject(t);else{if(!o.opListReadCapability)throw t;o.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof RenderingCancelledException){let i=100;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}class PDFWorker{#Yi=Promise.withResolvers();#ks=null;#Li=null;#Is=null;static#Rs=0;static#Fs=!1;static#Ls=new WeakMap;static{if(t){this.#Fs=!0;GlobalWorkerOptions.workerSrc||="./pdf.worker.mjs"}this._isSameOrigin=(t,e)=>{const i=URL.parse(t);if(!i?.origin||"null"===i.origin)return!1;const s=new URL(e,i);return i.origin===s.origin};this._createCDNWrapper=t=>{const e=`await import("${t}");`;return URL.createObjectURL(new Blob([e],{type:"text/javascript"}))};this.fromPort=t=>{!function deprecated(t){console.log("Deprecated API usage: "+t)}("`PDFWorker.fromPort` - please use `PDFWorker.create` instead.");if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");return this.create(t)}}constructor({name:t=null,port:e=null,verbosity:i=getVerbosityLevel()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;if(e){if(PDFWorker.#Ls.has(e))throw new Error("Cannot use more than one PDFWorker per port.");PDFWorker.#Ls.set(e,this);this.#Os(e)}else this.#Ns()}get promise(){return this.#Yi.promise}#Bs(){this.#Yi.resolve();this.#ks.send("configure",{verbosity:this.verbosity})}get port(){return this.#Li}get messageHandler(){return this.#ks}#Os(t){this.#Li=t;this.#ks=new MessageHandler("main","worker",t);this.#ks.on("ready",(()=>{}));this.#Bs()}#Ns(){if(PDFWorker.#Fs||PDFWorker.#Hs){this.#Us();return}let{workerSrc:t}=PDFWorker;try{PDFWorker._isSameOrigin(window.location,t)||(t=PDFWorker._createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t,{type:"module"}),i=new MessageHandler("main","worker",e),terminateEarly=()=>{s.abort();i.destroy();e.terminate();this.destroyed?this.#Yi.reject(new Error("Worker was destroyed")):this.#Us()},s=new AbortController;e.addEventListener("error",(()=>{this.#Is||terminateEarly()}),{signal:s.signal});i.on("test",(t=>{s.abort();if(!this.destroyed&&t){this.#ks=i;this.#Li=e;this.#Is=e;this.#Bs()}else terminateEarly()}));i.on("ready",(t=>{s.abort();if(this.destroyed)terminateEarly();else try{sendTest()}catch{this.#Us()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{info("The worker has been disabled.")}this.#Us()}#Us(){if(!PDFWorker.#Fs){warn("Setting up fake worker.");PDFWorker.#Fs=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this.#Yi.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this.#Li=e;const i="fake"+PDFWorker.#Rs++,s=new MessageHandler(i+"_worker",i,e);t.setup(s,e);this.#ks=new MessageHandler(i,i+"_worker",e);this.#Bs()})).catch((t=>{this.#Yi.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;this.#Is?.terminate();this.#Is=null;PDFWorker.#Ls.delete(this.#Li);this.#Li=null;this.#ks?.destroy();this.#ks=null}static create(t){const e=this.#Ls.get(t?.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.create - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(GlobalWorkerOptions.workerSrc)return GlobalWorkerOptions.workerSrc;throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get#Hs(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){return shadow(this,"_setupFakeWorkerGlobal",(async()=>{if(this.#Hs)return this.#Hs;return(await import( -/*webpackIgnore: true*/ -/*@vite-ignore*/ -this.workerSrc)).WorkerMessageHandler})())}}class WorkerTransport{#zs=new Map;#Gs=new Map;#$s=new Map;#Ws=new Map;#Vs=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this.loadingParams=s.loadingParams;this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.wasmFactory=n.wasmFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=Promise.withResolvers();this.setupMessageHandler()}#js(t,e=null){const i=this.#zs.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#zs.set(t,s);return s}get annotationStorage(){return shadow(this,"annotationStorage",new AnnotationStorage)}getRenderingIntent(t,e=c.ENABLE,i=null,u=!1,p=!1){let g=n,f=j;switch(t){case"any":g=s;break;case"display":break;case"print":g=a;break;default:warn(`getRenderingIntent - invalid intent: ${t}`)}const m=g&a&&i instanceof PrintAnnotationStorage?i:this.annotationStorage;switch(e){case c.DISABLE:g+=l;break;case c.ENABLE:break;case c.ENABLE_FORMS:g+=r;break;case c.ENABLE_STORAGE:g+=o;f=m.serializable;break;default:warn(`getRenderingIntent - invalid annotationMode: ${e}`)}u&&(g+=h);p&&(g+=d);const{ids:b,hash:A}=m.modifiedIds;return{renderingIntent:g,cacheKey:[g,f.hash,A].join("_"),annotationStorageSerializable:f,modifiedIds:b}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=Promise.withResolvers();this.#Vs?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#Gs.values())t.push(e._destroy());this.#Gs.clear();this.#$s.clear();this.#Ws.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#zs.clear();this.filterFactory.destroy();TextLayer.cleanup();this._networkStream?.cancelAllRequests(new AbortException("Worker was terminated."));this.messageHandler?.destroy();this.messageHandler=null;this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{assert(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(async t=>{await this._fullReader.headersReady;const{isStreamingSupported:i,isRangeSupported:s,contentLength:n}=this._fullReader;if(!i||!s){this._lastProgress&&e.onProgress?.(this._lastProgress);this._fullReader.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}return{isStreamingSupported:i,isRangeSupported:s,contentLength:n}}));t.on("GetRangeReader",((t,e)=>{assert(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{assert(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(t=>{e._capability.reject(wrapReason(t))}));t.on("PasswordRequest",(t=>{this.#Vs=Promise.withResolvers();try{if(!e.onPassword)throw wrapReason(t);const updatePassword=t=>{t instanceof Error?this.#Vs.reject(t):this.#Vs.resolve({password:t})};e.onPassword(updatePassword,t.code)}catch(t){this.#Vs.reject(t)}return this.#Vs.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#Gs.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(this.destroyed)return null;if(this.commonObjs.has(e))return null;switch(i){case"Font":if("error"in s){const t=s.error;warn(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const n=this._params.pdfBug&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,a=new FontFaceObject(s,n);this.fontLoader.bind(a).catch((()=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!a.fontExtraProperties&&a.data&&(a.data=null);this.commonObjs.resolve(e,a)}));break;case"CopyLocalImage":const{imageRef:r}=s;assert(r,"The imageRef must be defined.");for(const t of this.#Gs.values())for(const[,i]of t.objs)if(i?.ref===r){if(!i.dataLen)return null;this.commonObjs.resolve(e,structuredClone(i));return i.dataLen}break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}return null}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#Gs.get(e);if(!n.objs.has(t))if(0!==n._intentStates.size)switch(i){case"Image":case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}else s?.bitmap?.close()}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBinaryData",(async t=>{if(this.destroyed)throw new Error("Worker was destroyed.");const e=this[t.type];if(!e)throw new Error(`${t.type} not initialized, see the \`useWorkerFetch\` parameter.`);return e.fetch(t)}))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&warn("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfer:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#$s.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((i=>{if(this.destroyed)throw new Error("Transport destroyed");i.refStr&&this.#Ws.set(i.refStr,t);const s=new PDFPageProxy(e,i,this,this._params.pdfBug);this.#Gs.set(e,s);return s}));this.#$s.set(e,s);return s}getPageIndex(t){return isRefProxy(t)?this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen}):Promise.reject(new Error("Invalid pageIndex request."))}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#js("GetFieldObjects")}hasJSActions(){return this.#js("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#js("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(t){return this.#js("GetOptionalContentConfig").then((e=>new OptionalContentConfig(e,t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#zs.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#zs.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#Gs.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#zs.clear();this.filterFactory.destroy(!0);TextLayer.cleanup()}}cachedPageNumber(t){if(!isRefProxy(t))return null;const e=0===t.gen?`${t.num}R`:`${t.num}R${t.gen}`;return this.#Ws.get(e)??null}}class RenderTask{#qs=null;onContinue=null;onError=null;constructor(t){this.#qs=t}get promise(){return this.#qs.capability.promise}cancel(t=0){this.#qs.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#qs.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#qs;return t.form||t.canvas&&e?.size>0}}class InternalRenderTask{#Xs=null;static#Ks=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:d=!1,pageColors:c=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=d;this.pageColors=c;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=Promise.withResolvers();this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#Ks.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#Ks.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();if(this.#Xs){window.cancelAnimationFrame(this.#Xs);this.#Xs=null}InternalRenderTask.#Ks.delete(this._canvas);t||=new RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e);this.callback(t);this.task.onError?.(t)}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?this.#Xs=window.requestAnimationFrame((()=>{this.#Xs=null;this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#Ks.delete(this._canvas);this.callback()}}}}}const yt="5.3.31",vt="47ad820d9";function makeColorComp(t){return Math.floor(255*Math.max(0,Math.min(1,t))).toString(16).padStart(2,"0")}function scaleAndClamp(t){return Math.max(0,Math.min(255,255*t))}class ColorConverters{static CMYK_G([t,e,i,s]){return["G",1-Math.min(1,.3*t+.59*i+.11*e+s)]}static G_CMYK([t]){return["CMYK",0,0,0,1-t]}static G_RGB([t]){return["RGB",t,t,t]}static G_rgb([t]){return[t=scaleAndClamp(t),t,t]}static G_HTML([t]){const e=makeColorComp(t);return`#${e}${e}${e}`}static RGB_G([t,e,i]){return["G",.3*t+.59*e+.11*i]}static RGB_rgb(t){return t.map(scaleAndClamp)}static RGB_HTML(t){return`#${t.map(makeColorComp).join("")}`}static T_HTML(){return"#00000000"}static T_rgb(){return[null]}static CMYK_RGB([t,e,i,s]){return["RGB",1-Math.min(1,t+s),1-Math.min(1,i+s),1-Math.min(1,e+s)]}static CMYK_rgb([t,e,i,s]){return[scaleAndClamp(1-Math.min(1,t+s)),scaleAndClamp(1-Math.min(1,i+s)),scaleAndClamp(1-Math.min(1,e+s))]}static CMYK_HTML(t){const e=this.CMYK_RGB(t).slice(1);return this.RGB_HTML(e)}static RGB_CMYK([t,e,i]){const s=1-t,n=1-e,a=1-i;return["CMYK",s,n,a,Math.min(s,n,a)]}}class BaseSVGFactory{create(t,e,i=!1){if(t<=0||e<=0)throw new Error("Invalid SVG dimensions");const s=this._createSVG("svg:svg");s.setAttribute("version","1.1");if(!i){s.setAttribute("width",`${t}px`);s.setAttribute("height",`${e}px`)}s.setAttribute("preserveAspectRatio","none");s.setAttribute("viewBox",`0 0 ${t} ${e}`);return s}createElement(t){if("string"!=typeof t)throw new Error("Invalid SVG element type");return this._createSVG(t)}_createSVG(t){unreachable("Abstract method `_createSVG` called.")}}class DOMSVGFactory extends BaseSVGFactory{_createSVG(t){return document.createElementNS(z,t)}}class XfaLayer{static setupStorage(t,e,i,s,n){const a=s.getValue(e,{value:null});switch(i.name){case"textarea":null!==a.value&&(t.textContent=a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}));break;case"input":if("radio"===i.attributes.type||"checkbox"===i.attributes.type){a.value===i.attributes.xfaOn?t.setAttribute("checked",!0):a.value===i.attributes.xfaOff&&t.removeAttribute("checked");if("print"===n)break;t.addEventListener("change",(t=>{s.setValue(e,{value:t.target.checked?t.target.getAttribute("xfaOn"):t.target.getAttribute("xfaOff")})}))}else{null!==a.value&&t.setAttribute("value",a.value);if("print"===n)break;t.addEventListener("input",(t=>{s.setValue(e,{value:t.target.value})}))}break;case"select":if(null!==a.value){t.setAttribute("value",a.value);for(const t of i.children)t.attributes.value===a.value?t.attributes.selected=!0:t.attributes.hasOwnProperty("selected")&&delete t.attributes.selected}t.addEventListener("input",(t=>{const i=t.target.options,n=-1===i.selectedIndex?"":i[i.selectedIndex].value;s.setValue(e,{value:n})}))}}static setAttributes({html:t,element:e,storage:i=null,intent:s,linkService:n}){const{attributes:a}=e,r=t instanceof HTMLAnchorElement;"radio"===a.type&&(a.name=`${a.name}-${s}`);for(const[e,i]of Object.entries(a))if(null!=i)switch(e){case"class":i.length&&t.setAttribute(e,i.join(" "));break;case"dataId":break;case"id":t.setAttribute("data-element-id",i);break;case"style":Object.assign(t.style,i);break;case"textContent":t.textContent=i;break;default:(!r||"href"!==e&&"newWindow"!==e)&&t.setAttribute(e,i)}r&&n.addLinkAttributes(t,a.href,a.newWindow);i&&a.dataId&&this.setupStorage(t,a.dataId,e,i)}static render(t){const e=t.annotationStorage,i=t.linkService,s=t.xfaHtml,n=t.intent||"display",a=document.createElement(s.name);s.attributes&&this.setAttributes({html:a,element:s,intent:n,linkService:i});const r="richText"!==n,o=t.div;o.append(a);if(t.viewport){const e=`matrix(${t.viewport.transform.join(",")})`;o.style.transform=e}r&&o.setAttribute("class","xfaLayer xfaFont");const l=[];if(0===s.children.length){if(s.value){const t=document.createTextNode(s.value);a.append(t);r&&XfaText.shouldBuildText(s.name)&&l.push(t)}return{textDivs:l}}const h=[[s,-1,a]];for(;h.length>0;){const[t,s,a]=h.at(-1);if(s+1===t.children.length){h.pop();continue}const o=t.children[++h.at(-1)[1]];if(null===o)continue;const{name:d}=o;if("#text"===d){const t=document.createTextNode(o.value);l.push(t);a.append(t);continue}const c=o?.attributes?.xmlns?document.createElementNS(o.attributes.xmlns,d):document.createElement(d);a.append(c);o.attributes&&this.setAttributes({html:c,element:o,storage:e,intent:n,linkService:i});if(o.children?.length>0)h.push([o,-1,c]);else if(o.value){const t=document.createTextNode(o.value);r&&XfaText.shouldBuildText(d)&&l.push(t);c.append(t)}}for(const t of o.querySelectorAll(".xfaNonInteractive input, .xfaNonInteractive textarea"))t.setAttribute("readOnly",!0);return{textDivs:l}}static update(t){const e=`matrix(${t.viewport.transform.join(",")})`;t.div.style.transform=e;t.div.hidden=!1}}const xt=1e3,Et=new WeakSet;class AnnotationElementFactory{static create(t){switch(t.data.annotationType){case x.LINK:return new LinkAnnotationElement(t);case x.TEXT:return new TextAnnotationElement(t);case x.WIDGET:switch(t.data.fieldType){case"Tx":return new TextWidgetAnnotationElement(t);case"Btn":return t.data.radioButton?new RadioButtonWidgetAnnotationElement(t):t.data.checkBox?new CheckboxWidgetAnnotationElement(t):new PushButtonWidgetAnnotationElement(t);case"Ch":return new ChoiceWidgetAnnotationElement(t);case"Sig":return new SignatureWidgetAnnotationElement(t)}return new WidgetAnnotationElement(t);case x.POPUP:return new PopupAnnotationElement(t);case x.FREETEXT:return new FreeTextAnnotationElement(t);case x.LINE:return new LineAnnotationElement(t);case x.SQUARE:return new SquareAnnotationElement(t);case x.CIRCLE:return new CircleAnnotationElement(t);case x.POLYLINE:return new PolylineAnnotationElement(t);case x.CARET:return new CaretAnnotationElement(t);case x.INK:return new InkAnnotationElement(t);case x.POLYGON:return new PolygonAnnotationElement(t);case x.HIGHLIGHT:return new HighlightAnnotationElement(t);case x.UNDERLINE:return new UnderlineAnnotationElement(t);case x.SQUIGGLY:return new SquigglyAnnotationElement(t);case x.STRIKEOUT:return new StrikeOutAnnotationElement(t);case x.STAMP:return new StampAnnotationElement(t);case x.FILEATTACHMENT:return new FileAttachmentAnnotationElement(t);default:return new AnnotationElement(t)}}}class AnnotationElement{#Ys=null;#Qs=!1;#Js=null;constructor(t,{isRenderable:e=!1,ignoreBorder:i=!1,createQuadrilaterals:s=!1}={}){this.isRenderable=e;this.data=t.data;this.layer=t.layer;this.linkService=t.linkService;this.downloadManager=t.downloadManager;this.imageResourcesPath=t.imageResourcesPath;this.renderForms=t.renderForms;this.svgFactory=t.svgFactory;this.annotationStorage=t.annotationStorage;this.enableScripting=t.enableScripting;this.hasJSActions=t.hasJSActions;this._fieldObjects=t.fieldObjects;this.parent=t.parent;e&&(this.container=this._createContainer(i));s&&this._createQuadrilaterals()}static _hasPopupData({titleObj:t,contentsObj:e,richText:i}){return!!(t?.str||e?.str||i?.str)}get _isEditable(){return this.data.isEditable}get hasPopupData(){return AnnotationElement._hasPopupData(this.data)}updateEdited(t){if(!this.container)return;this.#Ys||={rect:this.data.rect.slice(0)};const{rect:e}=t;e&&this.#Zs(e);this.#Js?.popup.updateEdited(t)}resetEdited(){if(this.#Ys){this.#Zs(this.#Ys.rect);this.#Js?.popup.resetEdited();this.#Ys=null}}#Zs(t){const{container:{style:e},data:{rect:i,rotation:s},parent:{viewport:{rawDims:{pageWidth:n,pageHeight:a,pageX:r,pageY:o}}}}=this;i?.splice(0,4,...t);e.left=100*(t[0]-r)/n+"%";e.top=100*(a-t[3]+o)/a+"%";if(0===s){e.width=100*(t[2]-t[0])/n+"%";e.height=100*(t[3]-t[1])/a+"%"}else this.setRotation(s)}_createContainer(t){const{data:e,parent:{page:i,viewport:s}}=this,n=document.createElement("section");n.setAttribute("data-annotation-id",e.id);this instanceof WidgetAnnotationElement||(n.tabIndex=xt);const{style:a}=n;a.zIndex=this.parent.zIndex++;e.alternativeText&&(n.title=e.alternativeText);e.noRotate&&n.classList.add("norotate");if(!e.rect||this instanceof PopupAnnotationElement){const{rotation:t}=e;e.hasOwnCanvas||0===t||this.setRotation(t,n);return n}const{width:r,height:o}=this;if(!t&&e.borderStyle.width>0){a.borderWidth=`${e.borderStyle.width}px`;const t=e.borderStyle.horizontalCornerRadius,i=e.borderStyle.verticalCornerRadius;if(t>0||i>0){const e=`calc(${t}px * var(--total-scale-factor)) / calc(${i}px * var(--total-scale-factor))`;a.borderRadius=e}else if(this instanceof RadioButtonWidgetAnnotationElement){const t=`calc(${r}px * var(--total-scale-factor)) / calc(${o}px * var(--total-scale-factor))`;a.borderRadius=t}switch(e.borderStyle.style){case E:a.borderStyle="solid";break;case _:a.borderStyle="dashed";break;case S:warn("Unimplemented border style: beveled");break;case C:warn("Unimplemented border style: inset");break;case T:a.borderBottomStyle="solid"}const s=e.borderColor||null;if(s){this.#Qs=!0;a.borderColor=Util.makeHexColor(0|s[0],0|s[1],0|s[2])}else a.borderWidth=0}const l=Util.normalizeRect([e.rect[0],i.view[3]-e.rect[1]+i.view[1],e.rect[2],i.view[3]-e.rect[3]+i.view[1]]),{pageWidth:h,pageHeight:d,pageX:c,pageY:u}=s.rawDims;a.left=100*(l[0]-c)/h+"%";a.top=100*(l[1]-u)/d+"%";const{rotation:p}=e;if(e.hasOwnCanvas||0===p){a.width=100*r/h+"%";a.height=100*o/d+"%"}else this.setRotation(p,n);return n}setRotation(t,e=this.container){if(!this.data.rect)return;const{pageWidth:i,pageHeight:s}=this.parent.viewport.rawDims;let{width:n,height:a}=this;t%180!=0&&([n,a]=[a,n]);e.style.width=100*n/i+"%";e.style.height=100*a/s+"%";e.setAttribute("data-main-rotation",(360-t)%360)}get _commonActions(){const setColor=(t,e,i)=>{const s=i.detail[t],n=s[0],a=s.slice(1);i.target.style[e]=ColorConverters[`${n}_HTML`](a);this.annotationStorage.setValue(this.data.id,{[e]:ColorConverters[`${n}_rgb`](a)})};return shadow(this,"_commonActions",{display:t=>{const{display:e}=t.detail,i=e%2==1;this.container.style.visibility=i?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noView:i,noPrint:1===e||2===e})},print:t=>{this.annotationStorage.setValue(this.data.id,{noPrint:!t.detail.print})},hidden:t=>{const{hidden:e}=t.detail;this.container.style.visibility=e?"hidden":"visible";this.annotationStorage.setValue(this.data.id,{noPrint:e,noView:e})},focus:t=>{setTimeout((()=>t.target.focus({preventScroll:!1})),0)},userName:t=>{t.target.title=t.detail.userName},readonly:t=>{t.target.disabled=t.detail.readonly},required:t=>{this._setRequired(t.target,t.detail.required)},bgColor:t=>{setColor("bgColor","backgroundColor",t)},fillColor:t=>{setColor("fillColor","backgroundColor",t)},fgColor:t=>{setColor("fgColor","color",t)},textColor:t=>{setColor("textColor","color",t)},borderColor:t=>{setColor("borderColor","borderColor",t)},strokeColor:t=>{setColor("strokeColor","borderColor",t)},rotation:t=>{const e=t.detail.rotation;this.setRotation(e);this.annotationStorage.setValue(this.data.id,{rotation:e})}})}_dispatchEventFromSandbox(t,e){const i=this._commonActions;for(const s of Object.keys(e.detail)){const n=t[s]||i[s];n?.(e)}}_setDefaultPropertiesFromJS(t){if(!this.enableScripting)return;const e=this.annotationStorage.getRawValue(this.data.id);if(!e)return;const i=this._commonActions;for(const[s,n]of Object.entries(e)){const a=i[s];if(a){a({detail:{[s]:n},target:t});delete e[s]}}}_createQuadrilaterals(){if(!this.container)return;const{quadPoints:t}=this.data;if(!t)return;const[e,i,s,n]=this.data.rect.map((t=>Math.fround(t)));if(8===t.length){const[a,r,o,l]=t.subarray(2,6);if(s===a&&n===r&&e===o&&i===l)return}const{style:a}=this.container;let r;if(this.#Qs){const{borderColor:t,borderWidth:e}=a;a.borderWidth=0;r=["url('data:image/svg+xml;utf8,",'',``];this.container.classList.add("hasBorder")}const o=s-e,l=n-i,{svgFactory:h}=this,d=h.createElement("svg");d.classList.add("quadrilateralsContainer");d.setAttribute("width",0);d.setAttribute("height",0);const c=h.createElement("defs");d.append(c);const u=h.createElement("clipPath"),p=`clippath_${this.data.id}`;u.setAttribute("id",p);u.setAttribute("clipPathUnits","objectBoundingBox");c.append(u);for(let i=2,s=t.length;i`)}if(this.#Qs){r.push("')");a.backgroundImage=r.join("")}this.container.append(d);this.container.style.clipPath=`url(#${p})`}_createPopup(){const{data:t}=this,e=this.#Js=new PopupAnnotationElement({data:{color:t.color,titleObj:t.titleObj,modificationDate:t.modificationDate,contentsObj:t.contentsObj,richText:t.richText,parentRect:t.rect,borderStyle:0,id:`popup_${t.id}`,rotation:t.rotation},parent:this.parent,elements:[this]});this.parent.div.append(e.render())}render(){unreachable("Abstract method `AnnotationElement.render` called")}_getElementsByName(t,e=null){const i=[];if(this._fieldObjects){const s=this._fieldObjects[t];if(s)for(const{page:t,id:n,exportValues:a}of s){if(-1===t)continue;if(n===e)continue;const s="string"==typeof a?a:null,r=document.querySelector(`[data-element-id="${n}"]`);!r||Et.has(r)?i.push({id:n,exportValue:s,domElement:r}):warn(`_getElementsByName - element not allowed: ${n}`)}return i}for(const s of document.getElementsByName(t)){const{exportValue:t}=s,n=s.getAttribute("data-element-id");n!==e&&(Et.has(s)&&i.push({id:n,exportValue:t,domElement:s}))}return i}show(){this.container&&(this.container.hidden=!1);this.popup?.maybeShow()}hide(){this.container&&(this.container.hidden=!0);this.popup?.forceHide()}getElementsToTriggerPopup(){return this.container}addHighlightArea(){const t=this.getElementsToTriggerPopup();if(Array.isArray(t))for(const e of t)e.classList.add("highlightArea");else t.classList.add("highlightArea")}_editOnDoubleClick(){if(!this._isEditable)return;const{annotationEditorType:t,data:{id:e}}=this;this.container.addEventListener("dblclick",(()=>{this.linkService.eventBus?.dispatch("switchannotationeditormode",{source:this,mode:t,editId:e})}))}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class LinkAnnotationElement extends AnnotationElement{constructor(t,e=null){super(t,{isRenderable:!0,ignoreBorder:!!e?.ignoreBorder,createQuadrilaterals:!0});this.isTooltipOnly=t.data.isTooltipOnly}render(){const{data:t,linkService:e}=this,i=document.createElement("a");i.setAttribute("data-element-id",t.id);let s=!1;if(t.url){e.addLinkAttributes(i,t.url,t.newWindow);s=!0}else if(t.action){this._bindNamedAction(i,t.action);s=!0}else if(t.attachment){this.#tn(i,t.attachment,t.attachmentDest);s=!0}else if(t.setOCGState){this.#en(i,t.setOCGState);s=!0}else if(t.dest){this._bindLink(i,t.dest);s=!0}else{if(t.actions&&(t.actions.Action||t.actions["Mouse Up"]||t.actions["Mouse Down"])&&this.enableScripting&&this.hasJSActions){this._bindJSAction(i,t);s=!0}if(t.resetForm){this._bindResetFormAction(i,t.resetForm);s=!0}else if(this.isTooltipOnly&&!s){this._bindLink(i,"");s=!0}}this.container.classList.add("linkAnnotation");s&&this.container.append(i);return this.container}#in(){this.container.setAttribute("data-internal-link","")}_bindLink(t,e){t.href=this.linkService.getDestinationHash(e);t.onclick=()=>{e&&this.linkService.goToDestination(e);return!1};(e||""===e)&&this.#in()}_bindNamedAction(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeNamedAction(e);return!1};this.#in()}#tn(t,e,i=null){t.href=this.linkService.getAnchorUrl("");e.description&&(t.title=e.description);t.onclick=()=>{this.downloadManager?.openOrDownloadData(e.content,e.filename,i);return!1};this.#in()}#en(t,e){t.href=this.linkService.getAnchorUrl("");t.onclick=()=>{this.linkService.executeSetOCGState(e);return!1};this.#in()}_bindJSAction(t,e){t.href=this.linkService.getAnchorUrl("");const i=new Map([["Action","onclick"],["Mouse Up","onmouseup"],["Mouse Down","onmousedown"]]);for(const s of Object.keys(e.actions)){const n=i.get(s);n&&(t[n]=()=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e.id,name:s}});return!1})}t.onclick||(t.onclick=()=>!1);this.#in()}_bindResetFormAction(t,e){const i=t.onclick;i||(t.href=this.linkService.getAnchorUrl(""));this.#in();if(this._fieldObjects)t.onclick=()=>{i?.();const{fields:t,refs:s,include:n}=e,a=[];if(0!==t.length||0!==s.length){const e=new Set(s);for(const i of t){const t=this._fieldObjects[i]||[];for(const{id:i}of t)e.add(i)}for(const t of Object.values(this._fieldObjects))for(const i of t)e.has(i.id)===n&&a.push(i)}else for(const t of Object.values(this._fieldObjects))a.push(...t);const r=this.annotationStorage,o=[];for(const t of a){const{id:e}=t;o.push(e);switch(t.type){case"text":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}case"checkbox":case"radiobutton":{const i=t.defaultValue===t.exportValues;r.setValue(e,{value:i});break}case"combobox":case"listbox":{const i=t.defaultValue||"";r.setValue(e,{value:i});break}default:continue}const i=document.querySelector(`[data-element-id="${e}"]`);i&&(Et.has(i)?i.dispatchEvent(new Event("resetform")):warn(`_bindResetFormAction - element not allowed: ${e}`))}this.enableScripting&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:"app",ids:o,name:"ResetForm"}});return!1};else{warn('_bindResetFormAction - "resetForm" action not supported, ensure that the `fieldObjects` parameter is provided.');i||(t.onclick=()=>!1)}}}class TextAnnotationElement extends AnnotationElement{constructor(t){super(t,{isRenderable:!0})}render(){this.container.classList.add("textAnnotation");const t=document.createElement("img");t.src=this.imageResourcesPath+"annotation-"+this.data.name.toLowerCase()+".svg";t.setAttribute("data-l10n-id","pdfjs-text-annotation-type");t.setAttribute("data-l10n-args",JSON.stringify({type:this.data.name}));!this.data.popupRef&&this.hasPopupData&&this._createPopup();this.container.append(t);return this.container}}class WidgetAnnotationElement extends AnnotationElement{render(){return this.container}showElementAndHideCanvas(t){if(this.data.hasOwnCanvas){"CANVAS"===t.previousSibling?.nodeName&&(t.previousSibling.hidden=!0);t.hidden=!1}}_getKeyModifier(t){return util_FeatureTest.platform.isMac?t.metaKey:t.ctrlKey}_setEventListener(t,e,i,s,n){i.includes("mouse")?t.addEventListener(i,(t=>{this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t),shift:t.shiftKey,modifier:this._getKeyModifier(t)}})})):t.addEventListener(i,(t=>{if("blur"===i){if(!e.focused||!t.relatedTarget)return;e.focused=!1}else if("focus"===i){if(e.focused)return;e.focused=!0}n&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:this.data.id,name:s,value:n(t)}})}))}_setEventListeners(t,e,i,s){for(const[n,a]of i)if("Action"===a||this.data.actions?.[a]){"Focus"!==a&&"Blur"!==a||(e||={focused:!1});this._setEventListener(t,e,n,a,s);"Focus"!==a||this.data.actions?.Blur?"Blur"!==a||this.data.actions?.Focus||this._setEventListener(t,e,"focus","Focus",null):this._setEventListener(t,e,"blur","Blur",null)}}_setBackgroundColor(t){const e=this.data.backgroundColor||null;t.style.backgroundColor=null===e?"transparent":Util.makeHexColor(e[0],e[1],e[2])}_setTextStyle(t){const e=["left","center","right"],{fontColor:s}=this.data.defaultAppearanceData,n=this.data.defaultAppearanceData.fontSize||9,a=t.style;let r;const roundToOneDecimal=t=>Math.round(10*t)/10;if(this.data.multiLine){const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2),e=t/(Math.round(t/(i*n))||1);r=Math.min(n,roundToOneDecimal(e/i))}else{const t=Math.abs(this.data.rect[3]-this.data.rect[1]-2);r=Math.min(n,roundToOneDecimal(t/i))}a.fontSize=`calc(${r}px * var(--total-scale-factor))`;a.color=Util.makeHexColor(s[0],s[1],s[2]);null!==this.data.textAlignment&&(a.textAlign=e[this.data.textAlignment])}_setRequired(t,e){e?t.setAttribute("required",!0):t.removeAttribute("required");t.setAttribute("aria-required",e)}}class TextWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms||t.data.hasOwnCanvas||!t.data.hasAppearance&&!!t.data.fieldValue})}setPropertyOnSiblings(t,e,i,s){const n=this.annotationStorage;for(const a of this._getElementsByName(t.name,t.id)){a.domElement&&(a.domElement[e]=i);n.setValue(a.id,{[s]:i})}}render(){const t=this.annotationStorage,e=this.data.id;this.container.classList.add("textWidgetAnnotation");let i=null;if(this.renderForms){const s=t.getValue(e,{value:this.data.fieldValue});let n=s.value||"";const a=t.getValue(e,{charLimit:this.data.maxLen}).charLimit;a&&n.length>a&&(n=n.slice(0,a));let r=s.formattedValue||this.data.textContent?.join("\n")||null;r&&this.data.comb&&(r=r.replaceAll(/\s+/g,""));const o={userValue:n,formattedValue:r,lastCommittedValue:null,commitKey:1,focused:!1};if(this.data.multiLine){i=document.createElement("textarea");i.textContent=r??n;this.data.doNotScroll&&(i.style.overflowY="hidden")}else{i=document.createElement("input");i.type=this.data.password?"password":"text";i.setAttribute("value",r??n);this.data.doNotScroll&&(i.style.overflowX="hidden")}this.data.hasOwnCanvas&&(i.hidden=!0);Et.add(i);i.setAttribute("data-element-id",e);i.disabled=this.data.readOnly;i.name=this.data.fieldName;i.tabIndex=xt;this._setRequired(i,this.data.required);a&&(i.maxLength=a);i.addEventListener("input",(s=>{t.setValue(e,{value:s.target.value});this.setPropertyOnSiblings(i,"value",s.target.value,"value");o.formattedValue=null}));i.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue??"";i.value=o.userValue=e;o.formattedValue=null}));let blurListener=t=>{const{formattedValue:e}=o;null!=e&&(t.target.value=e);t.target.scrollLeft=0};if(this.enableScripting&&this.hasJSActions){i.addEventListener("focus",(t=>{if(o.focused)return;const{target:e}=t;o.userValue&&(e.value=o.userValue);o.lastCommittedValue=e.value;o.commitKey=1;this.data.actions?.Focus||(o.focused=!0)}));i.addEventListener("updatefromsandbox",(i=>{this.showElementAndHideCanvas(i.target);const s={value(i){o.userValue=i.detail.value??"";t.setValue(e,{value:o.userValue.toString()});i.target.value=o.userValue},formattedValue(i){const{formattedValue:s}=i.detail;o.formattedValue=s;null!=s&&i.target!==document.activeElement&&(i.target.value=s);t.setValue(e,{formattedValue:s})},selRange(t){t.target.setSelectionRange(...t.detail.selRange)},charLimit:i=>{const{charLimit:s}=i.detail,{target:n}=i;if(0===s){n.removeAttribute("maxLength");return}n.setAttribute("maxLength",s);let a=o.userValue;if(a&&!(a.length<=s)){a=a.slice(0,s);n.value=o.userValue=a;t.setValue(e,{value:a});this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:a,willCommit:!0,commitKey:1,selStart:n.selectionStart,selEnd:n.selectionEnd}})}}};this._dispatchEventFromSandbox(s,i)}));i.addEventListener("keydown",(t=>{o.commitKey=1;let i=-1;"Escape"===t.key?i=0:"Enter"!==t.key||this.data.multiLine?"Tab"===t.key&&(o.commitKey=3):i=2;if(-1===i)return;const{value:s}=t.target;if(o.lastCommittedValue!==s){o.lastCommittedValue=s;o.userValue=s;this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:s,willCommit:!0,commitKey:i,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}})}}));const s=blurListener;blurListener=null;i.addEventListener("blur",(t=>{if(!o.focused||!t.relatedTarget)return;this.data.actions?.Blur||(o.focused=!1);const{value:i}=t.target;o.userValue=i;o.lastCommittedValue!==i&&this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:i,willCommit:!0,commitKey:o.commitKey,selStart:t.target.selectionStart,selEnd:t.target.selectionEnd}});s(t)}));this.data.actions?.Keystroke&&i.addEventListener("beforeinput",(t=>{o.lastCommittedValue=null;const{data:i,target:s}=t,{value:n,selectionStart:a,selectionEnd:r}=s;let l=a,h=r;switch(t.inputType){case"deleteWordBackward":{const t=n.substring(0,a).match(/\w*[^\w]*$/);t&&(l-=t[0].length);break}case"deleteWordForward":{const t=n.substring(a).match(/^[^\w]*\w*/);t&&(h+=t[0].length);break}case"deleteContentBackward":a===r&&(l-=1);break;case"deleteContentForward":a===r&&(h+=1)}t.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:n,change:i||"",willCommit:!1,selStart:l,selEnd:h}})}));this._setEventListeners(i,o,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.value))}blurListener&&i.addEventListener("blur",blurListener);if(this.data.comb){const t=(this.data.rect[2]-this.data.rect[0])/a;i.classList.add("comb");i.style.letterSpacing=`calc(${t}px * var(--total-scale-factor) - 1ch)`}}else{i=document.createElement("div");i.textContent=this.data.fieldValue;i.style.verticalAlign="middle";i.style.display="table-cell";this.data.hasOwnCanvas&&(i.hidden=!0)}this._setTextStyle(i);this._setBackgroundColor(i);this._setDefaultPropertiesFromJS(i);this.container.append(i);return this.container}}class SignatureWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:!!t.data.hasOwnCanvas})}}class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.exportValue===e.fieldValue}).value;if("string"==typeof s){s="Off"!==s;t.setValue(i,{value:s})}this.container.classList.add("buttonWidgetAnnotation","checkBox");const n=document.createElement("input");Et.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="checkbox";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.setAttribute("exportValue",e.exportValue);n.tabIndex=xt;n.addEventListener("change",(s=>{const{name:n,checked:a}=s.target;for(const s of this._getElementsByName(n,i)){const i=a&&s.exportValue===e.exportValue;s.domElement&&(s.domElement.checked=i);t.setValue(s.id,{value:i})}t.setValue(i,{value:a})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue||"Off";t.target.checked=i===e.exportValue}));if(this.enableScripting&&this.hasJSActions){n.addEventListener("updatefromsandbox",(e=>{const s={value(e){e.target.checked="Off"!==e.detail.value;t.setValue(i,{value:e.target.checked})}};this._dispatchEventFromSandbox(s,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("buttonWidgetAnnotation","radioButton");const t=this.annotationStorage,e=this.data,i=e.id;let s=t.getValue(i,{value:e.fieldValue===e.buttonValue}).value;if("string"==typeof s){s=s!==e.buttonValue;t.setValue(i,{value:s})}if(s)for(const s of this._getElementsByName(e.fieldName,i))t.setValue(s.id,{value:!1});const n=document.createElement("input");Et.add(n);n.setAttribute("data-element-id",i);n.disabled=e.readOnly;this._setRequired(n,this.data.required);n.type="radio";n.name=e.fieldName;s&&n.setAttribute("checked",!0);n.tabIndex=xt;n.addEventListener("change",(e=>{const{name:s,checked:n}=e.target;for(const e of this._getElementsByName(s,i))t.setValue(e.id,{value:!1});t.setValue(i,{value:n})}));n.addEventListener("resetform",(t=>{const i=e.defaultFieldValue;t.target.checked=null!=i&&i===e.buttonValue}));if(this.enableScripting&&this.hasJSActions){const s=e.buttonValue;n.addEventListener("updatefromsandbox",(e=>{const n={value:e=>{const n=s===e.detail.value;for(const s of this._getElementsByName(e.target.name)){const e=n&&s.id===i;s.domElement&&(s.domElement.checked=e);t.setValue(s.id,{value:e})}}};this._dispatchEventFromSandbox(n,e)}));this._setEventListeners(n,null,[["change","Validate"],["change","Action"],["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"]],(t=>t.target.checked))}this._setBackgroundColor(n);this._setDefaultPropertiesFromJS(n);this.container.append(n);return this.container}}class PushButtonWidgetAnnotationElement extends LinkAnnotationElement{constructor(t){super(t,{ignoreBorder:t.data.hasAppearance})}render(){const t=super.render();t.classList.add("buttonWidgetAnnotation","pushButton");const e=t.lastChild;if(this.enableScripting&&this.hasJSActions&&e){this._setDefaultPropertiesFromJS(e);e.addEventListener("updatefromsandbox",(t=>{this._dispatchEventFromSandbox({},t)}))}return t}}class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement{constructor(t){super(t,{isRenderable:t.renderForms})}render(){this.container.classList.add("choiceWidgetAnnotation");const t=this.annotationStorage,e=this.data.id,i=t.getValue(e,{value:this.data.fieldValue}),s=document.createElement("select");Et.add(s);s.setAttribute("data-element-id",e);s.disabled=this.data.readOnly;this._setRequired(s,this.data.required);s.name=this.data.fieldName;s.tabIndex=xt;let n=this.data.combo&&this.data.options.length>0;if(!this.data.combo){s.size=this.data.options.length;this.data.multiSelect&&(s.multiple=!0)}s.addEventListener("resetform",(t=>{const e=this.data.defaultFieldValue;for(const t of s.options)t.selected=t.value===e}));for(const t of this.data.options){const e=document.createElement("option");e.textContent=t.displayValue;e.value=t.exportValue;if(i.value.includes(t.exportValue)){e.setAttribute("selected",!0);n=!1}s.append(e)}let a=null;if(n){const t=document.createElement("option");t.value=" ";t.setAttribute("hidden",!0);t.setAttribute("selected",!0);s.prepend(t);a=()=>{t.remove();s.removeEventListener("input",a);a=null};s.addEventListener("input",a)}const getValue=t=>{const e=t?"value":"textContent",{options:i,multiple:n}=s;return n?Array.prototype.filter.call(i,(t=>t.selected)).map((t=>t[e])):-1===i.selectedIndex?null:i[i.selectedIndex][e]};let r=getValue(!1);const getItems=t=>{const e=t.target.options;return Array.prototype.map.call(e,(t=>({displayValue:t.textContent,exportValue:t.value})))};if(this.enableScripting&&this.hasJSActions){s.addEventListener("updatefromsandbox",(i=>{const n={value(i){a?.();const n=i.detail.value,o=new Set(Array.isArray(n)?n:[n]);for(const t of s.options)t.selected=o.has(t.value);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},multipleSelection(t){s.multiple=!0},remove(i){const n=s.options,a=i.detail.remove;n[a].selected=!1;s.remove(a);if(n.length>0){-1===Array.prototype.findIndex.call(n,(t=>t.selected))&&(n[0].selected=!0)}t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},clear(i){for(;0!==s.length;)s.remove(0);t.setValue(e,{value:null,items:[]});r=getValue(!1)},insert(i){const{index:n,displayValue:a,exportValue:o}=i.detail.insert,l=s.children[n],h=document.createElement("option");h.textContent=a;h.value=o;l?l.before(h):s.append(h);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},items(i){const{items:n}=i.detail;for(;0!==s.length;)s.remove(0);for(const t of n){const{displayValue:e,exportValue:i}=t,n=document.createElement("option");n.textContent=e;n.value=i;s.append(n)}s.options.length>0&&(s.options[0].selected=!0);t.setValue(e,{value:getValue(!0),items:getItems(i)});r=getValue(!1)},indices(i){const s=new Set(i.detail.indices);for(const t of i.target.options)t.selected=s.has(t.index);t.setValue(e,{value:getValue(!0)});r=getValue(!1)},editable(t){t.target.disabled=!t.detail.editable}};this._dispatchEventFromSandbox(n,i)}));s.addEventListener("input",(i=>{const s=getValue(!0),n=getValue(!1);t.setValue(e,{value:s});i.preventDefault();this.linkService.eventBus?.dispatch("dispatcheventinsandbox",{source:this,detail:{id:e,name:"Keystroke",value:r,change:n,changeEx:s,willCommit:!1,commitKey:1,keyDown:!1}})}));this._setEventListeners(s,null,[["focus","Focus"],["blur","Blur"],["mousedown","Mouse Down"],["mouseenter","Mouse Enter"],["mouseleave","Mouse Exit"],["mouseup","Mouse Up"],["input","Action"],["input","Validate"]],(t=>t.target.value))}else s.addEventListener("input",(function(i){t.setValue(e,{value:getValue(!0)})}));this.data.combo&&this._setTextStyle(s);this._setBackgroundColor(s);this._setDefaultPropertiesFromJS(s);this.container.append(s);return this.container}}class PopupAnnotationElement extends AnnotationElement{constructor(t){const{data:e,elements:i}=t;super(t,{isRenderable:AnnotationElement._hasPopupData(e)});this.elements=i;this.popup=null}render(){this.container.classList.add("popupAnnotation");const t=this.popup=new PopupElement({container:this.container,color:this.data.color,titleObj:this.data.titleObj,modificationDate:this.data.modificationDate,contentsObj:this.data.contentsObj,richText:this.data.richText,rect:this.data.rect,parentRect:this.data.parentRect||null,parent:this.parent,elements:this.elements,open:this.data.open}),e=[];for(const i of this.elements){i.popup=t;i.container.ariaHasPopup="dialog";e.push(i.data.id);i.addHighlightArea()}this.container.setAttribute("aria-controls",e.map((t=>`${U}${t}`)).join(","));return this.container}}class PopupElement{#sn=this.#nn.bind(this);#an=this.#rn.bind(this);#on=this.#ln.bind(this);#hn=this.#dn.bind(this);#cn=null;#mt=null;#un=null;#pn=null;#gn=null;#fn=null;#mn=null;#bn=!1;#An=null;#T=null;#wn=null;#yn=null;#vn=null;#Ys=null;#xn=!1;constructor({container:t,color:e,elements:i,titleObj:s,modificationDate:n,contentsObj:a,richText:r,parent:o,rect:l,parentRect:h,open:d}){this.#mt=t;this.#vn=s;this.#un=a;this.#yn=r;this.#fn=o;this.#cn=e;this.#wn=l;this.#mn=h;this.#gn=i;this.#pn=PDFDateString.toDateObject(n);this.trigger=i.flatMap((t=>t.getElementsToTriggerPopup()));for(const t of this.trigger){t.addEventListener("click",this.#hn);t.addEventListener("mouseenter",this.#on);t.addEventListener("mouseleave",this.#an);t.classList.add("popupTriggerArea")}for(const t of i)t.container?.addEventListener("keydown",this.#sn);this.#mt.hidden=!0;d&&this.#dn()}render(){if(this.#An)return;const t=this.#An=document.createElement("div");t.className="popup";if(this.#cn){const e=t.style.outlineColor=Util.makeHexColor(...this.#cn);t.style.backgroundColor=`color-mix(in srgb, ${e} 30%, white)`}const e=document.createElement("span");e.className="header";const i=document.createElement("h1");e.append(i);({dir:i.dir,str:i.textContent}=this.#vn);t.append(e);if(this.#pn){const t=document.createElement("span");t.classList.add("popupDate");t.setAttribute("data-l10n-id","pdfjs-annotation-date-time-string");t.setAttribute("data-l10n-args",JSON.stringify({dateObj:this.#pn.valueOf()}));e.append(t)}const s=this.#En;if(s){XfaLayer.render({xfaHtml:s,intent:"richText",div:t});t.lastChild.classList.add("richText","popupContent")}else{const e=this._formatContents(this.#un);t.append(e)}this.#mt.append(t)}get#En(){const t=this.#yn,e=this.#un;return!t?.str||e?.str&&e.str!==t.str?null:this.#yn.html||null}get#_n(){return this.#En?.attributes?.style?.fontSize||0}get#Sn(){return this.#En?.attributes?.style?.color||null}#Cn(t){const e=[],i={str:t,html:{name:"div",attributes:{dir:"auto"},children:[{name:"p",children:e}]}},s={style:{color:this.#Sn,fontSize:this.#_n?`calc(${this.#_n}px * var(--total-scale-factor))`:""}};for(const i of t.split("\n"))e.push({name:"span",value:i,attributes:s});return i}_formatContents({str:t,dir:e}){const i=document.createElement("p");i.classList.add("popupContent");i.dir=e;const s=t.split(/(?:\r\n?|\n)/);for(let t=0,e=s.length;t=0&&n.setAttribute("stroke-width",e||1);if(i)for(let t=0,e=this.#Rn.length;t{"Enter"===t.key&&(s?t.metaKey:t.ctrlKey)&&this.#On()}));!e.popupRef&&this.hasPopupData?this._createPopup():i.classList.add("popupTriggerArea");t.append(i);return t}getElementsToTriggerPopup(){return this.#Ln}addHighlightArea(){this.container.classList.add("highlightArea")}#On(){this.downloadManager?.openOrDownloadData(this.content,this.filename)}}class AnnotationLayer{#Nn=null;#Bn=null;#Hn=new Map;#Un=null;constructor({div:t,accessibilityManager:e,annotationCanvasMap:i,annotationEditorUIManager:s,page:n,viewport:a,structTreeLayer:r}){this.div=t;this.#Nn=e;this.#Bn=i;this.#Un=r||null;this.page=n;this.viewport=a;this.zIndex=0;this._annotationEditorUIManager=s}hasEditableAnnotations(){return this.#Hn.size>0}async#zn(t,e){const i=t.firstChild||t,s=i.id=`${U}${e}`,n=await(this.#Un?.getAriaAttributes(s));if(n)for(const[t,e]of n)i.setAttribute(t,e);this.div.append(t);this.#Nn?.moveElementInDOM(this.div,t,i,!1)}async render(t){const{annotations:e}=t,i=this.div;setLayerDimensions(i,this.viewport);const s=new Map,n={data:null,layer:i,linkService:t.linkService,downloadManager:t.downloadManager,imageResourcesPath:t.imageResourcesPath||"",renderForms:!1!==t.renderForms,svgFactory:new DOMSVGFactory,annotationStorage:t.annotationStorage||new AnnotationStorage,enableScripting:!0===t.enableScripting,hasJSActions:t.hasJSActions,fieldObjects:t.fieldObjects,parent:this,elements:null};for(const t of e){if(t.noHTML)continue;const e=t.annotationType===x.POPUP;if(e){const e=s.get(t.id);if(!e)continue;n.elements=e}else if(t.rect[2]===t.rect[0]||t.rect[3]===t.rect[1])continue;n.data=t;const i=AnnotationElementFactory.create(n);if(!i.isRenderable)continue;if(!e&&t.popupRef){const e=s.get(t.popupRef);e?e.push(i):s.set(t.popupRef,[i])}const a=i.render();t.hidden&&(a.style.visibility="hidden");await this.#zn(a,t.id);if(i._isEditable){this.#Hn.set(i.data.id,i);this._annotationEditorUIManager?.renderAnnotationElement(i)}}this.#Gn()}async addLinkAnnotations(t,e){const i={data:null,layer:this.div,linkService:e,svgFactory:new DOMSVGFactory,parent:this};for(const e of t){e.borderStyle||=AnnotationLayer._defaultBorderStyle;i.data=e;const t=AnnotationElementFactory.create(i);if(!t.isRenderable)continue;const s=t.render();await this.#zn(s,e.id)}}update({viewport:t}){const e=this.div;this.viewport=t;setLayerDimensions(e,{rotation:t.rotation});this.#Gn();e.hidden=!1}#Gn(){if(!this.#Bn)return;const t=this.div;for(const[e,i]of this.#Bn){const s=t.querySelector(`[data-annotation-id="${e}"]`);if(!s)continue;i.className="annotationContent";const{firstChild:n}=s;n?"CANVAS"===n.nodeName?n.replaceWith(i):n.classList.contains("annotationContent")?n.after(i):n.before(i):s.append(i);const a=this.#Hn.get(e);if(a)if(a._hasNoCanvas){this._annotationEditorUIManager?.setMissingCanvas(e,s.id,i);a._hasNoCanvas=!1}else a.canvas=i}this.#Bn.clear()}getEditableAnnotations(){return Array.from(this.#Hn.values())}getEditableAnnotation(t){return this.#Hn.get(t)}static get _defaultBorderStyle(){return shadow(this,"_defaultBorderStyle",Object.freeze({width:1,rawWidth:1,style:E,dashArray:[3],horizontalCornerRadius:0,verticalCornerRadius:0}))}}const _t=/\r\n?|\n/g;class FreeTextEditor extends AnnotationEditor{#cn;#$n="";#Wn=`${this.id}-editor`;#Vn=null;#_n;static _freeTextDefaultContent="";static _internalPadding=0;static _defaultColor=null;static _defaultFontSize=10;static get _keyboardManager(){const t=FreeTextEditor.prototype,arrowChecker=t=>t.isEmpty(),e=AnnotationEditorUIManager.TRANSLATE_SMALL,i=AnnotationEditorUIManager.TRANSLATE_BIG;return shadow(this,"_keyboardManager",new KeyboardManager([[["ctrl+s","mac+meta+s","ctrl+p","mac+meta+p"],t.commitOrRemove,{bubbles:!0}],[["ctrl+Enter","mac+meta+Enter","Escape","mac+Escape"],t.commitOrRemove],[["ArrowLeft","mac+ArrowLeft"],t._translateEmpty,{args:[-e,0],checker:arrowChecker}],[["ctrl+ArrowLeft","mac+shift+ArrowLeft"],t._translateEmpty,{args:[-i,0],checker:arrowChecker}],[["ArrowRight","mac+ArrowRight"],t._translateEmpty,{args:[e,0],checker:arrowChecker}],[["ctrl+ArrowRight","mac+shift+ArrowRight"],t._translateEmpty,{args:[i,0],checker:arrowChecker}],[["ArrowUp","mac+ArrowUp"],t._translateEmpty,{args:[0,-e],checker:arrowChecker}],[["ctrl+ArrowUp","mac+shift+ArrowUp"],t._translateEmpty,{args:[0,-i],checker:arrowChecker}],[["ArrowDown","mac+ArrowDown"],t._translateEmpty,{args:[0,e],checker:arrowChecker}],[["ctrl+ArrowDown","mac+shift+ArrowDown"],t._translateEmpty,{args:[0,i],checker:arrowChecker}]]))}static _type="freetext";static _editorType=u.FREETEXT;constructor(t){super({...t,name:"freeTextEditor"});this.#cn=t.color||FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor;this.#_n=t.fontSize||FreeTextEditor._defaultFontSize}static initialize(t,e){AnnotationEditor.initialize(t,e);const i=getComputedStyle(document.documentElement);this._internalPadding=parseFloat(i.getPropertyValue("--freetext-padding"))}static updateDefaultParams(t,e){switch(t){case p.FREETEXT_SIZE:FreeTextEditor._defaultFontSize=e;break;case p.FREETEXT_COLOR:FreeTextEditor._defaultColor=e}}updateParams(t,e){switch(t){case p.FREETEXT_SIZE:this.#jn(e);break;case p.FREETEXT_COLOR:this.#qn(e)}}static get defaultPropertiesToUpdate(){return[[p.FREETEXT_SIZE,FreeTextEditor._defaultFontSize],[p.FREETEXT_COLOR,FreeTextEditor._defaultColor||AnnotationEditor._defaultLineColor]]}get propertiesToUpdate(){return[[p.FREETEXT_SIZE,this.#_n],[p.FREETEXT_COLOR,this.#cn]]}#jn(t){const setFontsize=t=>{this.editorDiv.style.fontSize=`calc(${t}px * var(--total-scale-factor))`;this.translate(0,-(t-this.#_n)*this.parentScale);this.#_n=t;this.#Xn()},e=this.#_n;this.addCommands({cmd:setFontsize.bind(this,t),undo:setFontsize.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_SIZE,overwriteIfSameType:!0,keepUndo:!0})}#qn(t){const setColor=t=>{this.#cn=this.editorDiv.style.color=t},e=this.#cn;this.addCommands({cmd:setColor.bind(this,t),undo:setColor.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.FREETEXT_COLOR,overwriteIfSameType:!0,keepUndo:!0})}_translateEmpty(t,e){this._uiManager.translateSelectedEditors(t,e,!0)}getInitialTranslation(){const t=this.parentScale;return[-FreeTextEditor._internalPadding*t,-(FreeTextEditor._internalPadding+this.#_n)*t]}rebuild(){if(this.parent){super.rebuild();null!==this.div&&(this.isAttachedToDOM||this.parent.add(this))}}enableEditMode(){if(!super.enableEditMode())return!1;this.overlayDiv.classList.remove("enabled");this.editorDiv.contentEditable=!0;this._isDraggable=!1;this.div.removeAttribute("aria-activedescendant");this.#Vn=new AbortController;const t=this._uiManager.combinedSignal(this.#Vn);this.editorDiv.addEventListener("keydown",this.editorDivKeydown.bind(this),{signal:t});this.editorDiv.addEventListener("focus",this.editorDivFocus.bind(this),{signal:t});this.editorDiv.addEventListener("blur",this.editorDivBlur.bind(this),{signal:t});this.editorDiv.addEventListener("input",this.editorDivInput.bind(this),{signal:t});this.editorDiv.addEventListener("paste",this.editorDivPaste.bind(this),{signal:t});return!0}disableEditMode(){if(!super.disableEditMode())return!1;this.overlayDiv.classList.add("enabled");this.editorDiv.contentEditable=!1;this.div.setAttribute("aria-activedescendant",this.#Wn);this._isDraggable=!0;this.#Vn?.abort();this.#Vn=null;this.div.focus({preventScroll:!0});this.isEditing=!1;this.parent.div.classList.add("freetextEditing");return!0}focusin(t){if(this._focusEventsAllowed){super.focusin(t);t.target!==this.editorDiv&&this.editorDiv.focus()}}onceAdded(t){if(!this.width){this.enableEditMode();t&&this.editorDiv.focus();this._initialOptions?.isCentered&&this.center();this._initialOptions=null}}isEmpty(){return!this.editorDiv||""===this.editorDiv.innerText.trim()}remove(){this.isEditing=!1;if(this.parent){this.parent.setEditingState(!0);this.parent.div.classList.add("freetextEditing")}super.remove()}#Kn(){const t=[];this.editorDiv.normalize();let e=null;for(const i of this.editorDiv.childNodes)if(e?.nodeType!==Node.TEXT_NODE||"BR"!==i.nodeName){t.push(FreeTextEditor.#Yn(i));e=i}return t.join("\n")}#Xn(){const[t,e]=this.parentDimensions;let i;if(this.isAttachedToDOM)i=this.div.getBoundingClientRect();else{const{currentLayer:t,div:e}=this,s=e.style.display,n=e.classList.contains("hidden");e.classList.remove("hidden");e.style.display="hidden";t.div.append(this.div);i=e.getBoundingClientRect();e.remove();e.style.display=s;e.classList.toggle("hidden",n)}if(this.rotation%180==this.parentRotation%180){this.width=i.width/t;this.height=i.height/e}else{this.width=i.height/t;this.height=i.width/e}this.fixAndSetPosition()}commit(){if(!this.isInEditMode())return;super.commit();this.disableEditMode();const t=this.#$n,e=this.#$n=this.#Kn().trimEnd();if(t===e)return;const setText=t=>{this.#$n=t;if(t){this.#Qn();this._uiManager.rebuild(this);this.#Xn()}else this.remove()};this.addCommands({cmd:()=>{setText(e)},undo:()=>{setText(t)},mustExec:!1});this.#Xn()}shouldGetKeyboardEvents(){return this.isInEditMode()}enterInEditMode(){this.enableEditMode();this.editorDiv.focus()}keydown(t){if(t.target===this.div&&"Enter"===t.key){this.enterInEditMode();t.preventDefault()}}editorDivKeydown(t){FreeTextEditor._keyboardManager.exec(this,t)}editorDivFocus(t){this.isEditing=!0}editorDivBlur(t){this.isEditing=!1}editorDivInput(t){this.parent.div.classList.toggle("freetextEditing",this.isEmpty())}disableEditing(){this.editorDiv.setAttribute("role","comment");this.editorDiv.removeAttribute("aria-multiline")}enableEditing(){this.editorDiv.setAttribute("role","textbox");this.editorDiv.setAttribute("aria-multiline",!0)}get canChangeContent(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy||this.annotationElementId){t=this.x;e=this.y}super.render();this.editorDiv=document.createElement("div");this.editorDiv.className="internal";this.editorDiv.setAttribute("id",this.#Wn);this.editorDiv.setAttribute("data-l10n-id","pdfjs-free-text2");this.editorDiv.setAttribute("data-l10n-attrs","default-content");this.enableEditing();this.editorDiv.contentEditable=!0;const{style:i}=this.editorDiv;i.fontSize=`calc(${this.#_n}px * var(--total-scale-factor))`;i.color=this.#cn;this.div.append(this.editorDiv);this.overlayDiv=document.createElement("div");this.overlayDiv.classList.add("overlay","enabled");this.div.append(this.overlayDiv);if(this._isCopy||this.annotationElementId){const[i,s]=this.parentDimensions;if(this.annotationElementId){const{position:n}=this._initialData;let[a,r]=this.getInitialTranslation();[a,r]=this.pageTranslationToScreen(a,r);const[o,l]=this.pageDimensions,[h,d]=this.pageTranslation;let c,u;switch(this.rotation){case 0:c=t+(n[0]-h)/o;u=e+this.height-(n[1]-d)/l;break;case 90:c=t+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[r,-a];break;case 180:c=t-this.width+(n[0]-h)/o;u=e-(n[1]-d)/l;[a,r]=[-a,-r];break;case 270:c=t+(n[0]-h-this.height*l)/o;u=e+(n[1]-d-this.width*o)/l;[a,r]=[-r,a]}this.setAt(c*i,u*s,a,r)}else this._moveAfterPaste(t,e);this.#Qn();this._isDraggable=!0;this.editorDiv.contentEditable=!1}else{this._isDraggable=!1;this.editorDiv.contentEditable=!0}return this.div}static#Yn(t){return(t.nodeType===Node.TEXT_NODE?t.nodeValue:t.innerText).replaceAll(_t,"")}editorDivPaste(t){const e=t.clipboardData||window.clipboardData,{types:i}=e;if(1===i.length&&"text/plain"===i[0])return;t.preventDefault();const s=FreeTextEditor.#Jn(e.getData("text")||"").replaceAll(_t,"\n");if(!s)return;const n=window.getSelection();if(!n.rangeCount)return;this.editorDiv.normalize();n.deleteFromDocument();const a=n.getRangeAt(0);if(!s.includes("\n")){a.insertNode(document.createTextNode(s));this.editorDiv.normalize();n.collapseToStart();return}const{startContainer:r,startOffset:o}=a,l=[],h=[];if(r.nodeType===Node.TEXT_NODE){const t=r.parentElement;h.push(r.nodeValue.slice(o).replaceAll(_t,""));if(t!==this.editorDiv){let e=l;for(const i of this.editorDiv.childNodes)i!==t?e.push(FreeTextEditor.#Yn(i)):e=h}l.push(r.nodeValue.slice(0,o).replaceAll(_t,""))}else if(r===this.editorDiv){let t=l,e=0;for(const i of this.editorDiv.childNodes){e++===o&&(t=h);t.push(FreeTextEditor.#Yn(i))}}this.#$n=`${l.join("\n")}${s}${h.join("\n")}`;this.#Qn();const d=new Range;let c=Math.sumPrecise(l.map((t=>t.length)));for(const{firstChild:t}of this.editorDiv.childNodes)if(t.nodeType===Node.TEXT_NODE){const e=t.nodeValue.length;if(c<=e){d.setStart(t,c);d.setEnd(t,c);break}c-=e}n.removeAllRanges();n.addRange(d)}#Qn(){this.editorDiv.replaceChildren();if(this.#$n)for(const t of this.#$n.split("\n")){const e=document.createElement("div");e.append(t?document.createTextNode(t):document.createElement("br"));this.editorDiv.append(e)}}#Zn(){return this.#$n.replaceAll("ย "," ")}static#Jn(t){return t.replaceAll(" ","ย ")}get contentDiv(){return this.editorDiv}static async deserialize(t,e,i){let s=null;if(t instanceof FreeTextAnnotationElement){const{data:{defaultAppearanceData:{fontSize:e,fontColor:i},rect:n,rotation:a,id:r,popupRef:o},textContent:l,textPosition:h,parent:{page:{pageNumber:d}}}=t;if(!l||0===l.length)return null;s=t={annotationType:u.FREETEXT,color:Array.from(i),fontSize:e,value:l.join("\n"),position:h,pageIndex:d-1,rect:n.slice(0),rotation:a,id:r,deleted:!1,popupRef:o}}const n=await super.deserialize(t,e,i);n.#_n=t.fontSize;n.#cn=Util.makeHexColor(...t.color);n.#$n=FreeTextEditor.#Jn(t.value);n.annotationElementId=t.id||null;n._initialData=s;return n}serialize(t=!1){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const e=FreeTextEditor._internalPadding*this.parentScale,i=this.getRect(e,e),s=AnnotationEditor._colorManager.convert(this.isAttachedToDOM?getComputedStyle(this.editorDiv).color:this.#cn),n={annotationType:u.FREETEXT,color:s,fontSize:this.#_n,value:this.#Zn(),pageIndex:this.pageIndex,rect:i,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){n.isCopy=!0;return n}if(this.annotationElementId&&!this.#ta(n))return null;n.id=this.annotationElementId;return n}#ta(t){const{value:e,fontSize:i,color:s,pageIndex:n}=this._initialData;return this._hasBeenMoved||t.value!==e||t.fontSize!==i||t.color.some(((t,e)=>t!==s[e]))||t.pageIndex!==n}renderAnnotationElement(t){const e=super.renderAnnotationElement(t);if(this.deleted)return e;const{style:i}=e;i.fontSize=`calc(${this.#_n}px * var(--total-scale-factor))`;i.color=this.#cn;e.replaceChildren();for(const t of this.#$n.split("\n")){const i=document.createElement("div");i.append(t?document.createTextNode(t):document.createElement("br"));e.append(i)}const s=FreeTextEditor._internalPadding*this.parentScale;t.updateEdited({rect:this.getRect(s,s),popupContent:this.#$n});return e}resetAnnotationElement(t){super.resetAnnotationElement(t);t.resetEdited()}}class Outline{static PRECISION=1e-4;toSVGPath(){unreachable("Abstract method `toSVGPath` must be implemented.")}get box(){unreachable("Abstract getter `box` must be implemented.")}serialize(t,e){unreachable("Abstract method `serialize` must be implemented.")}static _rescale(t,e,i,s,n,a){a||=new Float32Array(t.length);for(let r=0,o=t.length;r=6;t-=6)isNaN(e[t])?i.push(`L${e[t+4]} ${e[t+5]}`):i.push(`C${e[t]} ${e[t+1]} ${e[t+2]} ${e[t+3]} ${e[t+4]} ${e[t+5]}`);this.#ya(i);return i.join(" ")}#Aa(){const[t,e,i,s]=this.#ea,[n,a,r,o]=this.#ba();return`M${(this.#ra[2]-t)/i} ${(this.#ra[3]-e)/s} L${(this.#ra[4]-t)/i} ${(this.#ra[5]-e)/s} L${n} ${a} L${r} ${o} L${(this.#ra[16]-t)/i} ${(this.#ra[17]-e)/s} L${(this.#ra[14]-t)/i} ${(this.#ra[15]-e)/s} Z`}#ya(t){const e=this.#ia;t.push(`L${e[4]} ${e[5]} Z`)}#wa(t){const[e,i,s,n]=this.#ea,a=this.#ra.subarray(4,6),r=this.#ra.subarray(16,18),[o,l,h,d]=this.#ba();t.push(`L${(a[0]-e)/s} ${(a[1]-i)/n} L${o} ${l} L${h} ${d} L${(r[0]-e)/s} ${(r[1]-i)/n}`)}newFreeDrawOutline(t,e,i,s,n,a){return new FreeDrawOutline(t,e,i,s,n,a)}getOutlines(){const t=this.#aa,e=this.#ia,i=this.#ra,[s,n,a,r]=this.#ea,o=new Float32Array((this.#pa?.length??0)+2);for(let t=0,e=o.length-2;t=6;t-=6)for(let i=0;i<6;i+=2)if(isNaN(e[t+i])){l[h]=l[h+1]=NaN;h+=2}else{l[h]=e[t+i];l[h+1]=e[t+i+1];h+=2}this.#Ea(l,h);return this.newFreeDrawOutline(l,o,this.#ea,this.#ca,this.#sa,this.#na)}#va(t){const e=this.#ra,[i,s,n,a]=this.#ea,[r,o,l,h]=this.#ba(),d=new Float32Array(36);d.set([NaN,NaN,NaN,NaN,(e[2]-i)/n,(e[3]-s)/a,NaN,NaN,NaN,NaN,(e[4]-i)/n,(e[5]-s)/a,NaN,NaN,NaN,NaN,r,o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,(e[16]-i)/n,(e[17]-s)/a,NaN,NaN,NaN,NaN,(e[14]-i)/n,(e[15]-s)/a],0);return this.newFreeDrawOutline(d,t,this.#ea,this.#ca,this.#sa,this.#na)}#Ea(t,e){const i=this.#ia;t.set([NaN,NaN,NaN,NaN,i[4],i[5]],e);return e+6}#xa(t,e){const i=this.#ra.subarray(4,6),s=this.#ra.subarray(16,18),[n,a,r,o]=this.#ea,[l,h,d,c]=this.#ba();t.set([NaN,NaN,NaN,NaN,(i[0]-n)/r,(i[1]-a)/o,NaN,NaN,NaN,NaN,l,h,NaN,NaN,NaN,NaN,d,c,NaN,NaN,NaN,NaN,(s[0]-n)/r,(s[1]-a)/o],e);return e+24}}class FreeDrawOutline extends Outline{#ea;#_a=new Float32Array(4);#sa;#na;#pa;#ca;#Sa;constructor(t,e,i,s,n,a){super();this.#Sa=t;this.#pa=e;this.#ea=i;this.#ca=s;this.#sa=n;this.#na=a;this.lastPoint=[NaN,NaN];this.#Ca(a);const[r,o,l,h]=this.#_a;for(let e=0,i=t.length;et[0]-e[0]||t[1]-e[1]||t[2]-e[2]));const t=[];for(const e of this.#Ma)if(e[3]){t.push(...this.#Pa(e));this.#ka(e)}else{this.#Ia(e);t.push(...this.#Pa(e))}return this.#Ra(t)}#Ra(t){const e=[],i=new Set;for(const i of t){const[t,s,n]=i;e.push([t,s,i],[t,n,i])}e.sort(((t,e)=>t[1]-e[1]||t[0]-e[0]));for(let t=0,s=e.length;t0;){const t=i.values().next().value;let[e,a,r,o,l]=t;i.delete(t);let h=e,d=a;n=[e,r];s.push(n);for(;;){let t;if(i.has(o))t=o;else{if(!i.has(l))break;t=l}i.delete(t);[e,a,r,o,l]=t;if(h!==e){n.push(h,d,e,d===a?a:r);h=e}d=d===a?r:a}n.push(h,d)}return new HighlightOutline(s,this.#ea,this.#Ta)}#Fa(t){const e=this.#Da;let i=0,s=e.length-1;for(;i<=s;){const n=i+s>>1,a=e[n][0];if(a===t)return n;a=0;s--){const[i,n]=this.#Da[s];if(i!==t)break;if(i===t&&n===e){this.#Da.splice(s,1);return}}}#Pa(t){const[e,i,s]=t,n=[[e,i,s]],a=this.#Fa(s);for(let t=0;t=i)if(o>s)n[t][1]=s;else{if(1===a)return[];n.splice(t,1);t--;a--}else{n[t][2]=i;o>s&&n.push([e,s,o])}}}return n}}class HighlightOutline extends Outline{#ea;#La;constructor(t,e,i){super();this.#La=t;this.#ea=e;this.lastPoint=i}toSVGPath(){const t=[];for(const e of this.#La){let[i,s]=e;t.push(`M${i} ${s}`);for(let n=2;n-1){this.#ar=!0;this.#dr(t);this.#cr()}else if(this.#Ja){this.#Ya=t.anchorNode;this.#Qa=t.anchorOffset;this.#er=t.focusNode;this.#ir=t.focusOffset;this.#ur();this.#cr();this.rotate(this.rotation)}}get telemetryInitialData(){return{action:"added",type:this.#ar?"free_highlight":"highlight",color:this._uiManager.highlightColorNames.get(this.color),thickness:this.#ua,methodOfCreation:this.#hr}}get telemetryFinalData(){return{type:"highlight",color:this._uiManager.highlightColorNames.get(this.color)}}static computeTelemetryFinalData(t){return{numberOfColors:t.get("color").size}}#ur(){const t=new HighlightOutliner(this.#Ja,.001);this.#nr=t.getOutlines();[this.x,this.y,this.width,this.height]=this.#nr.box;const e=new HighlightOutliner(this.#Ja,.0025,.001,"ltr"===this._uiManager.direction);this.#tr=e.getOutlines();const{lastPoint:i}=this.#tr;this.#Ta=[(i[0]-this.x)/this.width,(i[1]-this.y)/this.height]}#dr({highlightOutlines:t,highlightId:e,clipPathId:i}){this.#nr=t;this.#tr=t.getNewOutline(this.#ua/2+1.5,.0025);if(e>=0){this.#y=e;this.#Za=i;this.parent.drawLayer.finalizeDraw(e,{bbox:t.box,path:{d:t.toSVGPath()}});this.#or=this.parent.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:!0},bbox:this.#tr.box,path:{d:this.#tr.toSVGPath()}},!0)}else if(this.parent){const e=this.parent.viewport.rotation;this.parent.drawLayer.updateProperties(this.#y,{bbox:HighlightEditor.#pr(this.#nr.box,(e-this.rotation+360)%360),path:{d:t.toSVGPath()}});this.parent.drawLayer.updateProperties(this.#or,{bbox:HighlightEditor.#pr(this.#tr.box,e),path:{d:this.#tr.toSVGPath()}})}const[s,n,a,r]=t.box;switch(this.rotation){case 0:this.x=s;this.y=n;this.width=a;this.height=r;break;case 90:{const[t,e]=this.parentDimensions;this.x=n;this.y=1-s;this.width=a*e/t;this.height=r*t/e;break}case 180:this.x=1-s;this.y=1-n;this.width=a;this.height=r;break;case 270:{const[t,e]=this.parentDimensions;this.x=1-n;this.y=s;this.width=a*e/t;this.height=r*t/e;break}}const{lastPoint:o}=this.#tr;this.#Ta=[(o[0]-s)/a,(o[1]-n)/r]}static initialize(t,e){AnnotationEditor.initialize(t,e);HighlightEditor._defaultColor||=e.highlightColors?.values().next().value||"#fff066"}static updateDefaultParams(t,e){switch(t){case p.HIGHLIGHT_DEFAULT_COLOR:HighlightEditor._defaultColor=e;break;case p.HIGHLIGHT_THICKNESS:HighlightEditor._defaultThickness=e}}translateInPage(t,e){}get toolbarPosition(){return this.#Ta}updateParams(t,e){switch(t){case p.HIGHLIGHT_COLOR:this.#qn(e);break;case p.HIGHLIGHT_THICKNESS:this.#gr(e)}}static get defaultPropertiesToUpdate(){return[[p.HIGHLIGHT_DEFAULT_COLOR,HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,HighlightEditor._defaultThickness]]}get propertiesToUpdate(){return[[p.HIGHLIGHT_COLOR,this.color||HighlightEditor._defaultColor],[p.HIGHLIGHT_THICKNESS,this.#ua||HighlightEditor._defaultThickness],[p.HIGHLIGHT_FREE,this.#ar]]}#qn(t){const setColorAndOpacity=(t,e)=>{this.color=t;this.#rr=e;this.parent?.drawLayer.updateProperties(this.#y,{root:{fill:t,"fill-opacity":e}});this.#n?.updateColor(t)},e=this.color,i=this.#rr;this.addCommands({cmd:setColorAndOpacity.bind(this,t,HighlightEditor._defaultOpacity),undo:setColorAndOpacity.bind(this,e,i),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.HIGHLIGHT_COLOR,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"color_changed",color:this._uiManager.highlightColorNames.get(t)},!0)}#gr(t){const e=this.#ua,setThickness=t=>{this.#ua=t;this.#fr(t)};this.addCommands({cmd:setThickness.bind(this,t),undo:setThickness.bind(this,e),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:p.INK_THICKNESS,overwriteIfSameType:!0,keepUndo:!0});this._reportTelemetry({action:"thickness_changed",thickness:t},!0)}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.highlightColors){this.#n=new ColorPicker({editor:this});t.addColorPicker(this.#n)}return t}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}fixAndSetPosition(){return super.fixAndSetPosition(this.#mr())}getBaseTranslation(){return[0,0]}getRect(t,e){return super.getRect(t,e,this.#mr())}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);t&&this.div.focus()}remove(){this.#br();this._reportTelemetry({action:"deleted"});super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#cr();this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t)this.#br();else if(t){this.#cr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);this.show(this._isVisible);e&&this.select()}#fr(t){if(!this.#ar)return;this.#dr({highlightOutlines:this.#nr.getNewOutline(t/2)});this.fixAndSetPosition();const[e,i]=this.parentDimensions;this.setDims(this.width*e,this.height*i)}#br(){if(null!==this.#y&&this.parent){this.parent.drawLayer.remove(this.#y);this.#y=null;this.parent.drawLayer.remove(this.#or);this.#or=null}}#cr(t=this.parent){if(null===this.#y){({id:this.#y,clipPathId:this.#Za}=t.drawLayer.draw({bbox:this.#nr.box,root:{viewBox:"0 0 1 1",fill:this.color,"fill-opacity":this.#rr},rootClass:{highlight:!0,free:this.#ar},path:{d:this.#nr.toSVGPath()}},!1,!0));this.#or=t.drawLayer.drawOutline({rootClass:{highlightOutline:!0,free:this.#ar},bbox:this.#tr.box,path:{d:this.#tr.toSVGPath()}},this.#ar);this.#sr&&(this.#sr.style.clipPath=this.#Za)}}static#pr([t,e,i,s],n){switch(n){case 90:return[1-e-s,t,s,i];case 180:return[1-t-i,1-e-s,i,s];case 270:return[e,1-t-i,s,i]}return[t,e,i,s]}rotate(t){const{drawLayer:e}=this.parent;let i;if(this.#ar){t=(t-this.rotation+360)%360;i=HighlightEditor.#pr(this.#nr.box,t)}else i=HighlightEditor.#pr([this.x,this.y,this.width,this.height],t);e.updateProperties(this.#y,{bbox:i,root:{"data-main-rotation":t}});e.updateProperties(this.#or,{bbox:HighlightEditor.#pr(this.#tr.box,t),root:{"data-main-rotation":t}})}render(){if(this.div)return this.div;const t=super.render();if(this.#lr){t.setAttribute("aria-label",this.#lr);t.setAttribute("role","mark")}this.#ar?t.classList.add("free"):this.div.addEventListener("keydown",this.#Ar.bind(this),{signal:this._uiManager._signal});const e=this.#sr=document.createElement("div");t.append(e);e.setAttribute("aria-hidden","true");e.className="internal";e.style.clipPath=this.#Za;const[i,s]=this.parentDimensions;this.setDims(this.width*i,this.height*s);bindEvents(this,this.#sr,["pointerover","pointerleave"]);this.enableEditing();return t}pointerover(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!0}})}pointerleave(){this.isSelected||this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!1}})}#Ar(t){HighlightEditor._keyboardManager.exec(this,t)}_moveCaret(t){this.parent.unselect(this);switch(t){case 0:case 2:this.#wr(!0);break;case 1:case 3:this.#wr(!1)}}#wr(t){if(!this.#Ya)return;const e=window.getSelection();t?e.setPosition(this.#Ya,this.#Qa):e.setPosition(this.#er,this.#ir)}select(){super.select();this.#or&&this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{hovered:!1,selected:!0}})}unselect(){super.unselect();if(this.#or){this.parent?.drawLayer.updateProperties(this.#or,{rootClass:{selected:!1}});this.#ar||this.#wr(!1)}}get _mustFixPosition(){return!this.#ar}show(t=this._isVisible){super.show(t);if(this.parent){this.parent.drawLayer.updateProperties(this.#y,{rootClass:{hidden:!t}});this.parent.drawLayer.updateProperties(this.#or,{rootClass:{hidden:!t}})}}#mr(){return this.#ar?this.rotation:0}#yr(){if(this.#ar)return null;const[t,e]=this.pageDimensions,[i,s]=this.pageTranslation,n=this.#Ja,a=new Float32Array(8*n.length);let r=0;for(const{x:o,y:l,width:h,height:d}of n){const n=o*t+i,c=(1-l)*e+s;a[r]=a[r+4]=n;a[r+1]=a[r+3]=c;a[r+2]=a[r+6]=n+h*t;a[r+5]=a[r+7]=c-d*e;r+=8}return a}#vr(t){return this.#nr.serialize(t,this.#mr())}static startHighlighting(t,e,{target:i,x:s,y:n}){const{x:a,y:r,width:o,height:l}=i.getBoundingClientRect(),h=new AbortController,d=t.combinedSignal(h),pointerUpCallback=e=>{h.abort();this.#xr(t,e)};window.addEventListener("blur",pointerUpCallback,{signal:d});window.addEventListener("pointerup",pointerUpCallback,{signal:d});window.addEventListener("pointerdown",stopEvent,{capture:!0,passive:!1,signal:d});window.addEventListener("contextmenu",noContextMenu,{signal:d});i.addEventListener("pointermove",this.#Er.bind(this,t),{signal:d});this._freeHighlight=new FreeHighlightOutliner({x:s,y:n},[a,r,o,l],t.scale,this._defaultThickness/2,e,.001);({id:this._freeHighlightId,clipPathId:this._freeHighlightClipId}=t.drawLayer.draw({bbox:[0,0,1,1],root:{viewBox:"0 0 1 1",fill:this._defaultColor,"fill-opacity":this._defaultOpacity},rootClass:{highlight:!0,free:!0},path:{d:this._freeHighlight.toSVGPath()}},!0,!0))}static#Er(t,e){this._freeHighlight.add(e)&&t.drawLayer.updateProperties(this._freeHighlightId,{path:{d:this._freeHighlight.toSVGPath()}})}static#xr(t,e){this._freeHighlight.isEmpty()?t.drawLayer.remove(this._freeHighlightId):t.createAndAddNewEditor(e,!1,{highlightId:this._freeHighlightId,highlightOutlines:this._freeHighlight.getOutlines(),clipPathId:this._freeHighlightClipId,methodOfCreation:"main_toolbar"});this._freeHighlightId=-1;this._freeHighlight=null;this._freeHighlightClipId=""}static async deserialize(t,e,i){let s=null;if(t instanceof HighlightAnnotationElement){const{data:{quadPoints:e,rect:i,rotation:n,id:a,color:r,opacity:o,popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),opacity:o,quadPoints:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}else if(t instanceof InkAnnotationElement){const{data:{inkLists:e,rect:i,rotation:n,id:a,color:r,borderStyle:{rawWidth:o},popupRef:l},parent:{page:{pageNumber:h}}}=t;s=t={annotationType:u.HIGHLIGHT,color:Array.from(r),thickness:o,inkLists:e,boxes:null,pageIndex:h-1,rect:i.slice(0),rotation:n,id:a,deleted:!1,popupRef:l}}const{color:n,quadPoints:a,inkLists:r,opacity:o}=t,l=await super.deserialize(t,e,i);l.color=Util.makeHexColor(...n);l.#rr=o||1;r&&(l.#ua=t.thickness);l.annotationElementId=t.id||null;l._initialData=s;const[h,d]=l.pageDimensions,[c,p]=l.pageTranslation;if(a){const t=l.#Ja=[];for(let e=0;et!==e[i]))}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class DrawingOptions{#_r=Object.create(null);updateProperty(t,e){this[t]=e;this.updateSVGProperty(t,e)}updateProperties(t){if(t)for(const[e,i]of Object.entries(t))e.startsWith("_")||this.updateProperty(e,i)}updateSVGProperty(t,e){this.#_r[t]=e}toSVGProperties(){const t=this.#_r;this.#_r=Object.create(null);return{root:t}}reset(){this.#_r=Object.create(null)}updateAll(t=this){this.updateProperties(t)}clone(){unreachable("Not implemented")}}class DrawingEditor extends AnnotationEditor{#Sr=null;#Cr;_drawId=null;static _currentDrawId=-1;static _currentParent=null;static#Tr=null;static#Mr=null;static#Dr=null;static#Pr=NaN;static#kr=null;static#Ir=null;static#Rr=NaN;static _INNER_MARGIN=3;constructor(t){super(t);this.#Cr=t.mustBeCommitted||!1;this._addOutlines(t)}_addOutlines(t){if(t.drawOutlines){this.#Fr(t);this.#cr()}}#Fr({drawOutlines:t,drawId:e,drawingOptions:i}){this.#Sr=t;this._drawingOptions||=i;if(e>=0){this._drawId=e;this.parent.drawLayer.finalizeDraw(e,t.defaultProperties)}else this._drawId=this.#Lr(t,this.parent);this.#Or(t.box)}#Lr(t,e){const{id:i}=e.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(),t.defaultSVGProperties),!1,!1);return i}static _mergeSVGProperties(t,e){const i=new Set(Object.keys(t));for(const[s,n]of Object.entries(e))i.has(s)?Object.assign(t[s],n):t[s]=n;return t}static getDefaultDrawingOptions(t){unreachable("Not implemented")}static get typesMap(){unreachable("Not implemented")}static get isDrawer(){return!0}static get supportMultipleDrawings(){return!1}static updateDefaultParams(t,e){const i=this.typesMap.get(t);i&&this._defaultDrawingOptions.updateProperty(i,e);if(this._currentParent){DrawingEditor.#Tr.updateProperty(i,e);this._currentParent.drawLayer.updateProperties(this._currentDrawId,this._defaultDrawingOptions.toSVGProperties())}}updateParams(t,e){const i=this.constructor.typesMap.get(t);i&&this._updateProperty(t,i,e)}static get defaultPropertiesToUpdate(){const t=[],e=this._defaultDrawingOptions;for(const[i,s]of this.typesMap)t.push([i,e[s]]);return t}get propertiesToUpdate(){const t=[],{_drawingOptions:e}=this;for(const[i,s]of this.constructor.typesMap)t.push([i,e[s]]);return t}_updateProperty(t,e,i){const s=this._drawingOptions,n=s[e],setter=t=>{s.updateProperty(e,t);const i=this.#Sr.updateProperty(e,t);i&&this.#Or(i);this.parent?.drawLayer.updateProperties(this._drawId,s.toSVGProperties())};this.addCommands({cmd:setter.bind(this,i),undo:setter.bind(this,n),post:this._uiManager.updateUI.bind(this._uiManager,this),mustExec:!0,type:t,overwriteIfSameType:!0,keepUndo:!0})}_onResizing(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathResizingSVGProperties(this.#Nr()),{bbox:this.#Br()}))}_onResized(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathResizedSVGProperties(this.#Nr()),{bbox:this.#Br()}))}_onTranslating(t,e){this.parent?.drawLayer.updateProperties(this._drawId,{bbox:this.#Br()})}_onTranslated(){this.parent?.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties(this.#Sr.getPathTranslatedSVGProperties(this.#Nr(),this.parentDimensions),{bbox:this.#Br()}))}_onStartDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!0}})}_onStopDragging(){this.parent?.drawLayer.updateProperties(this._drawId,{rootClass:{moving:!1}})}commit(){super.commit();this.disableEditMode();this.disableEditing()}disableEditing(){super.disableEditing();this.div.classList.toggle("disabled",!0)}enableEditing(){super.enableEditing();this.div.classList.toggle("disabled",!1)}getBaseTranslation(){return[0,0]}get isResizable(){return!0}onceAdded(t){this.annotationElementId||this.parent.addUndoableEditor(this);this._isDraggable=!0;if(this.#Cr){this.#Cr=!1;this.commit();this.parent.setSelected(this);t&&this.isOnScreen&&this.div.focus()}}remove(){this.#br();super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#cr();this.#Or(this.#Sr.box);this.isAttachedToDOM||this.parent.add(this)}}}setParent(t){let e=!1;if(this.parent&&!t){this._uiManager.removeShouldRescale(this);this.#br()}else if(t){this._uiManager.addShouldRescale(this);this.#cr(t);e=!this.parent&&this.div?.classList.contains("selectedEditor")}super.setParent(t);e&&this.select()}#br(){if(null!==this._drawId&&this.parent){this.parent.drawLayer.remove(this._drawId);this._drawId=null;this._drawingOptions.reset()}}#cr(t=this.parent){if(null===this._drawId||this.parent!==t)if(null===this._drawId){this._drawingOptions.updateAll();this._drawId=this.#Lr(this.#Sr,t)}else this.parent.drawLayer.updateParent(this._drawId,t.drawLayer)}#Hr([t,e,i,s]){const{parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[e,1-t,i*(a/n),s*(n/a)];case 180:return[1-t,1-e,i,s];case 270:return[1-e,t,i*(a/n),s*(n/a)];default:return[t,e,i,s]}}#Nr(){const{x:t,y:e,width:i,height:s,parentDimensions:[n,a],rotation:r}=this;switch(r){case 90:return[1-e,t,i*(n/a),s*(a/n)];case 180:return[1-t,1-e,i,s];case 270:return[e,1-t,i*(n/a),s*(a/n)];default:return[t,e,i,s]}}#Or(t){[this.x,this.y,this.width,this.height]=this.#Hr(t);if(this.div){this.fixAndSetPosition();const[t,e]=this.parentDimensions;this.setDims(this.width*t,this.height*e)}this._onResized()}#Br(){const{x:t,y:e,width:i,height:s,rotation:n,parentRotation:a,parentDimensions:[r,o]}=this;switch((4*n+a)/90){case 1:return[1-e-s,t,s,i];case 2:return[1-t-i,1-e-s,i,s];case 3:return[e,1-t-i,s,i];case 4:return[t,e-i*(r/o),s*(o/r),i*(r/o)];case 5:return[1-e,t,i*(r/o),s*(o/r)];case 6:return[1-t-s*(o/r),1-e,s*(o/r),i*(r/o)];case 7:return[e-i*(r/o),1-t-s*(o/r),i*(r/o),s*(o/r)];case 8:return[t-i,e-s,i,s];case 9:return[1-e,t-i,s,i];case 10:return[1-t,1-e,i,s];case 11:return[e-s,1-t,s,i];case 12:return[t-s*(o/r),e,s*(o/r),i*(r/o)];case 13:return[1-e-i*(r/o),t-s*(o/r),i*(r/o),s*(o/r)];case 14:return[1-t,1-e-i*(r/o),s*(o/r),i*(r/o)];case 15:return[e,1-t,i*(r/o),s*(o/r)];default:return[t,e,i,s]}}rotate(){this.parent&&this.parent.drawLayer.updateProperties(this._drawId,DrawingEditor._mergeSVGProperties({bbox:this.#Br()},this.#Sr.updateRotation((this.parentRotation-this.rotation+360)%360)))}onScaleChanging(){this.parent&&this.#Or(this.#Sr.updateParentDimensions(this.parentDimensions,this.parent.scale))}static onScaleChangingWhenDrawing(){}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}const i=super.render();i.classList.add("draw");const s=document.createElement("div");i.append(s);s.setAttribute("aria-hidden","true");s.className="internal";const[n,a]=this.parentDimensions;this.setDims(this.width*n,this.height*a);this._uiManager.addShouldRescale(this);this.disableEditing();this._isCopy&&this._moveAfterPaste(t,e);return i}static createDrawerInstance(t,e,i,s,n){unreachable("Not implemented")}static startDrawing(t,e,i,s){const{target:n,offsetX:a,offsetY:r,pointerId:o,pointerType:l}=s;if(DrawingEditor.#kr&&DrawingEditor.#kr!==l)return;const{viewport:{rotation:h}}=t,{width:d,height:c}=n.getBoundingClientRect(),u=DrawingEditor.#Mr=new AbortController,p=t.combinedSignal(u);DrawingEditor.#Pr||=o;DrawingEditor.#kr??=l;window.addEventListener("pointerup",(t=>{DrawingEditor.#Pr===t.pointerId?this._endDraw(t):DrawingEditor.#Ir?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointercancel",(t=>{DrawingEditor.#Pr===t.pointerId?this._currentParent.endDrawingSession():DrawingEditor.#Ir?.delete(t.pointerId)}),{signal:p});window.addEventListener("pointerdown",(t=>{if(DrawingEditor.#kr===t.pointerType){(DrawingEditor.#Ir||=new Set).add(t.pointerId);if(DrawingEditor.#Tr.isCancellable()){DrawingEditor.#Tr.removeLastElement();DrawingEditor.#Tr.isEmpty()?this._currentParent.endDrawingSession(!0):this._endDraw(null)}}}),{capture:!0,passive:!1,signal:p});window.addEventListener("contextmenu",noContextMenu,{signal:p});n.addEventListener("pointermove",this._drawMove.bind(this),{signal:p});n.addEventListener("touchmove",(t=>{t.timeStamp===DrawingEditor.#Rr&&stopEvent(t)}),{signal:p});t.toggleDrawing();e._editorUndoBar?.hide();if(DrawingEditor.#Tr)t.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.startNew(a,r,d,c,h));else{e.updateUIForDefaultProperties(this);DrawingEditor.#Tr=this.createDrawerInstance(a,r,d,c,h);DrawingEditor.#Dr=this.getDefaultDrawingOptions();this._currentParent=t;({id:this._currentDrawId}=t.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#Dr.toSVGProperties(),DrawingEditor.#Tr.defaultSVGProperties),!0,!1))}}static _drawMove(t){DrawingEditor.#Rr=-1;if(!DrawingEditor.#Tr)return;const{offsetX:e,offsetY:i,pointerId:s}=t;if(DrawingEditor.#Pr===s)if(DrawingEditor.#Ir?.size>=1)this._endDraw(t);else{this._currentParent.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.add(e,i));DrawingEditor.#Rr=t.timeStamp;stopEvent(t)}}static _cleanup(t){if(t){this._currentDrawId=-1;this._currentParent=null;DrawingEditor.#Tr=null;DrawingEditor.#Dr=null;DrawingEditor.#kr=null;DrawingEditor.#Rr=NaN}if(DrawingEditor.#Mr){DrawingEditor.#Mr.abort();DrawingEditor.#Mr=null;DrawingEditor.#Pr=NaN;DrawingEditor.#Ir=null}}static _endDraw(t){const e=this._currentParent;if(e){e.toggleDrawing(!0);this._cleanup(!1);t?.target===e.div&&e.drawLayer.updateProperties(this._currentDrawId,DrawingEditor.#Tr.end(t.offsetX,t.offsetY));if(this.supportMultipleDrawings){const t=DrawingEditor.#Tr,i=this._currentDrawId,s=t.getLastElement();e.addCommands({cmd:()=>{e.drawLayer.updateProperties(i,t.setLastElement(s))},undo:()=>{e.drawLayer.updateProperties(i,t.removeLastElement())},mustExec:!1,type:p.DRAW_STEP})}else this.endDrawing(!1)}}static endDrawing(t){const e=this._currentParent;if(!e)return null;e.toggleDrawing(!0);e.cleanUndoStack(p.DRAW_STEP);if(!DrawingEditor.#Tr.isEmpty()){const{pageDimensions:[i,s],scale:n}=e,a=e.createAndAddNewEditor({offsetX:0,offsetY:0},!1,{drawId:this._currentDrawId,drawOutlines:DrawingEditor.#Tr.getOutlines(i*n,s*n,n,this._INNER_MARGIN),drawingOptions:DrawingEditor.#Dr,mustBeCommitted:!t});this._cleanup(!0);return a}e.drawLayer.remove(this._currentDrawId);this._cleanup(!0);return null}createDrawingOptions(t){}static deserializeDraw(t,e,i,s,n,a){unreachable("Not implemented")}static async deserialize(t,e,i){const{rawDims:{pageWidth:s,pageHeight:n,pageX:a,pageY:r}}=e.viewport,o=this.deserializeDraw(a,r,s,n,this._INNER_MARGIN,t),l=await super.deserialize(t,e,i);l.createDrawingOptions(t);l.#Fr({drawOutlines:o});l.#cr();l.onScaleChanging();l.rotate();return l}serializeDraw(t){const[e,i]=this.pageTranslation,[s,n]=this.pageDimensions;return this.#Sr.serialize([e,i,s,n],t)}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}static canCreateNewEmptyEditor(){return!1}}class InkDrawOutliner{#ra=new Float64Array(6);#Mn;#Ur;#as;#ua;#pa;#zr="";#Gr=0;#La=new InkDrawOutline;#$r;#Wr;constructor(t,e,i,s,n,a){this.#$r=i;this.#Wr=s;this.#as=n;this.#ua=a;[t,e]=this.#Vr(t,e);const r=this.#Mn=[NaN,NaN,NaN,NaN,t,e];this.#pa=[t,e];this.#Ur=[{line:r,points:this.#pa}];this.#ra.set(r,0)}updateProperty(t,e){"stroke-width"===t&&(this.#ua=e)}#Vr(t,e){return Outline._normalizePoint(t,e,this.#$r,this.#Wr,this.#as)}isEmpty(){return!this.#Ur||0===this.#Ur.length}isCancellable(){return this.#pa.length<=10}add(t,e){[t,e]=this.#Vr(t,e);const[i,s,n,a]=this.#ra.subarray(2,6),r=t-n,o=e-a;if(Math.hypot(this.#$r*r,this.#Wr*o)<=2)return null;this.#pa.push(t,e);if(isNaN(i)){this.#ra.set([n,a,t,e],2);this.#Mn.push(NaN,NaN,NaN,NaN,t,e);return{path:{d:this.toSVGPath()}}}isNaN(this.#ra[0])&&this.#Mn.splice(6,6);this.#ra.set([i,s,n,a,t,e],0);this.#Mn.push(...Outline.createBezierPoints(i,s,n,a,t,e));return{path:{d:this.toSVGPath()}}}end(t,e){const i=this.add(t,e);return i||(2===this.#pa.length?{path:{d:this.toSVGPath()}}:null)}startNew(t,e,i,s,n){this.#$r=i;this.#Wr=s;this.#as=n;[t,e]=this.#Vr(t,e);const a=this.#Mn=[NaN,NaN,NaN,NaN,t,e];this.#pa=[t,e];const r=this.#Ur.at(-1);if(r){r.line=new Float32Array(r.line);r.points=new Float32Array(r.points)}this.#Ur.push({line:a,points:this.#pa});this.#ra.set(a,0);this.#Gr=0;this.toSVGPath();return null}getLastElement(){return this.#Ur.at(-1)}setLastElement(t){if(!this.#Ur)return this.#La.setLastElement(t);this.#Ur.push(t);this.#Mn=t.line;this.#pa=t.points;this.#Gr=0;return{path:{d:this.toSVGPath()}}}removeLastElement(){if(!this.#Ur)return this.#La.removeLastElement();this.#Ur.pop();this.#zr="";for(let t=0,e=this.#Ur.length;tt??NaN)),d,c,u,p),points:g(r[t].map((t=>t??NaN)),d,c,u,p)});const f=new this.prototype.constructor;f.build(h,i,s,1,o,l,n);return f}#Yr(t=this.#ua){const e=this.#sa+t/2*this.#qr;return this.#as%180==0?[e/this.#$r,e/this.#Wr]:[e/this.#Wr,e/this.#$r]}#Kr(){const[t,e,i,s]=this.#_a,[n,a]=this.#Yr(0);return[t+n,e+a,i-2*n,s-2*a]}#Xr(){const t=this.#_a=new Float32Array([1/0,1/0,-1/0,-1/0]);for(const{line:e}of this.#Ur){if(e.length<=12){for(let i=4,s=e.length;it!==e[i]))||t.thickness!==i||t.opacity!==s||t.pageIndex!==n}renderAnnotationElement(t){const{points:e,rect:i}=this.serializeDraw(!1);t.updateEdited({rect:i,thickness:this._drawingOptions["stroke-width"],points:e});return null}}class ContourDrawOutline extends InkDrawOutline{toSVGPath(){let t=super.toSVGPath();t.endsWith("Z")||(t+="Z");return t}}class SignatureExtractor{static#Qr={maxDim:512,sigmaSFactor:.02,sigmaR:25,kernelSize:16};static#Jr(t,e,i,s){s-=e;return 0===(i-=t)?s>0?0:4:1===i?s+6:2-s}static#Zr=new Int32Array([0,1,-1,1,-1,0,-1,-1,0,-1,1,-1,1,0,1,1]);static#to(t,e,i,s,n,a,r){const o=this.#Jr(i,s,n,a);for(let n=0;n<8;n++){const a=(-n+o-r+16)%8;if(0!==t[(i+this.#Zr[2*a])*e+(s+this.#Zr[2*a+1])])return a}return-1}static#eo(t,e,i,s,n,a,r){const o=this.#Jr(i,s,n,a);for(let n=0;n<8;n++){const a=(n+o+r+16)%8;if(0!==t[(i+this.#Zr[2*a])*e+(s+this.#Zr[2*a+1])])return a}return-1}static#io(t,e,i,s){const n=t.length,a=new Int32Array(n);for(let e=0;e=1&&0===a[s+1])){1!==n&&(r=Math.abs(n));continue}o+=1;d+=1;n>1&&(r=n)}const c=[i,t],u=d===i+1,p={isHole:u,points:c,id:o,parent:0};l.push(p);let g;for(const t of l)if(t.id===r){g=t;break}g?g.isHole?p.parent=u?g.parent:r:p.parent=u?r:g.parent:p.parent=u?r:0;const f=this.#to(a,e,t,i,h,d,0);if(-1===f){a[s]=-o;1!==a[s]&&(r=Math.abs(a[s]));continue}let m=this.#Zr[2*f],b=this.#Zr[2*f+1];const A=t+m,w=i+b;h=A;d=w;let y=t,v=i;for(;;){const n=this.#eo(a,e,y,v,h,d,1);m=this.#Zr[2*n];b=this.#Zr[2*n+1];const l=y+m,u=v+b;c.push(u,l);const p=y*e+v;0===a[p+1]?a[p]=-o:1===a[p]&&(a[p]=o);if(l===t&&u===i&&y===A&&v===w){1!==a[s]&&(r=Math.abs(a[s]));break}h=y;d=v;y=l;v=u}}}return l}static#so(t,e,i,s){if(i-e<=4){for(let n=e;ny){v=s;y=e}}if(y>(l*w)**2){this.#so(t,e,v+2,s);this.#so(t,v,i,s)}else s.push(n,a)}static#no(t){const e=[],i=t.length;this.#so(t,0,i,e);e.push(t[i-2],t[i-1]);return e.length<=4?null:e}static#ao(t,e,i,s,n,a){const r=new Float32Array(a**2),o=-2*s**2,l=a>>1;for(let t=0;t=i))for(let i=0;i=e)continue;const p=t[u*e+s],f=r[o*a+i]*h[Math.abs(p-d)];c+=p*f;g+=f}}p[u[o]=Math.round(c/g)]++}return[u,p]}static#ro(t){const e=new Uint32Array(256);for(const i of t)e[i]++;return e}static#oo(t){const e=t.length,i=new Uint8ClampedArray(e>>2);let s=-1/0,n=1/0;for(let e=0,a=i.length;es&&(s=a);a0!==t));let a=n,r=n;for(e=n;e<256;e++){const n=t[e];if(n>i){if(e-a>s){s=e-a;r=e-1}i=n;a=e}}for(e=r-1;e>=0&&!(t[e]>t[e+1]);e--);return e}static#ho(t){const e=t,{width:i,height:s}=t,{maxDim:n}=this.#Qr;let a=i,r=s;if(i>n||s>n){let o=i,l=s,h=Math.log2(Math.max(i,s)/n);const d=Math.floor(h);h=h===d?d-1:d;for(let i=0;i=-128&&o<=127?Int8Array:r>=-32768&&o<=32767?Int16Array:Int32Array;const h=t.length,d=8+3*h,c=new Uint32Array(d);let u=0;c[u++]=d*Uint32Array.BYTES_PER_ELEMENT+(l-2*h)*a.BYTES_PER_ELEMENT;c[u++]=0;c[u++]=s;c[u++]=n;c[u++]=e?0:1;c[u++]=Math.max(0,Math.floor(i??0));c[u++]=h;c[u++]=a.BYTES_PER_ELEMENT;for(const e of t){c[u++]=e.length-2;c[u++]=e[0];c[u++]=e[1]}const p=new CompressionStream("deflate-raw"),g=p.writable.getWriter();await g.ready;g.write(c);const f=a.prototype.constructor;for(const e of t){const t=new f(e.length-2);for(let i=2,s=e.length;i{await n.ready;await n.close()})).catch((()=>{}));let a=null,r=0;for await(const t of i){a||=new Uint8Array(new Uint32Array(t.buffer,0,4)[0]);a.set(t,r);r+=t.length}const o=new Uint32Array(a.buffer,0,a.length>>2),l=o[1];if(0!==l)throw new Error(`Invalid version: ${l}`);const h=o[2],d=o[3],c=0===o[4],u=o[5],p=o[6],g=o[7],f=[],m=(8+3*p)*Uint32Array.BYTES_PER_ELEMENT;let b;switch(g){case Int8Array.BYTES_PER_ELEMENT:b=new Int8Array(a.buffer,m);break;case Int16Array.BYTES_PER_ELEMENT:b=new Int16Array(a.buffer,m);break;case Int32Array.BYTES_PER_ELEMENT:b=new Int32Array(a.buffer,m)}r=0;for(let t=0;t{e?.updateEditSignatureButton(t)}))}getSignaturePreview(){const{newCurves:t,areContours:e,thickness:i,width:s,height:n}=this.#uo,a=Math.max(s,n);return{areContours:e,outline:SignatureExtractor.processDrawnLines({lines:{curves:t.map((t=>({points:t}))),thickness:i,width:s,height:n},pageWidth:a,pageHeight:a,rotation:0,innerMargin:0,mustSmooth:!1,areContours:e}).outline}}async addEditToolbar(){const t=await super.addEditToolbar();if(!t)return null;if(this._uiManager.signatureManager&&null!==this.#co){await t.addEditSignatureButton(this._uiManager.signatureManager,this.#po,this.#co);t.show()}return t}addSignature(t,e,i,s){const{x:n,y:a}=this,{outline:r}=this.#uo=t;this.#do=r instanceof ContourDrawOutline;this.#co=i;this.div.setAttribute("data-l10n-args",JSON.stringify({description:i}));let o;if(this.#do)o=SignatureEditor.getDefaultDrawingOptions();else{o=SignatureEditor._defaultDrawnSignatureOptions.clone();o.updateProperties({"stroke-width":r.thickness})}this._addOutlines({drawOutlines:r,drawingOptions:o});const[l,h]=this.parentDimensions,[,d]=this.pageDimensions;let c=e/d;c=c>=1?.5:c;this.width*=c/this.height;if(this.width>=1){c*=.9/this.width;this.width=.9}this.height=c;this.setDims(l*this.width,h*this.height);this.x=n;this.y=a;this.center();this._onResized();this.onScaleChanging();this.rotate();this._uiManager.addToAnnotationStorage(this);this.setUuid(s);this._reportTelemetry({action:"pdfjs.signature.inserted",data:{hasBeenSaved:!!s,hasDescription:!!i}});this.div.hidden=!1}getFromImage(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.process(t,e,i,s,SignatureEditor._INNER_MARGIN)}getFromText(t,e){const{rawDims:{pageWidth:i,pageHeight:s},rotation:n}=this.parent.viewport;return SignatureExtractor.extractContoursFromText(t,e,i,s,n,SignatureEditor._INNER_MARGIN)}getDrawnSignature(t){const{rawDims:{pageWidth:e,pageHeight:i},rotation:s}=this.parent.viewport;return SignatureExtractor.processDrawnLines({lines:t,pageWidth:e,pageHeight:i,rotation:s,innerMargin:SignatureEditor._INNER_MARGIN,mustSmooth:!1,areContours:!1})}createDrawingOptions({areContours:t,thickness:e}){if(t)this._drawingOptions=SignatureEditor.getDefaultDrawingOptions();else{this._drawingOptions=SignatureEditor._defaultDrawnSignatureOptions.clone();this._drawingOptions.updateProperties({"stroke-width":e})}}serialize(t=!1){if(this.isEmpty())return null;const{lines:e,points:i,rect:s}=this.serializeDraw(t),{_drawingOptions:{"stroke-width":n}}=this,a={annotationType:u.SIGNATURE,isSignature:!0,areContours:this.#do,color:[0,0,0],thickness:this.#do?0:n,pageIndex:this.pageIndex,rect:s,rotation:this.rotation,structTreeParentId:this._structTreeParentId};if(t){a.paths={lines:e,points:i};a.uuid=this.#po;a.isCopy=!0}else a.lines=e;this.#co&&(a.accessibilityData={type:"Figure",alt:this.#co});return a}static deserializeDraw(t,e,i,s,n,a){return a.areContours?ContourDrawOutline.deserialize(t,e,i,s,n,a):InkDrawOutline.deserialize(t,e,i,s,n,a)}static async deserialize(t,e,i){const s=await super.deserialize(t,e,i);s.#do=t.areContours;s.#co=t.accessibilityData?.alt||"";s.#po=t.uuid;return s}}class StampEditor extends AnnotationEditor{#go=null;#fo=null;#mo=null;#bo=null;#Ao=null;#wo="";#yo=null;#vo=!1;#xo=null;#Eo=!1;#_o=!1;static _type="stamp";static _editorType=u.STAMP;constructor(t){super({...t,name:"stampEditor"});this.#bo=t.bitmapUrl;this.#Ao=t.bitmapFile;this.defaultL10nId="pdfjs-editor-stamp-editor"}static initialize(t,e){AnnotationEditor.initialize(t,e)}static isHandlingMimeForPasting(t){return G.includes(t)}static paste(t,e){e.pasteEditor({mode:u.STAMP},{bitmapFile:t.getAsFile()})}altTextFinish(){this._uiManager.useNewAltTextFlow&&(this.div.hidden=!1);super.altTextFinish()}get telemetryFinalData(){return{type:"stamp",hasAltText:!!this.altTextData?.altText}}static computeTelemetryFinalData(t){const e=t.get("hasAltText");return{hasAltText:e.get(!0)??0,hasNoAltText:e.get(!1)??0}}#So(t,e=!1){if(t){this.#go=t.bitmap;if(!e){this.#fo=t.id;this.#Eo=t.isSvg}t.file&&(this.#wo=t.file.name);this.#Co()}else this.remove()}#To(){this.#mo=null;this._uiManager.enableWaiting(!1);if(this.#yo)if(this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#go){this._editToolbar.hide();this._uiManager.editAltText(this,!0)}else{if(!this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&this.#go){this._reportTelemetry({action:"pdfjs.image.image_added",data:{alt_text_modal:!1,alt_text_type:"empty"}});try{this.mlGuessAltText()}catch{}}this.div.focus()}}async mlGuessAltText(t=null,e=!0){if(this.hasAltTextData())return null;const{mlManager:i}=this._uiManager;if(!i)throw new Error("No ML.");if(!await i.isEnabledFor("altText"))throw new Error("ML isn't enabled for alt text.");const{data:s,width:n,height:a}=t||this.copyCanvas(null,null,!0).imageData,r=await i.guess({name:"altText",request:{data:s,width:n,height:a,channels:s.length/(n*a)}});if(!r)throw new Error("No response from the AI service.");if(r.error)throw new Error("Error from the AI service.");if(r.cancel)return null;if(!r.output)throw new Error("No valid response from the AI service.");const o=r.output;await this.setGuessedAltText(o);e&&!this.hasAltTextData()&&(this.altTextData={alt:o,decorative:!1});return o}#Mo(){if(this.#fo){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#fo).then((t=>this.#So(t,!0))).finally((()=>this.#To()));return}if(this.#bo){const t=this.#bo;this.#bo=null;this._uiManager.enableWaiting(!0);this.#mo=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#So(t))).finally((()=>this.#To()));return}if(this.#Ao){const t=this.#Ao;this.#Ao=null;this._uiManager.enableWaiting(!0);this.#mo=this._uiManager.imageManager.getFromFile(t).then((t=>this.#So(t))).finally((()=>this.#To()));return}const t=document.createElement("input");t.type="file";t.accept=G.join(",");const e=this._uiManager._signal;this.#mo=new Promise((i=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this._reportTelemetry({action:"pdfjs.image.image_selected",data:{alt_text_modal:this._uiManager.useNewAltTextFlow}});this.#So(e)}else this.remove();i()}),{signal:e});t.addEventListener("cancel",(()=>{this.remove();i()}),{signal:e})})).finally((()=>this.#To()));t.click()}remove(){if(this.#fo){this.#go=null;this._uiManager.imageManager.deleteId(this.#fo);this.#yo?.remove();this.#yo=null;if(this.#xo){clearTimeout(this.#xo);this.#xo=null}}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#fo&&null===this.#yo&&this.#Mo();this.isAttachedToDOM||this.parent.add(this)}}else this.#fo&&this.#Mo()}onceAdded(t){this._isDraggable=!0;t&&this.div.focus()}isEmpty(){return!(this.#mo||this.#go||this.#bo||this.#Ao||this.#fo||this.#vo)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this._isCopy){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.addAltTextButton();this.#vo||(this.#go?this.#Co():this.#Mo());this._isCopy&&this._moveAfterPaste(t,e);this._uiManager.addShouldRescale(this);return this.div}setCanvas(t,e){const{id:i,bitmap:s}=this._uiManager.imageManager.getFromCanvas(t,e);e.remove();if(i&&this._uiManager.imageManager.isValidId(i)){this.#fo=i;s&&(this.#go=s);this.#vo=!1;this.#Co()}}_onResized(){this.onScaleChanging()}onScaleChanging(){if(!this.parent)return;null!==this.#xo&&clearTimeout(this.#xo);this.#xo=setTimeout((()=>{this.#xo=null;this.#Do()}),200)}#Co(){const{div:t}=this;let{width:e,height:i}=this.#go;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#yo=document.createElement("canvas");l.setAttribute("role","img");this.addContainer(l);this.width=e/s;this.height=i/n;this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;this._uiManager.useNewAltTextWhenAddingImage&&this._uiManager.useNewAltTextFlow&&!this.annotationElementId||(t.hidden=!1);this.#Do();if(!this.#_o){this.parent.addUndoableEditor(this);this.#_o=!0}this._reportTelemetry({action:"inserted_image"});this.#wo&&this.div.setAttribute("aria-description",this.#wo)}copyCanvas(t,e,i=!1){t||(t=224);const{width:s,height:n}=this.#go,a=new OutputScale;let r=this.#go,o=s,l=n,h=null;if(e){if(s>e||n>e){const t=Math.min(e/s,e/n);o=Math.floor(s*t);l=Math.floor(n*t)}h=document.createElement("canvas");const t=h.width=Math.ceil(o*a.sx),i=h.height=Math.ceil(l*a.sy);this.#Eo||(r=this.#Po(t,i));const d=h.getContext("2d");d.filter=this._uiManager.hcmFilter;let c="white",u="#cfcfd8";if("none"!==this._uiManager.hcmFilter)u="black";else if(window.matchMedia?.("(prefers-color-scheme: dark)").matches){c="#8f8f9d";u="#42414d"}const p=15,g=p*a.sx,f=p*a.sy,m=new OffscreenCanvas(2*g,2*f),b=m.getContext("2d");b.fillStyle=c;b.fillRect(0,0,2*g,2*f);b.fillStyle=u;b.fillRect(0,0,g,f);b.fillRect(g,f,g,f);d.fillStyle=d.createPattern(m,"repeat");d.fillRect(0,0,t,i);d.drawImage(r,0,0,r.width,r.height,0,0,t,i)}let d=null;if(i){let e,i;if(a.symmetric&&r.widtht||n>t){const a=Math.min(t/s,t/n);e=Math.floor(s*a);i=Math.floor(n*a);this.#Eo||(r=this.#Po(e,i))}}const o=new OffscreenCanvas(e,i).getContext("2d",{willReadFrequently:!0});o.drawImage(r,0,0,r.width,r.height,0,0,e,i);d={width:e,height:i,data:o.getImageData(0,0,e,i).data}}return{canvas:h,width:o,height:l,imageData:d}}#Po(t,e){const{width:i,height:s}=this.#go;let n=i,a=s,r=this.#go;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#Do(){const[t,e]=this.parentDimensions,{width:i,height:s}=this,n=new OutputScale,a=Math.ceil(i*t*n.sx),r=Math.ceil(s*e*n.sy),o=this.#yo;if(!o||o.width===a&&o.height===r)return;o.width=a;o.height=r;const l=this.#Eo?this.#go:this.#Po(a,r),h=o.getContext("2d");h.filter=this._uiManager.hcmFilter;h.drawImage(l,0,0,l.width,l.height,0,0,a,r)}#ko(t){if(t){if(this.#Eo){const t=this._uiManager.imageManager.getSvgUrl(this.#fo);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#go);t.getContext("2d").drawImage(this.#go,0,0);return t.toDataURL()}if(this.#Eo){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#go,0,0,this.#go.width,this.#go.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#go)}static async deserialize(t,e,i){let s=null,n=!1;if(t instanceof StampAnnotationElement){const{data:{rect:a,rotation:r,id:o,structParent:l,popupRef:h},container:d,parent:{page:{pageNumber:c}},canvas:p}=t;let g,f;if(p){delete t.canvas;({id:g,bitmap:f}=i.imageManager.getFromCanvas(d.id,p));p.remove()}else{n=!0;t._hasNoCanvas=!0}const m=(await e._structTree.getAriaAttributes(`${U}${o}`))?.get("aria-label")||"";s=t={annotationType:u.STAMP,bitmapId:g,bitmap:f,pageIndex:c-1,rect:a.slice(0),rotation:r,id:o,deleted:!1,accessibilityData:{decorative:!1,altText:m},isSvg:!1,structParent:l,popupRef:h}}const a=await super.deserialize(t,e,i),{rect:r,bitmap:o,bitmapUrl:l,bitmapId:h,isSvg:d,accessibilityData:c}=t;if(n){i.addMissingCanvas(t.id,a);a.#vo=!0}else if(h&&i.imageManager.isValidId(h)){a.#fo=h;o&&(a.#go=o)}else a.#bo=l;a.#Eo=d;const[p,g]=a.pageDimensions;a.width=(r[2]-r[0])/p;a.height=(r[3]-r[1])/g;a.annotationElementId=t.id||null;c&&(a.altTextData=c);a._initialData=s;a.#_o=!!s;return a}serialize(t=!1,e=null){if(this.isEmpty())return null;if(this.deleted)return this.serializeDeleted();const i={annotationType:u.STAMP,bitmapId:this.#fo,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#Eo,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#ko(!0);i.accessibilityData=this.serializeAltText(!0);i.isCopy=!0;return i}const{decorative:s,altText:n}=this.serializeAltText(!1);!s&&n&&(i.accessibilityData={type:"Figure",alt:n});if(this.annotationElementId){const t=this.#ta(i);if(t.isSame)return null;t.isSameAltText?delete i.accessibilityData:i.accessibilityData.structParent=this._initialData.structParent??-1}i.id=this.annotationElementId;if(null===e)return i;e.stamps||=new Map;const a=this.#Eo?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#fo)){if(this.#Eo){const t=e.stamps.get(this.#fo);if(a>t.area){t.area=a;t.serialized.bitmap.close();t.serialized.bitmap=this.#ko(!1)}}}else{e.stamps.set(this.#fo,{area:a,serialized:i});i.bitmap=this.#ko(!1)}return i}#ta(t){const{pageIndex:e,accessibilityData:{altText:i}}=this._initialData,s=t.pageIndex===e,n=(t.accessibilityData?.alt||"")===i;return{isSame:!this._hasBeenMoved&&!this._hasBeenResized&&s&&n,isSameAltText:n}}renderAnnotationElement(t){t.updateEdited({rect:this.getRect(0,0)});return null}}class AnnotationEditorLayer{#Nn;#Io=!1;#Ro=null;#Fo=null;#Lo=null;#Oo=new Map;#No=!1;#Bo=!1;#Ho=!1;#Uo=null;#zo=null;#Go=null;#$o=null;#m;static _initialized=!1;static#G=new Map([FreeTextEditor,InkEditor,StampEditor,HighlightEditor,SignatureEditor].map((t=>[t._editorType,t])));constructor({uiManager:t,pageIndex:e,div:i,structTreeLayer:s,accessibilityManager:n,annotationLayer:a,drawLayer:r,textLayer:o,viewport:l,l10n:h}){const d=[...AnnotationEditorLayer.#G.values()];if(!AnnotationEditorLayer._initialized){AnnotationEditorLayer._initialized=!0;for(const e of d)e.initialize(h,t)}t.registerEditorTypes(d);this.#m=t;this.pageIndex=e;this.div=i;this.#Nn=n;this.#Ro=a;this.viewport=l;this.#Go=o;this.drawLayer=r;this._structTree=s;this.#m.addLayer(this)}get isEmpty(){return 0===this.#Oo.size}get isInvisible(){return this.isEmpty&&this.#m.getMode()===u.NONE}updateToolbar(t){this.#m.updateToolbar(t)}updateMode(t=this.#m.getMode()){this.#Wo();switch(t){case u.NONE:this.disableTextSelection();this.togglePointerEvents(!1);this.toggleAnnotationLayerPointerEvents(!0);this.disableClick();return;case u.INK:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick();break;case u.HIGHLIGHT:this.enableTextSelection();this.togglePointerEvents(!1);this.disableClick();break;default:this.disableTextSelection();this.togglePointerEvents(!0);this.enableClick()}this.toggleAnnotationLayerPointerEvents(!1);const{classList:e}=this.div;for(const i of AnnotationEditorLayer.#G.values())e.toggle(`${i._type}Editing`,t===i._editorType);this.div.hidden=!1}hasTextLayer(t){return t===this.#Go?.div}setEditingState(t){this.#m.setEditingState(t)}addCommands(t){this.#m.addCommands(t)}cleanUndoStack(t){this.#m.cleanUndoStack(t)}toggleDrawing(t=!1){this.div.classList.toggle("drawing",!t)}togglePointerEvents(t=!1){this.div.classList.toggle("disabled",!t)}toggleAnnotationLayerPointerEvents(t=!1){this.#Ro?.div.classList.toggle("disabled",!t)}async enable(){this.#Ho=!0;this.div.tabIndex=0;this.togglePointerEvents(!0);const t=new Set;for(const e of this.#Oo.values()){e.enableEditing();e.show(!0);if(e.annotationElementId){this.#m.removeChangedExistingAnnotation(e);t.add(e.annotationElementId)}}if(!this.#Ro){this.#Ho=!1;return}const e=this.#Ro.getEditableAnnotations();for(const i of e){i.hide();if(this.#m.isDeletedAnnotationElement(i.data.id))continue;if(t.has(i.data.id))continue;const e=await this.deserialize(i);if(e){this.addOrRebuild(e);e.enableEditing()}}this.#Ho=!1}disable(){this.#Bo=!0;this.div.tabIndex=-1;this.togglePointerEvents(!1);const t=new Map,e=new Map;for(const i of this.#Oo.values()){i.disableEditing();if(i.annotationElementId)if(null===i.serialize()){e.set(i.annotationElementId,i);this.getEditableAnnotation(i.annotationElementId)?.show();i.remove()}else t.set(i.annotationElementId,i)}if(this.#Ro){const i=this.#Ro.getEditableAnnotations();for(const s of i){const{id:i}=s.data;if(this.#m.isDeletedAnnotationElement(i))continue;let n=e.get(i);if(n){n.resetAnnotationElement(s);n.show(!1);s.show()}else{n=t.get(i);if(n){this.#m.addChangedExistingAnnotation(n);n.renderAnnotationElement(s)&&n.show(!1)}s.show()}}}this.#Wo();this.isEmpty&&(this.div.hidden=!0);const{classList:i}=this.div;for(const t of AnnotationEditorLayer.#G.values())i.remove(`${t._type}Editing`);this.disableTextSelection();this.toggleAnnotationLayerPointerEvents(!0);this.#Bo=!1}getEditableAnnotation(t){return this.#Ro?.getEditableAnnotation(t)||null}setActiveEditor(t){this.#m.getActive()!==t&&this.#m.setActiveEditor(t)}enableTextSelection(){this.div.tabIndex=-1;if(this.#Go?.div&&!this.#$o){this.#$o=new AbortController;const t=this.#m.combinedSignal(this.#$o);this.#Go.div.addEventListener("pointerdown",this.#Vo.bind(this),{signal:t});this.#Go.div.classList.add("highlighting")}}disableTextSelection(){this.div.tabIndex=0;if(this.#Go?.div&&this.#$o){this.#$o.abort();this.#$o=null;this.#Go.div.classList.remove("highlighting")}}#Vo(t){this.#m.unselectAll();const{target:e}=t;if(e===this.#Go.div||("img"===e.getAttribute("role")||e.classList.contains("endOfContent"))&&this.#Go.div.contains(e)){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;this.#m.showAllEditors("highlight",!0,!0);this.#Go.div.classList.add("free");this.toggleDrawing();HighlightEditor.startHighlighting(this,"ltr"===this.#m.direction,{target:this.#Go.div,x:t.x,y:t.y});this.#Go.div.addEventListener("pointerup",(()=>{this.#Go.div.classList.remove("free");this.toggleDrawing(!0)}),{once:!0,signal:this.#m._signal});t.preventDefault()}}enableClick(){if(this.#Fo)return;this.#Fo=new AbortController;const t=this.#m.combinedSignal(this.#Fo);this.div.addEventListener("pointerdown",this.pointerdown.bind(this),{signal:t});const e=this.pointerup.bind(this);this.div.addEventListener("pointerup",e,{signal:t});this.div.addEventListener("pointercancel",e,{signal:t})}disableClick(){this.#Fo?.abort();this.#Fo=null}attach(t){this.#Oo.set(t.id,t);const{annotationElementId:e}=t;e&&this.#m.isDeletedAnnotationElement(e)&&this.#m.removeDeletedAnnotationElement(t)}detach(t){this.#Oo.delete(t.id);this.#Nn?.removePointerInTextLayer(t.contentDiv);!this.#Bo&&t.annotationElementId&&this.#m.addDeletedAnnotationElement(t)}remove(t){this.detach(t);this.#m.removeEditor(t);t.div.remove();t.isAttachedToDOM=!1}changeParent(t){if(t.parent!==this){if(t.parent&&t.annotationElementId){this.#m.addDeletedAnnotationElement(t.annotationElementId);AnnotationEditor.deleteAnnotationElement(t);t.annotationElementId=null}this.attach(t);t.parent?.detach(t);t.setParent(this);if(t.div&&t.isAttachedToDOM){t.div.remove();this.div.append(t.div)}}}add(t){if(t.parent!==this||!t.isAttachedToDOM){this.changeParent(t);this.#m.addEditor(t);this.attach(t);if(!t.isAttachedToDOM){const e=t.render();this.div.append(e);t.isAttachedToDOM=!0}t.fixAndSetPosition();t.onceAdded(!this.#Ho);this.#m.addToAnnotationStorage(t);t._reportTelemetry(t.telemetryInitialData)}}moveEditorInDOM(t){if(!t.isAttachedToDOM)return;const{activeElement:e}=document;if(t.div.contains(e)&&!this.#Lo){t._focusEventsAllowed=!1;this.#Lo=setTimeout((()=>{this.#Lo=null;if(t.div.contains(document.activeElement))t._focusEventsAllowed=!0;else{t.div.addEventListener("focusin",(()=>{t._focusEventsAllowed=!0}),{once:!0,signal:this.#m._signal});e.focus()}}),0)}t._structTreeParentId=this.#Nn?.moveElementInDOM(this.div,t.div,t.contentDiv,!0)}addOrRebuild(t){if(t.needsToBeRebuilt()){t.parent||=this;t.rebuild();t.show()}else this.add(t)}addUndoableEditor(t){this.addCommands({cmd:()=>t._uiManager.rebuild(t),undo:()=>{t.remove()},mustExec:!1})}getNextId(){return this.#m.getId()}get#jo(){return AnnotationEditorLayer.#G.get(this.#m.getMode())}combinedSignal(t){return this.#m.combinedSignal(t)}#qo(t){const e=this.#jo;return e?new e.prototype.constructor(t):null}canCreateNewEmptyEditor(){return this.#jo?.canCreateNewEmptyEditor()}async pasteEditor(t,e){this.updateToolbar(t);await this.#m.updateMode(t.mode);const{offsetX:i,offsetY:s}=this.#Xo(),n=this.getNextId(),a=this.#qo({parent:this,id:n,x:i,y:s,uiManager:this.#m,isCentered:!0,...e});a&&this.add(a)}async deserialize(t){return await(AnnotationEditorLayer.#G.get(t.annotationType??t.annotationEditorType)?.deserialize(t,this,this.#m))||null}createAndAddNewEditor(t,e,i={}){const s=this.getNextId(),n=this.#qo({parent:this,id:s,x:t.offsetX,y:t.offsetY,uiManager:this.#m,isCentered:e,...i});n&&this.add(n);return n}#Xo(){const{x:t,y:e,width:i,height:s}=this.div.getBoundingClientRect(),n=Math.max(0,t),a=Math.max(0,e),r=(n+Math.min(window.innerWidth,t+i))/2-t,o=(a+Math.min(window.innerHeight,e+s))/2-e,[l,h]=this.viewport.rotation%180==0?[r,o]:[o,r];return{offsetX:l,offsetY:h}}addNewEditor(t={}){this.createAndAddNewEditor(this.#Xo(),!0,t)}setSelected(t){this.#m.setSelected(t)}toggleSelected(t){this.#m.toggleSelected(t)}unselect(t){this.#m.unselect(t)}pointerup(t){const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;if(!this.#No)return;this.#No=!1;if(this.#jo?.isDrawer&&this.#jo.supportMultipleDrawings)return;if(!this.#Io){this.#Io=!0;return}const i=this.#m.getMode();i!==u.STAMP&&i!==u.SIGNATURE?this.createAndAddNewEditor(t,!1):this.#m.unselectAll()}pointerdown(t){this.#m.getMode()===u.HIGHLIGHT&&this.enableTextSelection();if(this.#No){this.#No=!1;return}const{isMac:e}=util_FeatureTest.platform;if(0!==t.button||t.ctrlKey&&e)return;if(t.target!==this.div)return;this.#No=!0;if(this.#jo?.isDrawer){this.startDrawingSession(t);return}const i=this.#m.getActive();this.#Io=!i||i.isEmpty()}startDrawingSession(t){this.div.focus({preventScroll:!0});if(this.#Uo){this.#jo.startDrawing(this,this.#m,!1,t);return}this.#m.setCurrentDrawingSession(this);this.#Uo=new AbortController;const e=this.#m.combinedSignal(this.#Uo);this.div.addEventListener("blur",(({relatedTarget:t})=>{if(t&&!this.div.contains(t)){this.#zo=null;this.commitOrRemove()}}),{signal:e});this.#jo.startDrawing(this,this.#m,!1,t)}pause(t){if(t){const{activeElement:t}=document;this.div.contains(t)&&(this.#zo=t)}else this.#zo&&setTimeout((()=>{this.#zo?.focus();this.#zo=null}),0)}endDrawingSession(t=!1){if(!this.#Uo)return null;this.#m.setCurrentDrawingSession(null);this.#Uo.abort();this.#Uo=null;this.#zo=null;return this.#jo.endDrawing(t)}findNewParent(t,e,i){const s=this.#m.findParent(e,i);if(null===s||s===this)return!1;s.changeParent(t);return!0}commitOrRemove(){if(this.#Uo){this.endDrawingSession();return!0}return!1}onScaleChanging(){this.#Uo&&this.#jo.onScaleChangingWhenDrawing(this)}destroy(){this.commitOrRemove();if(this.#m.getActive()?.parent===this){this.#m.commitOrRemove();this.#m.setActiveEditor(null)}if(this.#Lo){clearTimeout(this.#Lo);this.#Lo=null}for(const t of this.#Oo.values()){this.#Nn?.removePointerInTextLayer(t.contentDiv);t.setParent(null);t.isAttachedToDOM=!1;t.div.remove()}this.div=null;this.#Oo.clear();this.#m.removeLayer(this)}#Wo(){for(const t of this.#Oo.values())t.isEmpty()&&t.remove()}render({viewport:t}){this.viewport=t;setLayerDimensions(this.div,t);for(const t of this.#m.getEditors(this.pageIndex)){this.add(t);t.rebuild()}this.updateMode()}update({viewport:t}){this.#m.commitOrRemove();this.#Wo();const e=this.viewport.rotation,i=t.rotation;this.viewport=t;setLayerDimensions(this.div,{rotation:i});if(e!==i)for(const t of this.#Oo.values())t.rotate(i)}get pageDimensions(){const{pageWidth:t,pageHeight:e}=this.viewport.rawDims;return[t,e]}get scale(){return this.#m.viewParameters.realScale}}class DrawLayer{#fn=null;#Ko=new Map;#Yo=new Map;static#y=0;constructor({pageIndex:t}){this.pageIndex=t}setParent(t){if(this.#fn){if(this.#fn!==t){if(this.#Ko.size>0)for(const e of this.#Ko.values()){e.remove();t.append(e)}this.#fn=t}}else this.#fn=t}static get _svgFactory(){return shadow(this,"_svgFactory",new DOMSVGFactory)}static#Qo(t,[e,i,s,n]){const{style:a}=t;a.top=100*i+"%";a.left=100*e+"%";a.width=100*s+"%";a.height=100*n+"%"}#Jo(){const t=DrawLayer._svgFactory.create(1,1,!0);this.#fn.append(t);t.setAttribute("aria-hidden",!0);return t}#Zo(t,e){const i=DrawLayer._svgFactory.createElement("clipPath");t.append(i);const s=`clip_${e}`;i.setAttribute("id",s);i.setAttribute("clipPathUnits","objectBoundingBox");const n=DrawLayer._svgFactory.createElement("use");i.append(n);n.setAttribute("href",`#${e}`);n.classList.add("clip");return s}#tl(t,e){for(const[i,s]of Object.entries(e))null===s?t.removeAttribute(i):t.setAttribute(i,s)}draw(t,e=!1,i=!1){const s=DrawLayer.#y++,n=this.#Jo(),a=DrawLayer._svgFactory.createElement("defs");n.append(a);const r=DrawLayer._svgFactory.createElement("path");a.append(r);const o=`path_p${this.pageIndex}_${s}`;r.setAttribute("id",o);r.setAttribute("vector-effect","non-scaling-stroke");e&&this.#Yo.set(s,r);const l=i?this.#Zo(a,o):null,h=DrawLayer._svgFactory.createElement("use");n.append(h);h.setAttribute("href",`#${o}`);this.updateProperties(n,t);this.#Ko.set(s,n);return{id:s,clipPathId:`url(#${l})`}}drawOutline(t,e){const i=DrawLayer.#y++,s=this.#Jo(),n=DrawLayer._svgFactory.createElement("defs");s.append(n);const a=DrawLayer._svgFactory.createElement("path");n.append(a);const r=`path_p${this.pageIndex}_${i}`;a.setAttribute("id",r);a.setAttribute("vector-effect","non-scaling-stroke");let o;if(e){const t=DrawLayer._svgFactory.createElement("mask");n.append(t);o=`mask_p${this.pageIndex}_${i}`;t.setAttribute("id",o);t.setAttribute("maskUnits","objectBoundingBox");const e=DrawLayer._svgFactory.createElement("rect");t.append(e);e.setAttribute("width","1");e.setAttribute("height","1");e.setAttribute("fill","white");const s=DrawLayer._svgFactory.createElement("use");t.append(s);s.setAttribute("href",`#${r}`);s.setAttribute("stroke","none");s.setAttribute("fill","black");s.setAttribute("fill-rule","nonzero");s.classList.add("mask")}const l=DrawLayer._svgFactory.createElement("use");s.append(l);l.setAttribute("href",`#${r}`);o&&l.setAttribute("mask",`url(#${o})`);const h=l.cloneNode();s.append(h);l.classList.add("mainOutline");h.classList.add("secondaryOutline");this.updateProperties(s,t);this.#Ko.set(i,s);return i}finalizeDraw(t,e){this.#Yo.delete(t);this.updateProperties(t,e)}updateProperties(t,e){if(!e)return;const{root:i,bbox:s,rootClass:n,path:a}=e,r="number"==typeof t?this.#Ko.get(t):t;if(r){i&&this.#tl(r,i);s&&DrawLayer.#Qo(r,s);if(n){const{classList:t}=r;for(const[e,i]of Object.entries(n))t.toggle(e,i)}if(a){const t=r.firstChild.firstChild;this.#tl(t,a)}}}updateParent(t,e){if(e===this)return;const i=this.#Ko.get(t);if(i){e.#fn.append(i);this.#Ko.delete(t);e.#Ko.set(t,i)}}remove(t){this.#Yo.delete(t);if(null!==this.#fn){this.#Ko.get(t).remove();this.#Ko.delete(t)}}destroy(){this.#fn=null;for(const t of this.#Ko.values())t.remove();this.#Ko.clear();this.#Yo.clear()}}globalThis._pdfjsTestingUtils={HighlightOutliner};globalThis.pdfjsLib={AbortException,AnnotationEditorLayer,AnnotationEditorParamsType:p,AnnotationEditorType:u,AnnotationEditorUIManager,AnnotationLayer,AnnotationMode:c,AnnotationType:x,build:vt,ColorPicker,createValidAbsoluteUrl,DOMSVGFactory,DrawLayer,FeatureTest:util_FeatureTest,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,GlobalWorkerOptions,ImageKind:v,InvalidPDFException,isDataScheme,isPdfFile,isValidExplicitDest:q,MathClamp,noContextMenu,normalizeUnicode,OPS:D,OutputScale,PasswordResponses:F,PDFDataRangeTransport,PDFDateString,PDFWorker,PermissionFlag:g,PixelsPerInch,RenderingCancelledException,ResponseException,setLayerDimensions,shadow,SignatureExtractor,stopEvent,SupportedImageMimeTypes:G,TextLayer,TouchManager,updateUrlHash,Util,VerbosityLevel:M,version:yt,XfaLayer};export{AbortException,AnnotationEditorLayer,p as AnnotationEditorParamsType,u as AnnotationEditorType,AnnotationEditorUIManager,AnnotationLayer,c as AnnotationMode,x as AnnotationType,ColorPicker,DOMSVGFactory,DrawLayer,util_FeatureTest as FeatureTest,GlobalWorkerOptions,v as ImageKind,InvalidPDFException,MathClamp,D as OPS,OutputScale,PDFDataRangeTransport,PDFDateString,PDFWorker,F as PasswordResponses,g as PermissionFlag,PixelsPerInch,RenderingCancelledException,ResponseException,SignatureExtractor,G as SupportedImageMimeTypes,TextLayer,TouchManager,Util,M as VerbosityLevel,XfaLayer,vt as build,createValidAbsoluteUrl,fetchData,getDocument,getFilenameFromUrl,getPdfFilenameFromUrl,getUuid,getXfaPageViewport,isDataScheme,isPdfFile,q as isValidExplicitDest,noContextMenu,normalizeUnicode,setLayerDimensions,shadow,stopEvent,updateUrlHash,yt as version}; \ No newline at end of file diff --git a/src/client/public/js/pdf.worker.min.mjs b/src/client/public/js/pdf.worker.min.mjs deleted file mode 100644 index de36edc..0000000 --- a/src/client/public/js/pdf.worker.min.mjs +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @licstart The following is the entire license notice for the - * JavaScript code in this page - * - * Copyright 2024 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @licend The above is the entire license notice for the - * JavaScript code in this page - */ -/** - * pdfjsVersion = 5.3.31 - * pdfjsBuild = 47ad820d9 - */ -const e=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type),t=[.001,0,0,.001,0,0],a=1.35,r=.35,i=.25925925925925924,n=1,s=2,o=4,c=8,l=16,h=64,u=128,d=256,f="pdfjs_internal_editor_",g=3,p=9,m=13,b=15,y=101,w={PRINT:4,MODIFY_CONTENTS:8,COPY:16,MODIFY_ANNOTATIONS:32,FILL_INTERACTIVE_FORMS:256,COPY_FOR_ACCESSIBILITY:512,ASSEMBLE:1024,PRINT_HIGH_QUALITY:2048},S=0,x=4,k=1,C=2,v=3,F=1,O=2,T=3,M=4,D=5,R=6,N=7,E=8,L=9,j=10,_=11,U=12,X=13,q=14,H=15,W=16,z=17,$=20,G="Group",V="R",K=1,J=2,Y=4,Z=16,Q=32,ee=128,te=512,ae=1,re=2,ie=4096,ne=8192,se=32768,oe=65536,ce=131072,le=1048576,he=2097152,ue=8388608,de=16777216,fe=1,ge=2,pe=3,me=4,be=5,ye={E:"Mouse Enter",X:"Mouse Exit",D:"Mouse Down",U:"Mouse Up",Fo:"Focus",Bl:"Blur",PO:"PageOpen",PC:"PageClose",PV:"PageVisible",PI:"PageInvisible",K:"Keystroke",F:"Format",V:"Validate",C:"Calculate"},we={WC:"WillClose",WS:"WillSave",DS:"DidSave",WP:"WillPrint",DP:"DidPrint"},Se={O:"PageOpen",C:"PageClose"},xe=1,Ae=5,ke=1,Ce=2,ve=3,Fe=4,Ie=5,Oe=6,Te=7,Me=8,De=9,Be=10,Re=11,Ne=12,Ee=13,Pe=14,Le=15,je=16,_e=17,Ue=18,Xe=19,qe=20,He=21,We=22,ze=23,$e=24,Ge=25,Ve=26,Ke=27,Je=28,Ye=29,Ze=30,Qe=31,et=32,tt=33,at=34,rt=35,it=36,nt=37,st=38,ot=39,ct=40,lt=41,ht=42,ut=43,dt=44,ft=45,gt=46,pt=47,mt=48,bt=49,yt=50,wt=51,St=52,xt=53,At=54,kt=55,Ct=56,vt=57,Ft=58,It=59,Ot=60,Tt=61,Mt=62,Dt=63,Bt=64,Rt=65,Nt=66,Et=67,Pt=68,Lt=69,jt=70,_t=71,Ut=72,Xt=73,qt=74,Ht=75,Wt=76,zt=77,$t=80,Gt=81,Vt=83,Kt=84,Jt=85,Yt=86,Zt=87,Qt=88,ea=89,ta=90,aa=91,ra=92,ia=93,na=94,sa=0,oa=1,ca=2,la=3,ha=1,ua=2;let da=xe;function getVerbosityLevel(){return da}function info(e){da>=Ae&&console.log(`Info: ${e}`)}function warn(e){da>=xe&&console.log(`Warning: ${e}`)}function unreachable(e){throw new Error(e)}function assert(e,t){e||unreachable(t)}function createValidAbsoluteUrl(e,t=null,a=null){if(!e)return null;if(a&&"string"==typeof e){if(a.addDefaultProtocol&&e.startsWith("www.")){const t=e.match(/\./g);t?.length>=2&&(e=`http://${e}`)}if(a.tryConvertEncoding)try{e=stringToUTF8String(e)}catch{}}const r=t?URL.parse(e,t):URL.parse(e);return function _isValidProtocol(e){switch(e?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(r)?r:null}function shadow(e,t,a,r=!1){Object.defineProperty(e,t,{value:a,enumerable:!r,configurable:!0,writable:!1});return a}const fa=function BaseExceptionClosure(){function BaseException(e,t){this.message=e;this.name=t}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();class PasswordException extends fa{constructor(e,t){super(e,"PasswordException");this.code=t}}class UnknownErrorException extends fa{constructor(e,t){super(e,"UnknownErrorException");this.details=t}}class InvalidPDFException extends fa{constructor(e){super(e,"InvalidPDFException")}}class ResponseException extends fa{constructor(e,t,a){super(e,"ResponseException");this.status=t;this.missing=a}}class FormatError extends fa{constructor(e){super(e,"FormatError")}}class AbortException extends fa{constructor(e){super(e,"AbortException")}}function bytesToString(e){"object"==typeof e&&void 0!==e?.length||unreachable("Invalid argument for bytesToString");const t=e.length,a=8192;if(t>24&255,e>>16&255,e>>8&255,255&e)}function objectSize(e){return Object.keys(e).length}class FeatureTest{static get isLittleEndian(){return shadow(this,"isLittleEndian",function isLittleEndian(){const e=new Uint8Array(4);e[0]=1;return 1===new Uint32Array(e.buffer,0,1)[0]}())}static get isEvalSupported(){return shadow(this,"isEvalSupported",function isEvalSupported(){try{new Function("");return!0}catch{return!1}}())}static get isOffscreenCanvasSupported(){return shadow(this,"isOffscreenCanvasSupported","undefined"!=typeof OffscreenCanvas)}static get isImageDecoderSupported(){return shadow(this,"isImageDecoderSupported","undefined"!=typeof ImageDecoder)}static get platform(){const{platform:e,userAgent:t}=navigator;return shadow(this,"platform",{isAndroid:t.includes("Android"),isLinux:e.includes("Linux"),isMac:e.includes("Mac"),isWindows:e.includes("Win"),isFirefox:t.includes("Firefox")})}static get isCSSRoundSupported(){return shadow(this,"isCSSRoundSupported",globalThis.CSS?.supports?.("width: round(1.5px, 1px)"))}}const ga=Array.from(Array(256).keys(),(e=>e.toString(16).padStart(2,"0")));class Util{static makeHexColor(e,t,a){return`#${ga[e]}${ga[t]}${ga[a]}`}static scaleMinMax(e,t){let a;if(e[0]){if(e[0]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[0];t[2]*=e[0];if(e[3]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[3];t[3]*=e[3]}else{a=t[0];t[0]=t[1];t[1]=a;a=t[2];t[2]=t[3];t[3]=a;if(e[1]<0){a=t[1];t[1]=t[3];t[3]=a}t[1]*=e[1];t[3]*=e[1];if(e[2]<0){a=t[0];t[0]=t[2];t[2]=a}t[0]*=e[2];t[2]*=e[2]}t[0]+=e[4];t[1]+=e[5];t[2]+=e[4];t[3]+=e[5]}static transform(e,t){return[e[0]*t[0]+e[2]*t[1],e[1]*t[0]+e[3]*t[1],e[0]*t[2]+e[2]*t[3],e[1]*t[2]+e[3]*t[3],e[0]*t[4]+e[2]*t[5]+e[4],e[1]*t[4]+e[3]*t[5]+e[5]]}static applyTransform(e,t,a=0){const r=e[a],i=e[a+1];e[a]=r*t[0]+i*t[2]+t[4];e[a+1]=r*t[1]+i*t[3]+t[5]}static applyTransformToBezier(e,t,a=0){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5];for(let t=0;t<6;t+=2){const l=e[a+t],h=e[a+t+1];e[a+t]=l*r+h*n+o;e[a+t+1]=l*i+h*s+c}}static applyInverseTransform(e,t){const a=e[0],r=e[1],i=t[0]*t[3]-t[1]*t[2];e[0]=(a*t[3]-r*t[2]+t[2]*t[5]-t[4]*t[3])/i;e[1]=(-a*t[1]+r*t[0]+t[4]*t[1]-t[5]*t[0])/i}static axialAlignedBoundingBox(e,t,a){const r=t[0],i=t[1],n=t[2],s=t[3],o=t[4],c=t[5],l=e[0],h=e[1],u=e[2],d=e[3];let f=r*l+o,g=f,p=r*u+o,m=p,b=s*h+c,y=b,w=s*d+c,S=w;if(0!==i||0!==n){const e=i*l,t=i*u,a=n*h,r=n*d;f+=a;m+=a;p+=r;g+=r;b+=e;S+=e;w+=t;y+=t}a[0]=Math.min(a[0],f,p,g,m);a[1]=Math.min(a[1],b,w,y,S);a[2]=Math.max(a[2],f,p,g,m);a[3]=Math.max(a[3],b,w,y,S)}static inverseTransform(e){const t=e[0]*e[3]-e[1]*e[2];return[e[3]/t,-e[1]/t,-e[2]/t,e[0]/t,(e[2]*e[5]-e[4]*e[3])/t,(e[4]*e[1]-e[5]*e[0])/t]}static singularValueDecompose2dScale(e,t){const a=e[0],r=e[1],i=e[2],n=e[3],s=a**2+r**2,o=a*i+r*n,c=i**2+n**2,l=(s+c)/2,h=Math.sqrt(l**2-(s*c-o**2));t[0]=Math.sqrt(l+h||1);t[1]=Math.sqrt(l-h||1)}static normalizeRect(e){const t=e.slice(0);if(e[0]>e[2]){t[0]=e[2];t[2]=e[0]}if(e[1]>e[3]){t[1]=e[3];t[3]=e[1]}return t}static intersect(e,t){const a=Math.max(Math.min(e[0],e[2]),Math.min(t[0],t[2])),r=Math.min(Math.max(e[0],e[2]),Math.max(t[0],t[2]));if(a>r)return null;const i=Math.max(Math.min(e[1],e[3]),Math.min(t[1],t[3])),n=Math.min(Math.max(e[1],e[3]),Math.max(t[1],t[3]));return i>n?null:[a,i,r,n]}static pointBoundingBox(e,t,a){a[0]=Math.min(a[0],e);a[1]=Math.min(a[1],t);a[2]=Math.max(a[2],e);a[3]=Math.max(a[3],t)}static rectBoundingBox(e,t,a,r,i){i[0]=Math.min(i[0],e,a);i[1]=Math.min(i[1],t,r);i[2]=Math.max(i[2],e,a);i[3]=Math.max(i[3],t,r)}static#e(e,t,a,r,i,n,s,o,c,l){if(c<=0||c>=1)return;const h=1-c,u=c*c,d=u*c,f=h*(h*(h*e+3*c*t)+3*u*a)+d*r,g=h*(h*(h*i+3*c*n)+3*u*s)+d*o;l[0]=Math.min(l[0],f);l[1]=Math.min(l[1],g);l[2]=Math.max(l[2],f);l[3]=Math.max(l[3],g)}static#t(e,t,a,r,i,n,s,o,c,l,h,u){if(Math.abs(c)<1e-12){Math.abs(l)>=1e-12&&this.#e(e,t,a,r,i,n,s,o,-h/l,u);return}const d=l**2-4*h*c;if(d<0)return;const f=Math.sqrt(d),g=2*c;this.#e(e,t,a,r,i,n,s,o,(-l+f)/g,u);this.#e(e,t,a,r,i,n,s,o,(-l-f)/g,u)}static bezierBoundingBox(e,t,a,r,i,n,s,o,c){c[0]=Math.min(c[0],e,s);c[1]=Math.min(c[1],t,o);c[2]=Math.max(c[2],e,s);c[3]=Math.max(c[3],t,o);this.#t(e,a,i,s,t,r,n,o,3*(3*(a-i)-e+s),6*(e-2*a+i),3*(a-e),c);this.#t(e,a,i,s,t,r,n,o,3*(3*(r-n)-t+o),6*(t-2*r+n),3*(r-t),c)}}const pa=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,728,711,710,729,733,731,730,732,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8226,8224,8225,8230,8212,8211,402,8260,8249,8250,8722,8240,8222,8220,8221,8216,8217,8218,8482,64257,64258,321,338,352,376,381,305,322,339,353,382,0,8364];function stringToPDFString(e,t=!1){if(e[0]>="รฏ"){let a;if("รพ"===e[0]&&"รฟ"===e[1]){a="utf-16be";e.length%2==1&&(e=e.slice(0,-1))}else if("รฟ"===e[0]&&"รพ"===e[1]){a="utf-16le";e.length%2==1&&(e=e.slice(0,-1))}else"รฏ"===e[0]&&"ยป"===e[1]&&"ยฟ"===e[2]&&(a="utf-8");if(a)try{const r=new TextDecoder(a,{fatal:!0}),i=stringToBytes(e),n=r.decode(i);return t||!n.includes("")?n:n.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g,"")}catch(e){warn(`stringToPDFString: "${e}".`)}}const a=[];for(let r=0,i=e.length;rga[e])).join("")}"function"!=typeof Promise.try&&(Promise.try=function(e,...t){return new Promise((a=>{a(e(...t))}))});"function"!=typeof Math.sumPrecise&&(Math.sumPrecise=function(e){return e.reduce(((e,t)=>e+t),0)});const ya=Symbol("CIRCULAR_REF"),wa=Symbol("EOF");let Sa=Object.create(null),xa=Object.create(null),Aa=Object.create(null);class Name{constructor(e){this.name=e}static get(e){return xa[e]||=new Name(e)}}class Cmd{constructor(e){this.cmd=e}static get(e){return Sa[e]||=new Cmd(e)}}const ka=function nonSerializableClosure(){return ka};class Dict{constructor(e=null){this._map=new Map;this.xref=e;this.objId=null;this.suppressEncryption=!1;this.__nonSerializable__=ka}assignXref(e){this.xref=e}get size(){return this._map.size}get(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetch(r,this.suppressEncryption):r}async getAsync(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}return r instanceof Ref&&this.xref?this.xref.fetchAsync(r,this.suppressEncryption):r}getArray(e,t,a){let r=this._map.get(e);if(void 0===r&&void 0!==t){r=this._map.get(t);void 0===r&&void 0!==a&&(r=this._map.get(a))}r instanceof Ref&&this.xref&&(r=this.xref.fetch(r,this.suppressEncryption));if(Array.isArray(r)){r=r.slice();for(let e=0,t=r.length;e{unreachable("Should not call `set` on the empty dictionary.")};return shadow(this,"empty",e)}static merge({xref:e,dictArray:t,mergeSubDicts:a=!1}){const r=new Dict(e),i=new Map;for(const e of t)if(e instanceof Dict)for(const[t,r]of e._map){let e=i.get(t);if(void 0===e){e=[];i.set(t,e)}else if(!(a&&r instanceof Dict))continue;e.push(r)}for(const[t,a]of i){if(1===a.length||!(a[0]instanceof Dict)){r._map.set(t,a[0]);continue}const i=new Dict(e);for(const e of a)for(const[t,a]of e._map)i._map.has(t)||i._map.set(t,a);i.size>0&&r._map.set(t,i)}i.clear();return r.size>0?r:Dict.empty}clone(){const e=new Dict(this.xref);for(const t of this.getKeys())e.set(t,this.getRaw(t));return e}delete(e){delete this._map[e]}}class Ref{constructor(e,t){this.num=e;this.gen=t}toString(){return 0===this.gen?`${this.num}R`:`${this.num}R${this.gen}`}static fromString(e){const t=Aa[e];if(t)return t;const a=/^(\d+)R(\d*)$/.exec(e);return a&&"0"!==a[1]?Aa[e]=new Ref(parseInt(a[1]),a[2]?parseInt(a[2]):0):null}static get(e,t){const a=0===t?`${e}R`:`${e}R${t}`;return Aa[a]||=new Ref(e,t)}}class RefSet{constructor(e=null){this._set=new Set(e?._set)}has(e){return this._set.has(e.toString())}put(e){this._set.add(e.toString())}remove(e){this._set.delete(e.toString())}[Symbol.iterator](){return this._set.values()}clear(){this._set.clear()}}class RefSetCache{constructor(){this._map=new Map}get size(){return this._map.size}get(e){return this._map.get(e.toString())}has(e){return this._map.has(e.toString())}put(e,t){this._map.set(e.toString(),t)}putAlias(e,t){this._map.set(e.toString(),this.get(t))}[Symbol.iterator](){return this._map.values()}clear(){this._map.clear()}*values(){yield*this._map.values()}*items(){for(const[e,t]of this._map)yield[Ref.fromString(e),t]}}function isName(e,t){return e instanceof Name&&(void 0===t||e.name===t)}function isCmd(e,t){return e instanceof Cmd&&(void 0===t||e.cmd===t)}function isDict(e,t){return e instanceof Dict&&(void 0===t||isName(e.get("Type"),t))}function isRefsEqual(e,t){return e.num===t.num&&e.gen===t.gen}class BaseStream{get length(){unreachable("Abstract getter `length` accessed")}get isEmpty(){unreachable("Abstract getter `isEmpty` accessed")}get isDataLoaded(){return shadow(this,"isDataLoaded",!0)}getByte(){unreachable("Abstract method `getByte` called")}getBytes(e){unreachable("Abstract method `getBytes` called")}async getImageData(e,t){return this.getBytes(e,t)}async asyncGetBytes(){unreachable("Abstract method `asyncGetBytes` called")}get isAsync(){return!1}get isAsyncDecoder(){return!1}get canAsyncDecodeImageFromBuffer(){return!1}async getTransferableImage(){return null}peekByte(){const e=this.getByte();-1!==e&&this.pos--;return e}peekBytes(e){const t=this.getBytes(e);this.pos-=t.length;return t}getUint16(){const e=this.getByte(),t=this.getByte();return-1===e||-1===t?-1:(e<<8)+t}getInt32(){return(this.getByte()<<24)+(this.getByte()<<16)+(this.getByte()<<8)+this.getByte()}getByteRange(e,t){unreachable("Abstract method `getByteRange` called")}getString(e){return bytesToString(this.getBytes(e))}skip(e){this.pos+=e||1}reset(){unreachable("Abstract method `reset` called")}moveStart(){unreachable("Abstract method `moveStart` called")}makeSubStream(e,t,a=null){unreachable("Abstract method `makeSubStream` called")}getBaseStreams(){return null}}const Ca=/^[1-9]\.\d$/,va=2**31-1,Fa=[1,0,0,1,0,0],Ia=["ColorSpace","ExtGState","Font","Pattern","Properties","Shading","XObject"],Oa=["ExtGState","Font","Properties","XObject"];function getLookupTableFactory(e){let t;return function(){if(e){t=Object.create(null);e(t);e=null}return t}}class MissingDataException extends fa{constructor(e,t){super(`Missing data [${e}, ${t})`,"MissingDataException");this.begin=e;this.end=t}}class ParserEOFException extends fa{constructor(e){super(e,"ParserEOFException")}}class XRefEntryException extends fa{constructor(e){super(e,"XRefEntryException")}}class XRefParseException extends fa{constructor(e){super(e,"XRefParseException")}}function arrayBuffersToBytes(e){const t=e.length;if(0===t)return new Uint8Array(0);if(1===t)return new Uint8Array(e[0]);let a=0;for(let r=0;r0,"The number should be a positive integer.");const a="M".repeat(e/1e3|0)+Ta[e%1e3/100|0]+Ta[10+(e%100/10|0)]+Ta[20+e%10];return t?a.toLowerCase():a}function log2(e){return e>0?Math.ceil(Math.log2(e)):0}function readInt8(e,t){return e[t]<<24>>24}function readInt16(e,t){return(e[t]<<24|e[t+1]<<16)>>16}function readUint16(e,t){return e[t]<<8|e[t+1]}function readUint32(e,t){return(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0}function isWhiteSpace(e){return 32===e||9===e||13===e||10===e}function isNumberArray(e,t){return Array.isArray(e)?(null===t||e.length===t)&&e.every((e=>"number"==typeof e)):ArrayBuffer.isView(e)&&!(e instanceof BigInt64Array||e instanceof BigUint64Array)&&(null===t||e.length===t)}function lookupMatrix(e,t){return isNumberArray(e,6)?e:t}function lookupRect(e,t){return isNumberArray(e,4)?e:t}function lookupNormalRect(e,t){return isNumberArray(e,4)?Util.normalizeRect(e):t}function parseXFAPath(e){const t=/(.+)\[(\d+)\]$/;return e.split(".").map((e=>{const a=e.match(t);return a?{name:a[1],pos:parseInt(a[2],10)}:{name:e,pos:0}}))}function escapePDFName(e){const t=[];let a=0;for(let r=0,i=e.length;r126||35===i||40===i||41===i||60===i||62===i||91===i||93===i||123===i||125===i||47===i||37===i){a"\n"===e?"\\n":"\r"===e?"\\r":`\\${e}`))}function _collectJS(e,t,a,r){if(!e)return;let i=null;if(e instanceof Ref){if(r.has(e))return;i=e;r.put(i);e=t.fetch(e)}if(Array.isArray(e))for(const i of e)_collectJS(i,t,a,r);else if(e instanceof Dict){if(isName(e.get("S"),"JavaScript")){const t=e.get("JS");let r;t instanceof BaseStream?r=t.getString():"string"==typeof t&&(r=t);r&&=stringToPDFString(r,!0).replaceAll("\0","");r&&a.push(r)}_collectJS(e.getRaw("Next"),t,a,r)}i&&r.remove(i)}function collectActions(e,t,a){const r=Object.create(null),i=getInheritableProperty({dict:t,key:"AA",stopWhenFound:!1});if(i)for(let t=i.length-1;t>=0;t--){const n=i[t];if(n instanceof Dict)for(const t of n.getKeys()){const i=a[t];if(!i)continue;const s=[];_collectJS(n.getRaw(t),e,s,new RefSet);s.length>0&&(r[i]=s)}}if(t.has("A")){const a=[];_collectJS(t.get("A"),e,a,new RefSet);a.length>0&&(r.Action=a)}return objectSize(r)>0?r:null}const Ma={60:"<",62:">",38:"&",34:""",39:"'"};function*codePointIter(e){for(let t=0,a=e.length;t55295&&(a<57344||a>65533)&&t++;yield a}}function encodeToXmlString(e){const t=[];let a=0;for(let r=0,i=e.length;r55295&&(i<57344||i>65533)&&r++;a=r+1}}if(0===t.length)return e;a: ${e}.`);return!1}return!0}function validateCSSFont(e){const t=new Set(["100","200","300","400","500","600","700","800","900","1000","normal","bold","bolder","lighter"]),{fontFamily:a,fontWeight:r,italicAngle:i}=e;if(!validateFontName(a,!0))return!1;const n=r?r.toString():"";e.fontWeight=t.has(n)?n:"400";const s=parseFloat(i);e.italicAngle=isNaN(s)||s<-90||s>90?"14":i.toString();return!0}function recoverJsURL(e){const t=new RegExp("^\\s*("+["app.launchURL","window.open","xfa.host.gotoURL"].join("|").replaceAll(".","\\.")+")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))","i").exec(e);return t?.[2]?{url:t[2],newWindow:"app.launchURL"===t[1]&&"true"===t[3]}:null}function numberToString(e){if(Number.isInteger(e))return e.toString();const t=Math.round(100*e);return t%100==0?(t/100).toString():t%10==0?e.toFixed(1):e.toFixed(2)}function getNewAnnotationsMap(e){if(!e)return null;const t=new Map;for(const[a,r]of e){if(!a.startsWith(f))continue;let e=t.get(r.pageIndex);if(!e){e=[];t.set(r.pageIndex,e)}e.push(r)}return t.size>0?t:null}function stringToAsciiOrUTF16BE(e){return function isAscii(e){return/^[\x00-\x7F]*$/.test(e)}(e)?e:stringToUTF16String(e,!0)}function stringToUTF16HexString(e){const t=[];for(let a=0,r=e.length;a>8&255],ga[255&r])}return t.join("")}function stringToUTF16String(e,t=!1){const a=[];t&&a.push("รพรฟ");for(let t=0,r=e.length;t>8&255),String.fromCharCode(255&r))}return a.join("")}function getRotationMatrix(e,t,a){switch(e){case 90:return[0,1,-1,0,t,0];case 180:return[-1,0,0,-1,t,a];case 270:return[0,-1,1,0,0,a];default:throw new Error("Invalid rotation")}}function getSizeInBytes(e){return Math.ceil(Math.ceil(Math.log2(1+e))/8)}class QCMS{static#a=null;static _memory=null;static _mustAddAlpha=!1;static _destBuffer=null;static _destOffset=0;static _destLength=0;static _cssColor="";static _makeHexColor=null;static get _memoryArray(){const e=this.#a;return e?.byteLength?e:this.#a=new Uint8Array(this._memory.buffer)}}let Da;const Ba="undefined"!=typeof TextDecoder?new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}):{decode:()=>{throw Error("TextDecoder not available")}};"undefined"!=typeof TextDecoder&&Ba.decode();let Ra=null;function getUint8ArrayMemory0(){null!==Ra&&0!==Ra.byteLength||(Ra=new Uint8Array(Da.memory.buffer));return Ra}let Na=0;function passArray8ToWasm0(e,t){const a=t(1*e.length,1)>>>0;getUint8ArrayMemory0().set(e,a/1);Na=e.length;return a}const Ea=Object.freeze({RGB8:0,0:"RGB8",RGBA8:1,1:"RGBA8",BGRA8:2,2:"BGRA8",Gray8:3,3:"Gray8",GrayA8:4,4:"GrayA8",CMYK:5,5:"CMYK"}),Pa=Object.freeze({Perceptual:0,0:"Perceptual",RelativeColorimetric:1,1:"RelativeColorimetric",Saturation:2,2:"Saturation",AbsoluteColorimetric:3,3:"AbsoluteColorimetric"});function __wbg_get_imports(){const e={wbg:{}};e.wbg.__wbg_copyresult_b08ee7d273f295dd=function(e,t){!function copy_result(e,t){const{_mustAddAlpha:a,_destBuffer:r,_destOffset:i,_destLength:n,_memoryArray:s}=QCMS;if(t!==n)if(a)for(let a=e,n=e+t,o=i;a>>0,t>>>0)};e.wbg.__wbg_copyrgb_d60ce17bb05d9b67=function(e){!function copy_rgb(e){const{_destBuffer:t,_destOffset:a,_memoryArray:r}=QCMS;t[a]=r[e];t[a+1]=r[e+1];t[a+2]=r[e+2]}(e>>>0)};e.wbg.__wbg_makecssRGB_893bf0cd9fdb302d=function(e){!function make_cssRGB(e){const{_memoryArray:t}=QCMS;QCMS._cssColor=QCMS._makeHexColor(t[e],t[e+1],t[e+2])}(e>>>0)};e.wbg.__wbindgen_init_externref_table=function(){const e=Da.__wbindgen_export_0,t=e.grow(4);e.set(0,void 0);e.set(t+0,void 0);e.set(t+1,null);e.set(t+2,!0);e.set(t+3,!1)};e.wbg.__wbindgen_throw=function(e,t){throw new Error(function getStringFromWasm0(e,t){e>>>=0;return Ba.decode(getUint8ArrayMemory0().subarray(e,e+t))}(e,t))};return e}function __wbg_finalize_init(e,t){Da=e.exports;__wbg_init.__wbindgen_wasm_module=t;Ra=null;Da.__wbindgen_start();return Da}async function __wbg_init(e){if(void 0!==Da)return Da;void 0!==e&&(Object.getPrototypeOf(e)===Object.prototype?({module_or_path:e}=e):console.warn("using deprecated parameters for the initialization function; pass a single object instead"));const t=__wbg_get_imports();("string"==typeof e||"function"==typeof Request&&e instanceof Request||"function"==typeof URL&&e instanceof URL)&&(e=fetch(e));const{instance:a,module:r}=await async function __wbg_load(e,t){if("function"==typeof Response&&e instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(e,t)}catch(t){if("application/wasm"==e.headers.get("Content-Type"))throw t;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",t)}const a=await e.arrayBuffer();return await WebAssembly.instantiate(a,t)}{const a=await WebAssembly.instantiate(e,t);return a instanceof WebAssembly.Instance?{instance:a,module:e}:a}}(await e,t);return __wbg_finalize_init(a,r)}class ColorSpace{static#r=new Uint8ClampedArray(3);constructor(e,t){this.name=e;this.numComps=t}getRgb(e,t,a=new Uint8ClampedArray(3)){this.getRgbItem(e,t,a,0);return a}getRgbHex(e,t){const a=this.getRgb(e,t,ColorSpace.#r);return Util.makeHexColor(a[0],a[1],a[2])}getRgbItem(e,t,a,r){unreachable("Should not call ColorSpace.getRgbItem")}getRgbBuffer(e,t,a,r,i,n,s){unreachable("Should not call ColorSpace.getRgbBuffer")}getOutputLength(e,t){unreachable("Should not call ColorSpace.getOutputLength")}isPassthrough(e){return!1}isDefaultDecode(e,t){return ColorSpace.isDefaultDecode(e,this.numComps)}fillRgb(e,t,a,r,i,n,s,o,c){const l=t*a;let h=null;const u=1<u&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=s<=8?new Uint8Array(u):new Uint16Array(u);for(let e=0;e=.99554525?1:MathClamp(1.055*e**(1/2.4)-.055,0,1)}#b(e){return e<0?-this.#b(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#d}#y(e,t,a){if(0===e[0]&&0===e[1]&&0===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=this.#b(0),i=(1-r)/(1-this.#b(e[0])),n=1-i,s=(1-r)/(1-this.#b(e[1])),o=1-s,c=(1-r)/(1-this.#b(e[2])),l=1-c;a[0]=t[0]*i+n;a[1]=t[1]*s+o;a[2]=t[2]*c+l}#w(e,t,a){if(1===e[0]&&1===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=a;this.#f(CalRGBCS.#n,t,r);const i=CalRGBCS.#l;this.#g(e,r,i);this.#f(CalRGBCS.#s,i,a)}#S(e,t,a){const r=a;this.#f(CalRGBCS.#n,t,r);const i=CalRGBCS.#l;this.#p(e,r,i);this.#f(CalRGBCS.#s,i,a)}#i(e,t,a,r,i){const n=MathClamp(e[t]*i,0,1),s=MathClamp(e[t+1]*i,0,1),o=MathClamp(e[t+2]*i,0,1),c=1===n?1:n**this.GR,l=1===s?1:s**this.GG,h=1===o?1:o**this.GB,u=this.MXA*c+this.MXB*l+this.MXC*h,d=this.MYA*c+this.MYB*l+this.MYC*h,f=this.MZA*c+this.MZB*l+this.MZC*h,g=CalRGBCS.#h;g[0]=u;g[1]=d;g[2]=f;const p=CalRGBCS.#u;this.#w(this.whitePoint,g,p);const m=CalRGBCS.#h;this.#y(this.blackPoint,p,m);const b=CalRGBCS.#u;this.#S(CalRGBCS.#c,m,b);const y=CalRGBCS.#h;this.#f(CalRGBCS.#o,b,y);a[r]=255*this.#m(y[0]);a[r+1]=255*this.#m(y[1]);a[r+2]=255*this.#m(y[2])}getRgbItem(e,t,a,r){this.#i(e,t,a,r,1)}getRgbBuffer(e,t,a,r,i,n,s){const o=1/((1<this.amax||this.bmin>this.bmax){info("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#x(e){return e>=6/29?e**3:108/841*(e-4/29)}#A(e,t,a,r){return a+e*(r-a)/t}#i(e,t,a,r,i){let n=e[t],s=e[t+1],o=e[t+2];if(!1!==a){n=this.#A(n,a,0,100);s=this.#A(s,a,this.amin,this.amax);o=this.#A(o,a,this.bmin,this.bmax)}s>this.amax?s=this.amax:sthis.bmax?o=this.bmax:o{!function qcms_drop_transformer(e){Da.qcms_drop_transformer(e)}(e)}));constructor(e,t,a){if(!IccColorSpace.isUsable)throw new Error("No ICC color space support");super(t,a);let r;switch(a){case 1:r=Ea.Gray8;this.#C=(e,t,a)=>function qcms_convert_one(e,t,a){Da.qcms_convert_one(e,t,a)}(this.#k,255*e[t],a);break;case 3:r=Ea.RGB8;this.#C=(e,t,a)=>function qcms_convert_three(e,t,a,r,i){Da.qcms_convert_three(e,t,a,r,i)}(this.#k,255*e[t],255*e[t+1],255*e[t+2],a);break;case 4:r=Ea.CMYK;this.#C=(e,t,a)=>function qcms_convert_four(e,t,a,r,i,n){Da.qcms_convert_four(e,t,a,r,i,n)}(this.#k,255*e[t],255*e[t+1],255*e[t+2],255*e[t+3],a);break;default:throw new Error(`Unsupported number of components: ${a}`)}this.#k=function qcms_transformer_from_memory(e,t,a){const r=passArray8ToWasm0(e,Da.__wbindgen_malloc),i=Na;return Da.qcms_transformer_from_memory(r,i,t,a)>>>0}(e,r,Pa.Perceptual);if(!this.#k)throw new Error("Failed to create ICC color space");IccColorSpace.#I.register(this,this.#k)}getRgbHex(e,t){this.#C(e,t,!0);return QCMS._cssColor}getRgbItem(e,t,a,r){QCMS._destBuffer=a;QCMS._destOffset=r;QCMS._destLength=3;this.#C(e,t,!1);QCMS._destBuffer=null}getRgbBuffer(e,t,a,r,i,n,s){e=e.subarray(t,t+a*this.numComps);if(8!==n){const t=255/((1<=this.end?-1:this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e)return t.subarray(a,r);let i=a+e;i>r&&(i=r);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);return this.bytes.subarray(e,t)}reset(){this.pos=this.start}moveStart(){this.start=this.pos}makeSubStream(e,t,a=null){return new Stream(this.bytes.buffer,e,t,a)}}class StringStream extends Stream{constructor(e){super(stringToBytes(e))}}class NullStream extends Stream{constructor(){super(new Uint8Array(0))}}class ChunkedStream extends Stream{constructor(e,t,a){super(new Uint8Array(e),0,e,null);this.chunkSize=t;this._loadedChunks=new Set;this.numChunks=Math.ceil(e/t);this.manager=a;this.progressiveDataLength=0;this.lastSuccessfulEnsureByteChunk=-1}getMissingChunks(){const e=[];for(let t=0,a=this.numChunks;t=this.end?this.numChunks:Math.floor(t/this.chunkSize);for(let e=a;ethis.numChunks)&&t!==this.lastSuccessfulEnsureByteChunk){if(!this._loadedChunks.has(t))throw new MissingDataException(e,e+1);this.lastSuccessfulEnsureByteChunk=t}}ensureRange(e,t){if(e>=t)return;if(t<=this.progressiveDataLength)return;const a=Math.floor(e/this.chunkSize);if(a>this.numChunks)return;const r=Math.min(Math.floor((t-1)/this.chunkSize)+1,this.numChunks);for(let i=a;i=this.end)return-1;e>=this.progressiveDataLength&&this.ensureByte(e);return this.bytes[this.pos++]}getBytes(e){const t=this.bytes,a=this.pos,r=this.end;if(!e){r>this.progressiveDataLength&&this.ensureRange(a,r);return t.subarray(a,r)}let i=a+e;i>r&&(i=r);i>this.progressiveDataLength&&this.ensureRange(a,i);this.pos=i;return t.subarray(a,i)}getByteRange(e,t){e<0&&(e=0);t>this.end&&(t=this.end);t>this.progressiveDataLength&&this.ensureRange(e,t);return this.bytes.subarray(e,t)}makeSubStream(e,t,a=null){t?e+t>this.progressiveDataLength&&this.ensureRange(e,e+t):e>=this.progressiveDataLength&&this.ensureByte(e);function ChunkedStreamSubstream(){}ChunkedStreamSubstream.prototype=Object.create(this);ChunkedStreamSubstream.prototype.getMissingChunks=function(){const e=this.chunkSize,t=Math.floor(this.start/e),a=Math.floor((this.end-1)/e)+1,r=[];for(let e=t;e{const readChunk=({value:n,done:s})=>{try{if(s){const t=arrayBuffersToBytes(r);r=null;e(t);return}i+=n.byteLength;a.isStreamingSupported&&this.onProgress({loaded:i});r.push(n);a.read().then(readChunk,t)}catch(e){t(e)}};a.read().then(readChunk,t)})).then((t=>{this.aborted||this.onReceiveData({chunk:t,begin:e})}))}requestAllChunks(e=!1){if(!e){const e=this.stream.getMissingChunks();this._requestChunks(e)}return this._loadedStreamCapability.promise}_requestChunks(e){const t=this.currRequestId++,a=new Set;this._chunksNeededByRequest.set(t,a);for(const t of e)this.stream.hasChunk(t)||a.add(t);if(0===a.size)return Promise.resolve();const r=Promise.withResolvers();this._promisesByRequest.set(t,r);const i=[];for(const e of a){let a=this._requestsByChunk.get(e);if(!a){a=[];this._requestsByChunk.set(e,a);i.push(e)}a.push(t)}if(i.length>0){const e=this.groupChunks(i);for(const t of e){const e=t.beginChunk*this.chunkSize,a=Math.min(t.endChunk*this.chunkSize,this.length);this.sendRequest(e,a).catch(r.reject)}}return r.promise.catch((e=>{if(!this.aborted)throw e}))}getStream(){return this.stream}requestRange(e,t){t=Math.min(t,this.length);const a=this.getBeginChunk(e),r=this.getEndChunk(t),i=[];for(let e=a;ee-t));return this._requestChunks(t)}groupChunks(e){const t=[];let a=-1,r=-1;for(let i=0,n=e.length;i=0&&r+1!==n){t.push({beginChunk:a,endChunk:r+1});a=n}i+1===e.length&&t.push({beginChunk:a,endChunk:n+1});r=n}return t}onProgress(e){this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize+e.loaded,total:this.length})}onReceiveData(e){const t=e.chunk,a=void 0===e.begin,r=a?this.progressiveDataLength:e.begin,i=r+t.byteLength,n=Math.floor(r/this.chunkSize),s=i0||o.push(a)}}}if(!this.disableAutoFetch&&0===this._requestsByChunk.size){let e;if(1===this.stream.numChunksLoaded){const t=this.stream.numChunks-1;this.stream.hasChunk(t)||(e=t)}else e=this.stream.nextEmptyChunk(s);Number.isInteger(e)&&this._requestChunks([e])}for(const e of o){const t=this._promisesByRequest.get(e);this._promisesByRequest.delete(e);t.resolve()}this.msgHandler.send("DocProgress",{loaded:this.stream.numChunksLoaded*this.chunkSize,total:this.length})}onError(e){this._loadedStreamCapability.reject(e)}getBeginChunk(e){return Math.floor(e/this.chunkSize)}getEndChunk(e){return Math.floor((e-1)/this.chunkSize)+1}abort(e){this.aborted=!0;this.pdfNetworkStream?.cancelAllRequests(e);for(const t of this._promisesByRequest.values())t.reject(e)}}function convertToRGBA(e){switch(e.kind){case k:return convertBlackAndWhiteToRGBA(e);case C:return function convertRGBToRGBA({src:e,srcPos:t=0,dest:a,destPos:r=0,width:i,height:n}){let s=0;const o=i*n*3,c=o>>2,l=new Uint32Array(e.buffer,t,c);if(FeatureTest.isLittleEndian){for(;s>>24|t<<8|4278190080;a[r+2]=t>>>16|i<<16|4278190080;a[r+3]=i>>>8|4278190080}for(let i=4*s,n=t+o;i>>8|255;a[r+2]=t<<16|i>>>16|255;a[r+3]=i<<8|255}for(let i=4*s,n=t+o;i>3,u=7&r,d=e.length;a=new Uint32Array(a.buffer);let f=0;for(let r=0;ra||t>a)return!0;const r=e*t;if(this._hasMaxArea)return r>this.MAX_AREA;if(r(this.MAX_AREA=this.#T**2)}static getReducePowerForJPX(e,t,a){const r=e*t,i=2**30/(4*a);if(!this.needsToBeResized(e,t))return r>i?Math.ceil(Math.log2(r/i)):0;const{MAX_DIM:n,MAX_AREA:s}=this,o=Math.max(e/n,t/n,Math.sqrt(r/Math.min(i,s)));return Math.ceil(Math.log2(o))}static get MAX_DIM(){return shadow(this,"MAX_DIM",this._guessMax(2048,65537,0,1))}static get MAX_AREA(){this._hasMaxArea=!0;return shadow(this,"MAX_AREA",this._guessMax(this.#T,this.MAX_DIM,128,0)**2)}static set MAX_AREA(e){if(e>=0){this._hasMaxArea=!0;shadow(this,"MAX_AREA",e)}}static setOptions({canvasMaxAreaInBytes:e=-1,isImageDecoderSupported:t=!1}){this._hasMaxArea||(this.MAX_AREA=e>>2);this.#M=t}static _areGoodDims(e,t){try{const a=new OffscreenCanvas(e,t),r=a.getContext("2d");r.fillRect(0,0,1,1);const i=r.getImageData(0,0,1,1).data[3];a.width=a.height=1;return 0!==i}catch{return!1}}static _guessMax(e,t,a,r){for(;e+a+1va){const e=this.#D();if(e)return e}const r=this._encodeBMP();let i,n;if(await ImageResizer.canUseImageDecoder){i=new ImageDecoder({data:r,type:"image/bmp",preferAnimation:!1,transfer:[r.buffer]});n=i.decode().catch((e=>{warn(`BMP image decoding failed: ${e}`);return createImageBitmap(new Blob([this._encodeBMP().buffer],{type:"image/bmp"}))})).finally((()=>{i.close()}))}else n=createImageBitmap(new Blob([r.buffer],{type:"image/bmp"}));const{MAX_AREA:s,MAX_DIM:o}=ImageResizer,c=Math.max(t/o,a/o,Math.sqrt(t*a/s)),l=Math.max(c,2),h=Math.round(10*(c+1.25))/10/l,u=Math.floor(Math.log2(h)),d=new Array(u+2).fill(2);d[0]=l;d.splice(-1,1,h/(1<>s,c=r>>s;let l,h=r;try{l=new Uint8Array(n)}catch{let e=Math.floor(Math.log2(n+1));for(;;)try{l=new Uint8Array(2**e-1);break}catch{e-=1}h=Math.floor((2**e-1)/(4*a));const t=a*h*4;t>s;e>3,s=a+3&-4;if(a!==s){const e=new Uint8Array(s*t);let r=0;for(let n=0,o=t*a;ni&&(r=i)}else{for(;!this.eof;)this.readBlock(t);r=this.bufferLength}this.pos=r;return this.buffer.subarray(a,r)}async getImageData(e,t){if(!this.canAsyncDecodeImageFromBuffer)return this.isAsyncDecoder?this.decodeImage(null,t):this.getBytes(e,t);const a=await this.stream.asyncGetBytes();return this.decodeImage(a,t)}reset(){this.pos=0}makeSubStream(e,t,a=null){if(void 0===t)for(;!this.eof;)this.readBlock();else{const a=e+t;for(;this.bufferLength<=a&&!this.eof;)this.readBlock()}return new Stream(this.buffer,e,t,a)}getBaseStreams(){return this.str?this.str.getBaseStreams():null}}class StreamsSequenceStream extends DecodeStream{constructor(e,t=null){e=e.filter((e=>e instanceof BaseStream));let a=0;for(const t of e)a+=t instanceof DecodeStream?t._rawMinBufferLength:t.length;super(a);this.streams=e;this._onError=t}readBlock(){const e=this.streams;if(0===e.length){this.eof=!0;return}const t=e.shift();let a;try{a=t.getBytes()}catch(e){if(this._onError){this._onError(e,t.dict?.objId);return}throw e}const r=this.bufferLength,i=r+a.length;this.ensureBuffer(i).set(a,r);this.bufferLength=i}getBaseStreams(){const e=[];for(const t of this.streams){const a=t.getBaseStreams();a&&e.push(...a)}return e.length>0?e:null}}class ColorSpaceUtils{static parse({cs:e,xref:t,resources:a=null,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n,asyncIfNotCached:s=!1}){const o={xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n};let c,l,h;if(e instanceof Ref){l=e;const a=i.getByRef(l)||n.getByRef(l);if(a)return a;e=t.fetch(e)}if(e instanceof Name){c=e.name;const t=n.getByName(c);if(t)return t}try{h=this.#B(e,o)}catch(e){if(s&&!(e instanceof MissingDataException))return Promise.reject(e);throw e}if(c||l){n.set(c,l,h);l&&i.set(null,l,h)}return s?Promise.resolve(h):h}static#R(e,t){const{globalColorSpaceCache:a}=t;let r;if(e instanceof Ref){r=e;const t=a.getByRef(r);if(t)return t}const i=this.#B(e,t);r&&a.set(null,r,i);return i}static#B(e,t){const{xref:a,resources:r,pdfFunctionFactory:i,globalColorSpaceCache:n}=t;if((e=a.fetchIfRef(e))instanceof Name)switch(e.name){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"DeviceRGBA":return this.rgba;case"CMYK":case"DeviceCMYK":return this.cmyk;case"Pattern":return new PatternCS(null);default:if(r instanceof Dict){const a=r.get("ColorSpace");if(a instanceof Dict){const r=a.get(e.name);if(r){if(r instanceof Name)return this.#B(r,t);e=r;break}}}warn(`Unrecognized ColorSpace: ${e.name}`);return this.gray}if(Array.isArray(e)){const r=a.fetchIfRef(e[0]).name;let s,o,c,l,h,u;switch(r){case"G":case"DeviceGray":return this.gray;case"RGB":case"DeviceRGB":return this.rgb;case"CMYK":case"DeviceCMYK":return this.cmyk;case"CalGray":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.get("Gamma");return new CalGrayCS(l,h,u);case"CalRGB":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");u=s.getArray("Gamma");const d=s.getArray("Matrix");return new CalRGBCS(l,h,u,d);case"ICCBased":const f=e[1]instanceof Ref;if(f){const t=n.getByRef(e[1]);if(t)return t}const g=a.fetchIfRef(e[1]),p=g.dict;o=p.get("N");if(IccColorSpace.isUsable)try{const t=new IccColorSpace(g.getBytes(),"ICCBased",o);f&&n.set(null,e[1],t);return t}catch(t){if(t instanceof MissingDataException)throw t;warn(`ICCBased color space (${e[1]}): "${t}".`)}const m=p.getRaw("Alternate");if(m){const e=this.#R(m,t);if(e.numComps===o)return e;warn("ICCBased color space: Ignoring incorrect /Alternate entry.")}if(1===o)return this.gray;if(3===o)return this.rgb;if(4===o)return this.cmyk;break;case"Pattern":c=e[1]||null;c&&(c=this.#R(c,t));return new PatternCS(c);case"I":case"Indexed":c=this.#R(e[1],t);const b=MathClamp(a.fetchIfRef(e[2]),0,255),y=a.fetchIfRef(e[3]);return new IndexedCS(c,b,y);case"Separation":case"DeviceN":const w=a.fetchIfRef(e[1]);o=Array.isArray(w)?w.length:1;c=this.#R(e[2],t);const S=i.create(e[3]);return new AlternateCS(o,c,S);case"Lab":s=a.fetchIfRef(e[1]);l=s.getArray("WhitePoint");h=s.getArray("BlackPoint");const x=s.getArray("Range");return new LabCS(l,h,x);default:warn(`Unimplemented ColorSpace object: ${r}`);return this.gray}}warn(`Unrecognized ColorSpace object: ${e}`);return this.gray}static get gray(){return shadow(this,"gray",new DeviceGrayCS)}static get rgb(){return shadow(this,"rgb",new DeviceRgbCS)}static get rgba(){return shadow(this,"rgba",new DeviceRgbaCS)}static get cmyk(){if(CmykICCBasedCS.isUsable)try{return shadow(this,"cmyk",new CmykICCBasedCS)}catch{warn("CMYK fallback: DeviceCMYK")}return shadow(this,"cmyk",new DeviceCmykCS)}}class JpegError extends fa{constructor(e){super(e,"JpegError")}}class DNLMarkerError extends fa{constructor(e,t){super(e,"DNLMarkerError");this.scanLines=t}}class EOIMarkerError extends fa{constructor(e){super(e,"EOIMarkerError")}}const ja=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),_a=4017,Ua=799,Xa=3406,qa=2276,Ha=1567,Wa=3784,za=5793,$a=2896;function buildHuffmanTable(e,t){let a,r,i=0,n=16;for(;n>0&&!e[n-1];)n--;const s=[{children:[],index:0}];let o,c=s[0];for(a=0;a0;)c=s.pop();c.index++;s.push(c);for(;s.length<=a;){s.push(o={children:[],index:0});c.children[c.index]=o.children;c=o}i++}if(a+10){g--;return f>>g&1}f=e[t++];if(255===f){const r=e[t++];if(r){if(220===r&&l){const r=readUint16(e,t+=2);t+=2;if(r>0&&r!==a.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",r)}else if(217===r){if(l){const e=y*(8===a.precision?8:0);if(e>0&&Math.round(a.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(f<<8|r).toString(16)}`)}}g=7;return f>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<0){p--;return}let a=n;const r=s;for(;a<=r;){const r=decodeHuffman(e.huffmanTableAC),i=15&r,n=r>>4;if(0===i){if(n<15){p=receive(n)+(1<>4;if(0===i)if(l<15){p=receive(l)+(1<>4;if(0===r){if(n<15)break;i+=16;continue}i+=n;const s=ja[i];e.blockData[t+s]=receiveAndExtend(r);i++}};let O,T=0;const M=1===w?r[0].blocksPerLine*r[0].blocksPerColumn:h*a.mcusPerColumn;let D,R;for(;T<=M;){const a=i?Math.min(M-T,i):M;if(a>0){for(x=0;x0?"unexpected":"excessive"} MCU data, current marker is: ${O.invalid}`);t=O.offset}if(!(O.marker>=65488&&O.marker<=65495))break;t+=2}return t-d}function quantizeAndInverse(e,t,a){const r=e.quantizationTable,i=e.blockData;let n,s,o,c,l,h,u,d,f,g,p,m,b,y,w,S,x;if(!r)throw new JpegError("missing required Quantization Table.");for(let e=0;e<64;e+=8){f=i[t+e];g=i[t+e+1];p=i[t+e+2];m=i[t+e+3];b=i[t+e+4];y=i[t+e+5];w=i[t+e+6];S=i[t+e+7];f*=r[e];if(g|p|m|b|y|w|S){g*=r[e+1];p*=r[e+2];m*=r[e+3];b*=r[e+4];y*=r[e+5];w*=r[e+6];S*=r[e+7];n=za*f+128>>8;s=za*b+128>>8;o=p;c=w;l=$a*(g-S)+128>>8;d=$a*(g+S)+128>>8;h=m<<4;u=y<<4;n=n+s+1>>1;s=n-s;x=o*Wa+c*Ha+128>>8;o=o*Ha-c*Wa+128>>8;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*_a+2048>>12;h=h*_a-u*Ua+2048>>12;u=x;a[e]=n+d;a[e+7]=n-d;a[e+1]=s+u;a[e+6]=s-u;a[e+2]=o+h;a[e+5]=o-h;a[e+3]=c+l;a[e+4]=c-l}else{x=za*f+512>>10;a[e]=x;a[e+1]=x;a[e+2]=x;a[e+3]=x;a[e+4]=x;a[e+5]=x;a[e+6]=x;a[e+7]=x}}for(let e=0;e<8;++e){f=a[e];g=a[e+8];p=a[e+16];m=a[e+24];b=a[e+32];y=a[e+40];w=a[e+48];S=a[e+56];if(g|p|m|b|y|w|S){n=za*f+2048>>12;s=za*b+2048>>12;o=p;c=w;l=$a*(g-S)+2048>>12;d=$a*(g+S)+2048>>12;h=m;u=y;n=4112+(n+s+1>>1);s=n-s;x=o*Wa+c*Ha+2048>>12;o=o*Ha-c*Wa+2048>>12;c=x;l=l+u+1>>1;u=l-u;d=d+h+1>>1;h=d-h;n=n+c+1>>1;c=n-c;s=s+o+1>>1;o=s-o;x=l*qa+d*Xa+2048>>12;l=l*Xa-d*qa+2048>>12;d=x;x=h*Ua+u*_a+2048>>12;h=h*_a-u*Ua+2048>>12;u=x;f=n+d;S=n-d;g=s+u;w=s-u;p=o+h;y=o-h;m=c+l;b=c-l;f<16?f=0:f>=4080?f=255:f>>=4;g<16?g=0:g>=4080?g=255:g>>=4;p<16?p=0:p>=4080?p=255:p>>=4;m<16?m=0:m>=4080?m=255:m>>=4;b<16?b=0:b>=4080?b=255:b>>=4;y<16?y=0:y>=4080?y=255:y>>=4;w<16?w=0:w>=4080?w=255:w>>=4;S<16?S=0:S>=4080?S=255:S>>=4;i[t+e]=f;i[t+e+8]=g;i[t+e+16]=p;i[t+e+24]=m;i[t+e+32]=b;i[t+e+40]=y;i[t+e+48]=w;i[t+e+56]=S}else{x=za*f+8192>>14;x=x<-2040?0:x>=2024?255:x+2056>>4;i[t+e]=x;i[t+e+8]=x;i[t+e+16]=x;i[t+e+24]=x;i[t+e+32]=x;i[t+e+40]=x;i[t+e+48]=x;i[t+e+56]=x}}}function buildComponentData(e,t){const a=t.blocksPerLine,r=t.blocksPerColumn,i=new Int16Array(64);for(let e=0;e=r)return null;const n=readUint16(e,t);if(n>=65472&&n<=65534)return{invalid:null,marker:n,offset:t};let s=readUint16(e,i);for(;!(s>=65472&&s<=65534);){if(++i>=r)return null;s=readUint16(e,i)}return{invalid:n.toString(16),marker:s,offset:i}}function prepareComponents(e){const t=Math.ceil(e.samplesPerLine/8/e.maxH),a=Math.ceil(e.scanLines/8/e.maxV);for(const r of e.components){const i=Math.ceil(Math.ceil(e.samplesPerLine/8)*r.h/e.maxH),n=Math.ceil(Math.ceil(e.scanLines/8)*r.v/e.maxV),s=t*r.h,o=64*(a*r.v)*(s+1);r.blockData=new Int16Array(o);r.blocksPerLine=i;r.blocksPerColumn=n}e.mcusPerLine=t;e.mcusPerColumn=a}function readDataBlock(e,t){const a=readUint16(e,t);let r=(t+=2)+a-2;const i=findNextFileMarker(e,r,t);if(i?.invalid){warn("readDataBlock - incorrect length, current marker is: "+i.invalid);r=i.offset}const n=e.subarray(t,r);return{appData:n,oldOffset:t,newOffset:t+n.length}}function skipData(e,t){const a=readUint16(e,t),r=(t+=2)+a-2,i=findNextFileMarker(e,r,t);return i?.invalid?i.offset:r}class JpegImage{constructor({decodeTransform:e=null,colorTransform:t=-1}={}){this._decodeTransform=e;this._colorTransform=t}static canUseImageDecoder(e,t=-1){let a=null,r=0,i=null,n=readUint16(e,r);r+=2;if(65496!==n)throw new JpegError("SOI not found");n=readUint16(e,r);r+=2;e:for(;65497!==n;){switch(n){case 65505:const{appData:t,oldOffset:s,newOffset:o}=readDataBlock(e,r);r=o;if(69===t[0]&&120===t[1]&&105===t[2]&&102===t[3]&&0===t[4]&&0===t[5]){if(a)throw new JpegError("Duplicate EXIF-blocks found.");a={exifStart:s+6,exifEnd:o}}n=readUint16(e,r);r+=2;continue;case 65472:case 65473:case 65474:i=e[r+7];break e;case 65535:255!==e[r]&&r--}r=skipData(e,r);n=readUint16(e,r);r+=2}return 4===i||3===i&&0===t?null:a||{}}parse(e,{dnlScanLines:t=null}={}){let a,r,i=0,n=null,s=null,o=0;const c=[],l=[],h=[];let u=readUint16(e,i);i+=2;if(65496!==u)throw new JpegError("SOI not found");u=readUint16(e,i);i+=2;e:for(;65497!==u;){let d,f,g;switch(u){case 65504:case 65505:case 65506:case 65507:case 65508:case 65509:case 65510:case 65511:case 65512:case 65513:case 65514:case 65515:case 65516:case 65517:case 65518:case 65519:case 65534:const{appData:p,newOffset:m}=readDataBlock(e,i);i=m;65504===u&&74===p[0]&&70===p[1]&&73===p[2]&&70===p[3]&&0===p[4]&&(n={version:{major:p[5],minor:p[6]},densityUnits:p[7],xDensity:p[8]<<8|p[9],yDensity:p[10]<<8|p[11],thumbWidth:p[12],thumbHeight:p[13],thumbData:p.subarray(14,14+3*p[12]*p[13])});65518===u&&65===p[0]&&100===p[1]&&111===p[2]&&98===p[3]&&101===p[4]&&(s={version:p[5]<<8|p[6],flags0:p[7]<<8|p[8],flags1:p[9]<<8|p[10],transformCode:p[11]});break;case 65499:const b=readUint16(e,i);i+=2;const y=b+i-2;let w;for(;i>4){if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(f=0;f<64;f++){w=ja[f];a[w]=readUint16(e,i);i+=2}}else for(f=0;f<64;f++){w=ja[f];a[w]=e[i++]}c[15&t]=a}break;case 65472:case 65473:case 65474:if(a)throw new JpegError("Only single frame JPEGs supported");i+=2;a={};a.extended=65473===u;a.progressive=65474===u;a.precision=e[i++];const S=readUint16(e,i);i+=2;a.scanLines=t||S;a.samplesPerLine=readUint16(e,i);i+=2;a.components=[];a.componentIds={};const x=e[i++];let k=0,C=0;for(d=0;d>4,n=15&e[i+1];k>4?l:h)[15&t]=buildHuffmanTable(a,n)}break;case 65501:i+=2;r=readUint16(e,i);i+=2;break;case 65498:const F=1==++o&&!t;i+=2;const O=e[i++],T=[];for(d=0;d>4];n.huffmanTableAC=l[15&s];T.push(n)}const M=e[i++],D=e[i++],R=e[i++];try{i+=decodeScan(e,i,a,T,r,M,D,R>>4,15&R,F)}catch(t){if(t instanceof DNLMarkerError){warn(`${t.message} -- attempting to re-parse the JPEG image.`);return this.parse(e,{dnlScanLines:t.scanLines})}if(t instanceof EOIMarkerError){warn(`${t.message} -- ignoring the rest of the image data.`);break e}throw t}break;case 65500:i+=4;break;case 65535:255!==e[i]&&i--;break;default:const N=findNextFileMarker(e,i-2,i-3);if(N?.invalid){warn("JpegImage.parse - unexpected data, current marker is: "+N.invalid);i=N.offset;break}if(!N||i>=e.length-1){warn("JpegImage.parse - reached the end of the image data without finding an EOI marker (0xFFD9).");break e}throw new JpegError("JpegImage.parse - unknown marker: "+u.toString(16))}u=readUint16(e,i);i+=2}if(!a)throw new JpegError("JpegImage.parse - no frame data found.");this.width=a.samplesPerLine;this.height=a.scanLines;this.jfif=n;this.adobe=s;this.components=[];for(const e of a.components){const t=c[e.quantizationId];t&&(e.quantizationTable=t);this.components.push({index:e.index,output:buildComponentData(0,e),scaleX:e.h/a.maxH,scaleY:e.v/a.maxV,blocksPerLine:e.blocksPerLine,blocksPerColumn:e.blocksPerColumn})}this.numComponents=this.components.length}_getLinearizedBlockData(e,t,a=!1){const r=this.width/e,i=this.height/t;let n,s,o,c,l,h,u,d,f,g,p,m=0;const b=this.components.length,y=e*t*b,w=new Uint8ClampedArray(y),S=new Uint32Array(e),x=4294967288;let k;for(u=0;u>8)+C[f+1];return w}get _isColorConversionNeeded(){return this.adobe?!!this.adobe.transformCode:3===this.numComponents?0!==this._colorTransform&&(82!==this.components[0].index||71!==this.components[1].index||66!==this.components[2].index):1===this._colorTransform}_convertYccToRgb(e){let t,a,r;for(let i=0,n=e.length;i4)throw new JpegError("Unsupported color mode");const n=this._getLinearizedBlockData(e,t,i);if(1===this.numComponents&&(a||r)){const e=n.length*(a?4:3),t=new Uint8ClampedArray(e);let r=0;if(a)!function grayToRGBA(e,t){if(FeatureTest.isLittleEndian)for(let a=0,r=e.length;a0&&(e=e.subarray(t));break}return e}decodeImage(e){if(this.eof)return this.buffer;e=this.#N(e||this.bytes);const t=new JpegImage(this.jpegOptions);t.parse(e);const a=t.getData({width:this.drawWidth,height:this.drawHeight,forceRGBA:this.forceRGBA,forceRGB:this.forceRGB,isSourcePDF:!0});this.buffer=a;this.bufferLength=a.length;this.eof=!0;return this.buffer}get canAsyncDecodeImageFromBuffer(){return this.stream.isAsync}async getTransferableImage(){if(!await JpegStream.canUseImageDecoder)return null;const e=this.jpegOptions;if(e.decodeTransform)return null;let t;try{const a=this.canAsyncDecodeImageFromBuffer&&await this.stream.asyncGetBytes()||this.bytes;if(!a)return null;let r=this.#N(a);const i=JpegImage.canUseImageDecoder(r,e.colorTransform);if(!i)return null;if(i.exifStart){r=r.slice();r.fill(0,i.exifStart,i.exifEnd)}t=new ImageDecoder({data:r,type:"image/jpeg",preferAnimation:!1});return(await t.decode()).image}catch(e){warn(`getTransferableImage - failed: "${e}".`);return null}finally{t?.close()}}}var OpenJPEG=async function(e={}){var t,a,r=e,i=new Promise(((e,r)=>{t=e;a=r})),n="./this.program",quit_=(e,t)=>{throw t},s=import.meta.url;try{new URL(".",s).href}catch{}var o,c,l,h,u,d,f=console.log.bind(console),g=console.error.bind(console),p=!1;function updateMemoryViews(){var e=o.buffer;l=new Int8Array(e);new Int16Array(e);h=new Uint8Array(e);new Uint16Array(e);u=new Int32Array(e);d=new Uint32Array(e);new Float32Array(e);new Float64Array(e);new BigInt64Array(e);new BigUint64Array(e)}var m=0,b=null;class ExitStatus{name="ExitStatus";constructor(e){this.message=`Program terminated with exit(${e})`;this.status=e}}var callRuntimeCallbacks=e=>{for(;e.length>0;)e.shift()(r)},y=[],addOnPostRun=e=>y.push(e),w=[],addOnPreRun=e=>w.push(e),S=!0,x=0,k={},handleException=e=>{if(e instanceof ExitStatus||"unwind"==e)return c;quit_(0,e)},keepRuntimeAlive=()=>S||x>0,_proc_exit=e=>{c=e;if(!keepRuntimeAlive()){r.onExit?.(e);p=!0}quit_(0,new ExitStatus(e))},_exit=(e,t)=>{c=e;_proc_exit(e)},callUserCallback=e=>{if(!p)try{e();(()=>{if(!keepRuntimeAlive())try{_exit(c)}catch(e){handleException(e)}})()}catch(e){handleException(e)}},growMemory=e=>{var t=(e-o.buffer.byteLength+65535)/65536|0;try{o.grow(t);updateMemoryViews();return 1}catch(e){}},C={},getEnvStrings=()=>{if(!getEnvStrings.strings){var e={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:n||"./this.program"};for(var t in C)void 0===C[t]?delete e[t]:e[t]=C[t];var a=[];for(var t in e)a.push(`${t}=${e[t]}`);getEnvStrings.strings=a}return getEnvStrings.strings},lengthBytesUTF8=e=>{for(var t=0,a=0;a=55296&&r<=57343){t+=4;++a}else t+=3}return t},v=[null,[],[]],F="undefined"!=typeof TextDecoder?new TextDecoder:void 0,UTF8ArrayToString=(e,t=0,a=NaN)=>{for(var r=t+a,i=t;e[i]&&!(i>=r);)++i;if(i-t>16&&e.buffer&&F)return F.decode(e.subarray(t,i));for(var n="";t>10,56320|1023&l)}}else n+=String.fromCharCode((31&s)<<6|o)}else n+=String.fromCharCode(s)}return n},printChar=(e,t)=>{var a=v[e];if(0===t||10===t){(1===e?f:g)(UTF8ArrayToString(a));a.length=0}else a.push(t)},UTF8ToString=(e,t)=>e?UTF8ArrayToString(h,e,t):"";r.noExitRuntime&&(S=r.noExitRuntime);r.print&&(f=r.print);r.printErr&&(g=r.printErr);r.wasmBinary&&r.wasmBinary;r.arguments&&r.arguments;r.thisProgram&&(n=r.thisProgram);r.writeArrayToMemory=(e,t)=>{l.set(e,t)};var O={l:()=>function abort(e){r.onAbort?.(e);g(e="Aborted("+e+")");p=!0;e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);a(t);throw t}(""),k:()=>{S=!1;x=0},m:(e,t)=>{if(k[e]){clearTimeout(k[e].id);delete k[e]}if(!t)return 0;var a=setTimeout((()=>{delete k[e];callUserCallback((()=>M(e,performance.now())))}),t);k[e]={id:a,timeout_ms:t};return 0},g:function _copy_pixels_1(e,t){e>>=2;const a=r.imageData=new Uint8ClampedArray(t),i=u.subarray(e,e+t);a.set(i)},f:function _copy_pixels_3(e,t,a,i){e>>=2;t>>=2;a>>=2;const n=r.imageData=new Uint8ClampedArray(3*i),s=u.subarray(e,e+i),o=u.subarray(t,t+i),c=u.subarray(a,a+i);for(let e=0;e>=2;t>>=2;a>>=2;i>>=2;const s=r.imageData=new Uint8ClampedArray(4*n),o=u.subarray(e,e+n),c=u.subarray(t,t+n),l=u.subarray(a,a+n),h=u.subarray(i,i+n);for(let e=0;e{var t,a,r=h.length,i=2147483648;if((e>>>=0)>i)return!1;for(var n=1;n<=4;n*=2){var s=r*(1+.2/n);s=Math.min(s,e+100663296);var o=Math.min(i,(t=Math.max(e,s),a=65536,Math.ceil(t/a)*a));if(growMemory(o))return!0}return!1},p:(e,t)=>{var a=0,r=0;for(var i of getEnvStrings()){var n=t+a;d[e+r>>2]=n;a+=((e,t,a,r)=>{if(!(r>0))return 0;for(var i=a,n=a+r-1,s=0;s=55296&&o<=57343&&(o=65536+((1023&o)<<10)|1023&e.charCodeAt(++s));if(o<=127){if(a>=n)break;t[a++]=o}else if(o<=2047){if(a+1>=n)break;t[a++]=192|o>>6;t[a++]=128|63&o}else if(o<=65535){if(a+2>=n)break;t[a++]=224|o>>12;t[a++]=128|o>>6&63;t[a++]=128|63&o}else{if(a+3>=n)break;t[a++]=240|o>>18;t[a++]=128|o>>12&63;t[a++]=128|o>>6&63;t[a++]=128|63&o}}t[a]=0;return a-i})(i,h,n,1/0)+1;r+=4}return 0},q:(e,t)=>{var a=getEnvStrings();d[e>>2]=a.length;var r=0;for(var i of a)r+=lengthBytesUTF8(i)+1;d[t>>2]=r;return 0},b:e=>52,o:function _fd_seek(e,t,a,r){t=(i=t)<-9007199254740992||i>9007199254740992?NaN:Number(i);var i;return 70},c:(e,t,a,r)=>{for(var i=0,n=0;n>2],o=d[t+4>>2];t+=8;for(var c=0;c>2]=i;return 0},r:function _gray_to_rgba(e,t){e>>=2;const a=r.imageData=new Uint8ClampedArray(4*t),i=u.subarray(e,e+t);for(let e=0;e>=2;t>>=2;const i=r.imageData=new Uint8ClampedArray(4*a),n=u.subarray(e,e+a),s=u.subarray(t,t+a);for(let e=0;e>=2;t>>=2;a>>=2;const n=r.imageData=new Uint8ClampedArray(4*i),s=u.subarray(e,e+i),o=u.subarray(t,t+i),c=u.subarray(a,a+i);for(let e=0;e{r.instantiateWasm(e,((e,a)=>{t(receiveInstance(e))}))}))}(),M=(T.t,r._malloc=T.u,r._free=T.v,r._jp2_decode=T.w,T.x);!function preInit(){if(r.preInit){"function"==typeof r.preInit&&(r.preInit=[r.preInit]);for(;r.preInit.length>0;)r.preInit.shift()()}}();!function run(){if(m>0)b=run;else{!function preRun(){if(r.preRun){"function"==typeof r.preRun&&(r.preRun=[r.preRun]);for(;r.preRun.length;)addOnPreRun(r.preRun.shift())}callRuntimeCallbacks(w)}();if(m>0)b=run;else if(r.setStatus){r.setStatus("Running...");setTimeout((()=>{setTimeout((()=>r.setStatus("")),1);doRun()}),1)}else doRun()}function doRun(){r.calledRun=!0;if(!p){!function initRuntime(){T.t()}();t(r);r.onRuntimeInitialized?.();!function postRun(){if(r.postRun){"function"==typeof r.postRun&&(r.postRun=[r.postRun]);for(;r.postRun.length;)addOnPostRun(r.postRun.shift())}callRuntimeCallbacks(y)}()}}}();return i};const Ga=OpenJPEG;class JpxError extends fa{constructor(e){super(e,"JpxError")}}class JpxImage{static#E=null;static#P=null;static#L=null;static#v=!0;static#j=!0;static#F=null;static setOptions({handler:e,useWasm:t,useWorkerFetch:a,wasmUrl:r}){this.#v=t;this.#j=a;this.#F=r;a||(this.#P=e)}static async#_(e){const t=`${this.#F}openjpeg_nowasm_fallback.js`;let a=null;try{a=(await import( -/*webpackIgnore: true*/ -/*@vite-ignore*/ -t)).default()}catch(e){warn(`JpxImage#getJsModule: ${e}`)}e(a)}static async#U(e,t,a){const r="openjpeg.wasm";try{this.#E||(this.#j?this.#E=await fetchBinaryData(`${this.#F}${r}`):this.#E=await this.#P.sendWithPromise("FetchBinaryData",{type:"wasmFactory",filename:r}));return a((await WebAssembly.instantiate(this.#E,t)).instance)}catch(t){warn(`JpxImage#instantiateWasm: ${t}`);this.#_(e);return null}finally{this.#P=null}}static async decode(e,{numComponents:t=4,isIndexedColormap:a=!1,smaskInData:r=!1,reducePower:i=0}={}){if(!this.#L){const{promise:e,resolve:t}=Promise.withResolvers(),a=[e];this.#v?a.push(Ga({warn,instantiateWasm:this.#U.bind(this,t)})):this.#_(t);this.#L=Promise.race(a)}const n=await this.#L;if(!n)throw new JpxError("OpenJPEG failed to initialize");let s;try{const o=e.length;s=n._malloc(o);n.writeArrayToMemory(e,s);if(n._jp2_decode(s,o,t>0?t:0,!!a,!!r,i)){const{errorMessages:e}=n;if(e){delete n.errorMessages;throw new JpxError(e)}throw new JpxError("Unknown error")}const{imageData:c}=n;n.imageData=null;return c}finally{s&&n._free(s)}}static cleanup(){this.#L=null}static parseImageProperties(e){let t=e.getByte();for(;t>=0;){const a=t;t=e.getByte();if(65361===(a<<8|t)){e.skip(4);const t=e.getInt32()>>>0,a=e.getInt32()>>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0;e.skip(16);return{width:t-r,height:a-i,bitsPerComponent:8,componentsCount:e.getUint16()}}}throw new JpxError("No size marker found in JPX stream")}}function addState(e,t,a,r,i){let n=e;for(let e=0,a=t.length-1;e1e3){l=Math.max(l,d);f+=u+2;d=0;u=0}h.push({transform:t,x:d,y:f,w:a.width,h:a.height});d+=a.width+2;u=Math.max(u,a.height)}const g=Math.max(l,d)+1,p=f+u+1,m=new Uint8Array(g*p*4),b=g<<2;for(let e=0;e=0;){t[n-4]=t[n];t[n-3]=t[n+1];t[n-2]=t[n+2];t[n-1]=t[n+3];t[n+a]=t[n+a-4];t[n+a+1]=t[n+a-3];t[n+a+2]=t[n+a-2];t[n+a+3]=t[n+a-1];n-=b}}const y={width:g,height:p};if(e.isOffscreenCanvasSupported){const e=new OffscreenCanvas(g,p);e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(m.buffer),g,p),0,0);y.bitmap=e.transferToImageBitmap();y.data=null}else{y.kind=v;y.data=m}a.splice(n,4*c,Zt);r.splice(n,4*c,[y,h]);return n+1}));addState(Va,[Be,Ne,Vt,Re],null,(function iterateImageMaskGroup(e,t){const a=e.fnArray,r=(t-(e.iCurr-3))%4;switch(r){case 0:return a[t]===Be;case 1:return a[t]===Ne;case 2:return a[t]===Vt;case 3:return a[t]===Re}throw new Error(`iterateImageMaskGroup - invalid pos: ${r}`)}),(function foundImageMaskGroup(e,t){const a=e.fnArray,r=e.argsArray,i=e.iCurr,n=i-3,s=i-2,o=i-1;let c=Math.floor((t-n)/4);if(c<10)return t-(t-n)%4;let l,h,u=!1;const d=r[o][0],f=r[s][0],g=r[s][1],p=r[s][2],m=r[s][3];if(g===p){u=!0;l=s+4;let e=o+4;for(let t=1;t=4&&a[n-4]===a[s]&&a[n-3]===a[o]&&a[n-2]===a[c]&&a[n-1]===a[l]&&r[n-4][0]===h&&r[n-4][1]===u){d++;f-=5}let g=f+4;for(let e=1;e{const t=e.argsArray,a=t[e.iCurr-1][0];if(a!==qe&&a!==He&&a!==$e&&a!==Ge&&a!==Ve&&a!==Ke)return!0;const r=t[e.iCurr-2];return 1===r[0]&&0===r[1]&&0===r[2]&&1===r[3]}),(()=>!1),((e,t)=>{const{fnArray:a,argsArray:r}=e,i=e.iCurr,n=i-3,s=i-2,o=r[i-1],c=r[s],[,[l],h]=o;if(h){Util.scaleMinMax(c,h);for(let e=0,t=l.length;e=a)break}r=(r||Va)[e[t]];if(r&&!Array.isArray(r)){n.iCurr=t;t++;if(!r.checkFn||(0,r.checkFn)(n)){i=r;r=null}else r=null}else t++}this.state=r;this.match=i;this.lastProcessed=t}flush(){for(;this.match;){const e=this.queue.fnArray.length;this.lastProcessed=(0,this.match.processFn)(this.context,e);this.match=null;this.state=null;this._optimize()}}reset(){this.state=null;this.match=null;this.lastProcessed=0}}class OperatorList{static CHUNK_SIZE=1e3;static CHUNK_SIZE_ABOUT=this.CHUNK_SIZE-5;static isOffscreenCanvasSupported=!1;constructor(e=0,t){this._streamSink=t;this.fnArray=[];this.argsArray=[];this.optimizer=!t||e&d?new NullOptimizer(this):new QueueOptimizer(this);this.dependencies=new Set;this._totalLength=0;this.weight=0;this._resolved=t?null:Promise.resolve()}static setOptions({isOffscreenCanvasSupported:e}){this.isOffscreenCanvasSupported=e}get length(){return this.argsArray.length}get ready(){return this._resolved||this._streamSink.ready}get totalLength(){return this._totalLength+this.length}addOp(e,t){this.optimizer.push(e,t);this.weight++;this._streamSink&&(this.weight>=OperatorList.CHUNK_SIZE||this.weight>=OperatorList.CHUNK_SIZE_ABOUT&&(e===Re||e===et))&&this.flush()}addImageOps(e,t,a,r=!1){if(r){this.addOp(Be);this.addOp(De,[[["SMask",!1]]])}void 0!==a&&this.addOp(jt,["OC",a]);this.addOp(e,t);void 0!==a&&this.addOp(_t,[]);r&&this.addOp(Re)}addDependency(e){if(!this.dependencies.has(e)){this.dependencies.add(e);this.addOp(ke,[e])}}addDependencies(e){for(const t of e)this.addDependency(t)}addOpList(e){if(e instanceof OperatorList){for(const t of e.dependencies)this.dependencies.add(t);for(let t=0,a=e.length;t>>0}function hexToStr(e,t){return 1===t?String.fromCharCode(e[0],e[1]):3===t?String.fromCharCode(e[0],e[1],e[2],e[3]):String.fromCharCode(...e.subarray(0,t+1))}function addHex(e,t,a){let r=0;for(let i=a;i>=0;i--){r+=e[i]+t[i];e[i]=255&r;r>>=8}}function incHex(e,t){let a=1;for(let r=t;r>=0&&a>0;r--){a+=e[r];e[r]=255&a;a>>=8}}const Ka=16;class BinaryCMapStream{constructor(e){this.buffer=e;this.pos=0;this.end=e.length;this.tmpBuf=new Uint8Array(19)}readByte(){return this.pos>=this.end?-1:this.buffer[this.pos++]}readNumber(){let e,t=0;do{const a=this.readByte();if(a<0)throw new FormatError("unexpected EOF in bcmap");e=!(128&a);t=t<<7|127&a}while(!e);return t}readSigned(){const e=this.readNumber();return 1&e?~(e>>>1):e>>>1}readHex(e,t){e.set(this.buffer.subarray(this.pos,this.pos+t+1));this.pos+=t+1}readHexNumber(e,t){let a;const r=this.tmpBuf;let i=0;do{const e=this.readByte();if(e<0)throw new FormatError("unexpected EOF in bcmap");a=!(128&e);r[i++]=127&e}while(!a);let n=t,s=0,o=0;for(;n>=0;){for(;o<8&&r.length>0;){s|=r[--i]<>=8;o-=8}}readHexSigned(e,t){this.readHexNumber(e,t);const a=1&e[t]?255:0;let r=0;for(let i=0;i<=t;i++){r=(1&r)<<8|e[i];e[i]=r>>1^a}}readString(){const e=this.readNumber(),t=new Array(e);for(let a=0;a=0;){const e=d>>5;if(7===e){switch(31&d){case 0:r.readString();break;case 1:n=r.readString()}continue}const a=!!(16&d),i=15&d;if(i+1>Ka)throw new Error("BinaryCMapReader.process: Invalid dataSize.");const f=1,g=r.readNumber();switch(e){case 0:r.readHex(s,i);r.readHexNumber(o,i);addHex(o,s,i);t.addCodespaceRange(i+1,hexToInt(s,i),hexToInt(o,i));for(let e=1;e=0;--i){r[a+i]=255&s;s>>=8}}}}class AsciiHexStream extends DecodeStream{constructor(e,t){t&&(t*=.5);super(t);this.str=e;this.dict=e.dict;this.firstDigit=-1}readBlock(){const e=this.str.getBytes(8e3);if(!e.length){this.eof=!0;return}const t=e.length+1>>1,a=this.ensureBuffer(this.bufferLength+t);let r=this.bufferLength,i=this.firstDigit;for(const t of e){let e;if(t>=48&&t<=57)e=15&t;else{if(!(t>=65&&t<=70||t>=97&&t<=102)){if(62===t){this.eof=!0;break}continue}e=9+(15&t)}if(i<0)i=e;else{a[r++]=i<<4|e;i=-1}}if(i>=0&&this.eof){a[r++]=i<<4;i=-1}this.firstDigit=i;this.bufferLength=r}}const Ja=-1,Ya=[[-1,-1],[-1,-1],[7,8],[7,7],[6,6],[6,6],[6,5],[6,5],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[4,0],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[3,3],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2],[1,2]],Za=[[-1,-1],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[12,1984],[12,2048],[12,2112],[12,2176],[12,2240],[12,2304],[11,1856],[11,1856],[11,1920],[11,1920],[12,2368],[12,2432],[12,2496],[12,2560]],Qa=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[8,29],[8,29],[8,30],[8,30],[8,45],[8,45],[8,46],[8,46],[7,22],[7,22],[7,22],[7,22],[7,23],[7,23],[7,23],[7,23],[8,47],[8,47],[8,48],[8,48],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[6,13],[7,20],[7,20],[7,20],[7,20],[8,33],[8,33],[8,34],[8,34],[8,35],[8,35],[8,36],[8,36],[8,37],[8,37],[8,38],[8,38],[7,19],[7,19],[7,19],[7,19],[8,31],[8,31],[8,32],[8,32],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,1],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[6,12],[8,53],[8,53],[8,54],[8,54],[7,26],[7,26],[7,26],[7,26],[8,39],[8,39],[8,40],[8,40],[8,41],[8,41],[8,42],[8,42],[8,43],[8,43],[8,44],[8,44],[7,21],[7,21],[7,21],[7,21],[7,28],[7,28],[7,28],[7,28],[8,61],[8,61],[8,62],[8,62],[8,63],[8,63],[8,0],[8,0],[8,320],[8,320],[8,384],[8,384],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[5,11],[7,27],[7,27],[7,27],[7,27],[8,59],[8,59],[8,60],[8,60],[9,1472],[9,1536],[9,1600],[9,1728],[7,18],[7,18],[7,18],[7,18],[7,24],[7,24],[7,24],[7,24],[8,49],[8,49],[8,50],[8,50],[8,51],[8,51],[8,52],[8,52],[7,25],[7,25],[7,25],[7,25],[8,55],[8,55],[8,56],[8,56],[8,57],[8,57],[8,58],[8,58],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,192],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[6,1664],[8,448],[8,448],[8,512],[8,512],[9,704],[9,768],[8,640],[8,640],[8,576],[8,576],[9,832],[9,896],[9,960],[9,1024],[9,1088],[9,1152],[9,1216],[9,1280],[9,1344],[9,1408],[7,256],[7,256],[7,256],[7,256],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,2],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[4,3],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,128],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,8],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[5,9],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,16],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[6,17],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,4],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[4,5],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,14],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[6,15],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[5,64],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,6],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7],[4,7]],er=[[-1,-1],[-1,-1],[12,-2],[12,-2],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[-1,-1],[11,1792],[11,1792],[11,1792],[11,1792],[12,1984],[12,1984],[12,2048],[12,2048],[12,2112],[12,2112],[12,2176],[12,2176],[12,2240],[12,2240],[12,2304],[12,2304],[11,1856],[11,1856],[11,1856],[11,1856],[11,1920],[11,1920],[11,1920],[11,1920],[12,2368],[12,2368],[12,2432],[12,2432],[12,2496],[12,2496],[12,2560],[12,2560],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[10,18],[12,52],[12,52],[13,640],[13,704],[13,768],[13,832],[12,55],[12,55],[12,56],[12,56],[13,1280],[13,1344],[13,1408],[13,1472],[12,59],[12,59],[12,60],[12,60],[13,1536],[13,1600],[11,24],[11,24],[11,24],[11,24],[11,25],[11,25],[11,25],[11,25],[13,1664],[13,1728],[12,320],[12,320],[12,384],[12,384],[12,448],[12,448],[13,512],[13,576],[12,53],[12,53],[12,54],[12,54],[13,896],[13,960],[13,1024],[13,1088],[13,1152],[13,1216],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64],[10,64]],tr=[[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[8,13],[11,23],[11,23],[12,50],[12,51],[12,44],[12,45],[12,46],[12,47],[12,57],[12,58],[12,61],[12,256],[10,16],[10,16],[10,16],[10,16],[10,17],[10,17],[10,17],[10,17],[12,48],[12,49],[12,62],[12,63],[12,30],[12,31],[12,32],[12,33],[12,40],[12,41],[11,22],[11,22],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[8,14],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,10],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[7,11],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[9,15],[12,128],[12,192],[12,26],[12,27],[12,28],[12,29],[11,19],[11,19],[11,20],[11,20],[12,34],[12,35],[12,36],[12,37],[12,38],[12,39],[11,21],[11,21],[12,42],[12,43],[10,0],[10,0],[10,0],[10,0],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12],[7,12]],ar=[[-1,-1],[-1,-1],[-1,-1],[-1,-1],[6,9],[6,8],[5,7],[5,7],[4,6],[4,6],[4,6],[4,6],[4,5],[4,5],[4,5],[4,5],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[3,4],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,3],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2],[2,2]];class CCITTFaxDecoder{constructor(e,t={}){if("function"!=typeof e?.next)throw new Error('CCITTFaxDecoder - invalid "source" parameter.');this.source=e;this.eof=!1;this.encoding=t.K||0;this.eoline=t.EndOfLine||!1;this.byteAlign=t.EncodedByteAlign||!1;this.columns=t.Columns||1728;this.rows=t.Rows||0;this.eoblock=t.EndOfBlock??!0;this.black=t.BlackIs1||!1;this.codingLine=new Uint32Array(this.columns+1);this.refLine=new Uint32Array(this.columns+2);this.codingLine[0]=this.columns;this.codingPos=0;this.row=0;this.nextLine2D=this.encoding<0;this.inputBits=0;this.inputBuf=0;this.outputBits=0;this.rowsDone=!1;let a;for(;0===(a=this._lookBits(12));)this._eatBits(1);1===a&&this._eatBits(12);if(this.encoding>0){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}}readNextChar(){if(this.eof)return-1;const e=this.refLine,t=this.codingLine,a=this.columns;let r,i,n,s,o;if(0===this.outputBits){this.rowsDone&&(this.eof=!0);if(this.eof)return-1;this.err=!1;let n,o,c;if(this.nextLine2D){for(s=0;t[s]=64);do{o+=c=this._getWhiteCode()}while(c>=64)}else{do{n+=c=this._getWhiteCode()}while(c>=64);do{o+=c=this._getBlackCode()}while(c>=64)}this._addPixels(t[this.codingPos]+n,i);t[this.codingPos]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]0?--r:++r;for(;e[r]<=t[this.codingPos]&&e[r]=64);else do{n+=c=this._getWhiteCode()}while(c>=64);this._addPixels(t[this.codingPos]+n,i);i^=1}}let l=!1;this.byteAlign&&(this.inputBits&=-8);if(this.eoblock||this.row!==this.rows-1){n=this._lookBits(12);if(this.eoline)for(;n!==Ja&&1!==n;){this._eatBits(1);n=this._lookBits(12)}else for(;0===n;){this._eatBits(1);n=this._lookBits(12)}if(1===n){this._eatBits(12);l=!0}else n===Ja&&(this.eof=!0)}else this.rowsDone=!0;if(!this.eof&&this.encoding>0&&!this.rowsDone){this.nextLine2D=!this._lookBits(1);this._eatBits(1)}if(this.eoblock&&l&&this.byteAlign){n=this._lookBits(12);if(1===n){this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}if(this.encoding>=0)for(s=0;s<4;++s){n=this._lookBits(12);1!==n&&info("bad rtc code: "+n);this._eatBits(12);if(this.encoding>0){this._lookBits(1);this._eatBits(1)}}this.eof=!0}}else if(this.err&&this.eoline){for(;;){n=this._lookBits(13);if(n===Ja){this.eof=!0;return-1}if(n>>1==1)break;this._eatBits(1)}this._eatBits(12);if(this.encoding>0){this._eatBits(1);this.nextLine2D=!(1&n)}}this.outputBits=t[0]>0?t[this.codingPos=0]:t[this.codingPos=1];this.row++}if(this.outputBits>=8){o=1&this.codingPos?0:255;this.outputBits-=8;if(0===this.outputBits&&t[this.codingPos]n){o<<=n;1&this.codingPos||(o|=255>>8-n);this.outputBits-=n;n=0}else{o<<=this.outputBits;1&this.codingPos||(o|=255>>8-this.outputBits);n-=this.outputBits;this.outputBits=0;if(t[this.codingPos]0){o<<=n;n=0}}}while(n)}this.black&&(o^=255);return o}_addPixels(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}this.codingPos=r}_addPixelsNeg(e,t){const a=this.codingLine;let r=this.codingPos;if(e>a[r]){if(e>this.columns){info("row is wrong length");this.err=!0;e=this.columns}1&r^t&&++r;a[r]=e}else if(e0&&e=i){const t=a[e-i];if(t[0]===r){this._eatBits(r);return[!0,t[1],!0]}}}return[!1,0,!1]}_getTwoDimCode(){let e,t=0;if(this.eoblock){t=this._lookBits(7);e=Ya[t];if(e?.[0]>0){this._eatBits(e[0]);return e[1]}}else{const e=this._findTableCode(1,7,Ya);if(e[0]&&e[2])return e[1]}info("Bad two dim code");return Ja}_getWhiteCode(){let e,t=0;if(this.eoblock){t=this._lookBits(12);if(t===Ja)return 1;e=t>>5?Qa[t>>3]:Za[t];if(e[0]>0){this._eatBits(e[0]);return e[1]}}else{let e=this._findTableCode(1,9,Qa);if(e[0])return e[1];e=this._findTableCode(11,12,Za);if(e[0])return e[1]}info("bad white code");this._eatBits(1);return 1}_getBlackCode(){let e,t;if(this.eoblock){e=this._lookBits(13);if(e===Ja)return 1;t=e>>7?!(e>>9)&&e>>7?tr[(e>>1)-64]:ar[e>>7]:er[e];if(t[0]>0){this._eatBits(t[0]);return t[1]}}else{let e=this._findTableCode(2,6,ar);if(e[0])return e[1];e=this._findTableCode(7,12,tr,64);if(e[0])return e[1];e=this._findTableCode(10,13,er);if(e[0])return e[1]}info("bad black code");this._eatBits(1);return 1}_lookBits(e){let t;for(;this.inputBits>16-e;this.inputBuf=this.inputBuf<<8|t;this.inputBits+=8}return this.inputBuf>>this.inputBits-e&65535>>16-e}_eatBits(e){(this.inputBits-=e)<0&&(this.inputBits=0)}}class CCITTFaxStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;a instanceof Dict||(a=Dict.empty);const r={next:()=>e.getByte()};this.ccittFaxDecoder=new CCITTFaxDecoder(r,{K:a.get("K"),EndOfLine:a.get("EndOfLine"),EncodedByteAlign:a.get("EncodedByteAlign"),Columns:a.get("Columns"),Rows:a.get("Rows"),EndOfBlock:a.get("EndOfBlock"),BlackIs1:a.get("BlackIs1")})}readBlock(){for(;!this.eof;){const e=this.ccittFaxDecoder.readNextChar();if(-1===e){this.eof=!0;return}this.ensureBuffer(this.bufferLength+1);this.buffer[this.bufferLength++]=e}}}const rr=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),ir=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),nr=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),sr=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],or=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5];class FlateStream extends DecodeStream{constructor(e,t){super(t);this.str=e;this.dict=e.dict;const a=e.getByte(),r=e.getByte();if(-1===a||-1===r)throw new FormatError(`Invalid header in flate stream: ${a}, ${r}`);if(8!=(15&a))throw new FormatError(`Unknown compression method in flate stream: ${a}, ${r}`);if(((a<<8)+r)%31!=0)throw new FormatError(`Bad FCHECK in flate stream: ${a}, ${r}`);if(32&r)throw new FormatError(`FDICT bit set in flate stream: ${a}, ${r}`);this.codeSize=0;this.codeBuf=0}async getImageData(e,t){const a=await this.asyncGetBytes();return a?a.length<=e?a:a.subarray(0,e):this.getBytes(e)}async asyncGetBytes(){this.str.reset();const e=this.str.getBytes();try{const{readable:t,writable:a}=new DecompressionStream("deflate"),r=a.getWriter();await r.ready;r.write(e).then((async()=>{await r.ready;await r.close()})).catch((()=>{}));const i=[];let n=0;for await(const e of t){i.push(e);n+=e.byteLength}const s=new Uint8Array(n);let o=0;for(const e of i){s.set(e,o);o+=e.byteLength}return s}catch{this.str=new Stream(e,2,e.length,this.str.dict);this.reset();return null}}get isAsync(){return!0}getBits(e){const t=this.str;let a,r=this.codeSize,i=this.codeBuf;for(;r>e;this.codeSize=r-=e;return a}getCode(e){const t=this.str,a=e[0],r=e[1];let i,n=this.codeSize,s=this.codeBuf;for(;n>16,l=65535&o;if(c<1||n>c;this.codeSize=n-c;return l}generateHuffmanTable(e){const t=e.length;let a,r=0;for(a=0;ar&&(r=e[a]);const i=1<>=1}for(a=e;a>=1;if(0===t){let t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}let a=t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}a|=t<<8;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}let i=t;if(-1===(t=r.getByte())){this.#X("Bad block header in flate stream");return}i|=t<<8;if(i!==(65535&~a)&&(0!==a||0!==i))throw new FormatError("Bad uncompressed block length in flate stream");this.codeBuf=0;this.codeSize=0;const n=this.bufferLength,s=n+a;e=this.ensureBuffer(s);this.bufferLength=s;if(0===a)-1===r.peekByte()&&(this.eof=!0);else{const t=r.getBytes(a);e.set(t,n);t.length0;)h[o++]=f}i=this.generateHuffmanTable(h.subarray(0,e));n=this.generateHuffmanTable(h.subarray(e,l))}}e=this.buffer;let s=e?e.length:0,o=this.bufferLength;for(;;){let t=this.getCode(i);if(t<256){if(o+1>=s){e=this.ensureBuffer(o+1);s=e.length}e[o++]=t;continue}if(256===t){this.bufferLength=o;return}t-=257;t=ir[t];let r=t>>16;r>0&&(r=this.getBits(r));a=(65535&t)+r;t=this.getCode(n);t=nr[t];r=t>>16;r>0&&(r=this.getBits(r));const c=(65535&t)+r;if(o+a>=s){e=this.ensureBuffer(o+a);s=e.length}for(let t=0;t>9&127;this.clow=this.clow<<7&65535;this.ct-=7;this.a=32768}byteIn(){const e=this.data;let t=this.bp;if(255===e[t])if(e[t+1]>143){this.clow+=65280;this.ct=8}else{t++;this.clow+=e[t]<<9;this.ct=7;this.bp=t}else{t++;this.clow+=t65535){this.chigh+=this.clow>>16;this.clow&=65535}}readBit(e,t){let a=e[t]>>1,r=1&e[t];const i=cr[a],n=i.qe;let s,o=this.a-n;if(this.chigh>15&1;this.clow=this.clow<<1&65535;this.ct--}while(!(32768&o));this.a=o;e[t]=a<<1|r;return s}}class Jbig2Error extends fa{constructor(e){super(e,"Jbig2Error")}}class ContextCache{getContexts(e){return e in this?this[e]:this[e]=new Int8Array(65536)}}class DecodingContext{constructor(e,t,a){this.data=e;this.start=t;this.end=a}get decoder(){return shadow(this,"decoder",new ArithmeticDecoder(this.data,this.start,this.end))}get contextCache(){return shadow(this,"contextCache",new ContextCache)}}function decodeInteger(e,t,a){const r=e.getContexts(t);let i=1;function readBits(e){let t=0;for(let n=0;n>>0}const n=readBits(1),s=readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(1)?readBits(32)+4436:readBits(12)+340:readBits(8)+84:readBits(6)+20:readBits(4)+4:readBits(2);let o;0===n?o=s:s>0&&(o=-s);return o>=-2147483648&&o<=va?o:null}function decodeIAID(e,t,a){const r=e.getContexts("IAID");let i=1;for(let e=0;ee.y-t.y||e.x-t.x));const h=l.length,u=new Int8Array(h),d=new Int8Array(h),f=[];let g,p,m=0,b=0,y=0,w=0;for(p=0;p=v&&E=F){q=q<<1&m;for(p=0;p=0&&j=0){_=D[L][j];_&&(q|=_<=e?l<<=1:l=l<<1|x[o][c]}for(f=0;f=w||c<0||c>=y?l<<=1:l=l<<1|r[o][c]}const g=k.readBit(C,l);t[s]=g}}return x}function decodeTextRegion(e,t,a,r,i,n,s,o,c,l,h,u,d,f,g,p,m,b,y){if(e&&t)throw new Jbig2Error("refinement with Huffman is not supported");const w=[];let S,x;for(S=0;S1&&(i=e?y.readBits(b):decodeInteger(C,"IAIT",k));const n=s*v+i,F=e?f.symbolIDTable.decode(y):decodeIAID(C,k,c),O=t&&(e?y.readBit():decodeInteger(C,"IARI",k));let T=o[F],M=T[0].length,D=T.length;if(O){const e=decodeInteger(C,"IARDW",k),t=decodeInteger(C,"IARDH",k);M+=e;D+=t;T=decodeRefinement(M,D,g,T,(e>>1)+decodeInteger(C,"IARDX",k),(t>>1)+decodeInteger(C,"IARDY",k),!1,p,m)}let R=0;l?1&u?R=D-1:r+=D-1:u>1?r+=M-1:R=M-1;const N=n-(1&u?0:D-1),E=r-(2&u?M-1:0);let L,j,_;if(l)for(L=0;L>5&7;const c=[31&s];let l=t+6;if(7===s){o=536870911&readUint32(e,l-1);l+=3;let t=o+7>>3;c[0]=e[l++];for(;--t>0;)c.push(e[l++])}else if(5===s||6===s)throw new Jbig2Error("invalid referred-to flags");a.retainBits=c;let h=4;a.number<=256?h=1:a.number<=65536&&(h=2);const u=[];let d,f;for(d=0;d>>24&255;n[3]=t.height>>16&255;n[4]=t.height>>8&255;n[5]=255&t.height;for(d=l,f=e.length;d>2&3;e.huffmanDWSelector=t>>4&3;e.bitmapSizeSelector=t>>6&1;e.aggregationInstancesSelector=t>>7&1;e.bitmapCodingContextUsed=!!(256&t);e.bitmapCodingContextRetained=!!(512&t);e.template=t>>10&3;e.refinementTemplate=t>>12&1;l+=2;if(!e.huffman){c=0===e.template?4:1;s=[];for(o=0;o>2&3;h.stripSize=1<>4&3;h.transposed=!!(64&u);h.combinationOperator=u>>7&3;h.defaultPixelValue=u>>9&1;h.dsOffset=u<<17>>27;h.refinementTemplate=u>>15&1;if(h.huffman){const e=readUint16(r,l);l+=2;h.huffmanFS=3&e;h.huffmanDS=e>>2&3;h.huffmanDT=e>>4&3;h.huffmanRefinementDW=e>>6&3;h.huffmanRefinementDH=e>>8&3;h.huffmanRefinementDX=e>>10&3;h.huffmanRefinementDY=e>>12&3;h.huffmanRefinementSizeSelector=!!(16384&e)}if(h.refinement&&!h.refinementTemplate){s=[];for(o=0;o<2;o++){s.push({x:readInt8(r,l),y:readInt8(r,l+1)});l+=2}h.refinementAt=s}h.numberOfSymbolInstances=readUint32(r,l);l+=4;n=[h,a.referredTo,r,l,i];break;case 16:const d={},f=r[l++];d.mmr=!!(1&f);d.template=f>>1&3;d.patternWidth=r[l++];d.patternHeight=r[l++];d.maxPatternIndex=readUint32(r,l);l+=4;n=[d,a.number,r,l,i];break;case 22:case 23:const g={};g.info=readRegionSegmentInformation(r,l);l+=gr;const p=r[l++];g.mmr=!!(1&p);g.template=p>>1&3;g.enableSkip=!!(8&p);g.combinationOperator=p>>4&7;g.defaultPixelValue=p>>7&1;g.gridWidth=readUint32(r,l);l+=4;g.gridHeight=readUint32(r,l);l+=4;g.gridOffsetX=4294967295&readUint32(r,l);l+=4;g.gridOffsetY=4294967295&readUint32(r,l);l+=4;g.gridVectorX=readUint16(r,l);l+=2;g.gridVectorY=readUint16(r,l);l+=2;n=[g,a.referredTo,r,l,i];break;case 38:case 39:const m={};m.info=readRegionSegmentInformation(r,l);l+=gr;const b=r[l++];m.mmr=!!(1&b);m.template=b>>1&3;m.prediction=!!(8&b);if(!m.mmr){c=0===m.template?4:1;s=[];for(o=0;o>2&1;y.combinationOperator=w>>3&3;y.requiresBuffer=!!(32&w);y.combinationOperatorOverride=!!(64&w);n=[y];break;case 49:case 50:case 51:case 62:break;case 53:n=[a.number,r,l,i];break;default:throw new Jbig2Error(`segment type ${a.typeName}(${a.type}) is not implemented`)}const h="on"+a.typeName;h in t&&t[h].apply(t,n)}function processSegments(e,t){for(let a=0,r=e.length;a>3,a=new Uint8ClampedArray(t*e.height);e.defaultPixelValue&&a.fill(255);this.buffer=a}drawBitmap(e,t){const a=this.currentPageInfo,r=e.width,i=e.height,n=a.width+7>>3,s=a.combinationOperatorOverride?e.combinationOperator:a.combinationOperator,o=this.buffer,c=128>>(7&e.x);let l,h,u,d,f=e.y*n+(e.x>>3);switch(s){case 0:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;case 2:for(l=0;l>=1;if(!u){u=128;d++}}f+=n}break;default:throw new Jbig2Error(`operator ${s} is not supported`)}}onImmediateGenericRegion(e,t,a,r){const i=e.info,n=new DecodingContext(t,a,r),s=decodeBitmap(e.mmr,i.width,i.height,e.template,e.prediction,null,e.at,n);this.drawBitmap(i,s)}onImmediateLosslessGenericRegion(){this.onImmediateGenericRegion(...arguments)}onSymbolDictionary(e,t,a,r,i,n){let s,o;if(e.huffman){s=function getSymbolDictionaryHuffmanTables(e,t,a){let r,i,n,s,o=0;switch(e.huffmanDHSelector){case 0:case 1:r=getStandardTable(e.huffmanDHSelector+4);break;case 3:r=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DH selector")}switch(e.huffmanDWSelector){case 0:case 1:i=getStandardTable(e.huffmanDWSelector+2);break;case 3:i=getCustomHuffmanTable(o,t,a);o++;break;default:throw new Jbig2Error("invalid Huffman DW selector")}if(e.bitmapSizeSelector){n=getCustomHuffmanTable(o,t,a);o++}else n=getStandardTable(1);s=e.aggregationInstancesSelector?getCustomHuffmanTable(o,t,a):getStandardTable(1);return{tableDeltaHeight:r,tableDeltaWidth:i,tableBitmapSize:n,tableAggregateInstances:s}}(e,a,this.customTables);o=new Reader(r,i,n)}let c=this.symbols;c||(this.symbols=c={});const l=[];for(const e of a){const t=c[e];t&&l.push(...t)}const h=new DecodingContext(r,i,n);c[t]=function decodeSymbolDictionary(e,t,a,r,i,n,s,o,c,l,h,u){if(e&&t)throw new Jbig2Error("symbol refinement with Huffman is not supported");const d=[];let f=0,g=log2(a.length+r);const p=h.decoder,m=h.contextCache;let b,y;if(e){b=getStandardTable(1);y=[];g=Math.max(g,1)}for(;d.length1)w=decodeTextRegion(e,t,r,f,0,i,1,a.concat(d),g,0,0,1,0,n,c,l,h,0,u);else{const e=decodeIAID(m,p,g),t=decodeInteger(m,"IARDX",p),i=decodeInteger(m,"IARDY",p);w=decodeRefinement(r,f,c,e=32){let a,r,s;switch(t){case 32:if(0===e)throw new Jbig2Error("no previous value in symbol ID table");r=i.readBits(2)+3;a=n[e-1].prefixLength;break;case 33:r=i.readBits(3)+3;a=0;break;case 34:r=i.readBits(7)+11;a=0;break;default:throw new Jbig2Error("invalid code length in symbol ID table")}for(s=0;s=0;m--){T=e?decodeMMRBitmap(O,c,l,!0):decodeBitmap(!1,c,l,a,!1,null,v,g);F[m]=T}for(M=0;M=0;b--){R^=F[b][M][D];N|=R<>8;j=u+M*d-D*f>>8;if(L>=0&&L+x<=r&&j>=0&&j+k<=i)for(m=0;m=i)){U=p[t];_=E[m];for(b=0;b=0&&e>1&7),c=1+(r>>4&7),l=[];let h,u,d=i;do{h=s.readBits(o);u=s.readBits(c);l.push(new HuffmanLine([d,h,u,0]));d+=1<>t&1;if(t<=0)this.children[a]=new HuffmanTreeNode(e);else{let r=this.children[a];r||(this.children[a]=r=new HuffmanTreeNode(null));r.buildTree(e,t-1)}}decodeNode(e){if(this.isLeaf){if(this.isOOB)return null;const t=e.readBits(this.rangeLength);return this.rangeLow+(this.isLowerRange?-t:t)}const t=this.children[e.readBit()];if(!t)throw new Jbig2Error("invalid Huffman data");return t.decodeNode(e)}}class HuffmanTable{constructor(e,t){t||this.assignPrefixCodes(e);this.rootNode=new HuffmanTreeNode(null);for(let t=0,a=e.length;t0&&this.rootNode.buildTree(a,a.prefixLength-1)}}decode(e){return this.rootNode.decodeNode(e)}assignPrefixCodes(e){const t=e.length;let a=0;for(let r=0;r=this.end)throw new Jbig2Error("end of data while reading bit");this.currentByte=this.data[this.position++];this.shift=7}const e=this.currentByte>>this.shift&1;this.shift--;return e}readBits(e){let t,a=0;for(t=e-1;t>=0;t--)a|=this.readBit()<=this.end?-1:this.data[this.position++]}}function getCustomHuffmanTable(e,t,a){let r=0;for(let i=0,n=t.length;i>a&1;a--}}if(r&&!o){const e=5;for(let t=0;t>>t&(1<0;if(e<256){d[0]=e;f=1}else{if(!(e>=258)){if(256===e){h=9;s=258;f=0;continue}this.eof=!0;delete this.lzwState;break}if(e=0;t--){d[t]=o[a];a=l[a]}}else d[f++]=d[0]}if(i){l[s]=u;c[s]=c[u]+1;o[s]=d[0];s++;h=s+n&s+n-1?h:0|Math.min(Math.log(s+n)/.6931471805599453+1,12)}u=e;g+=f;if(r15))throw new FormatError(`Unsupported predictor: ${r}`);this.readBlock=2===r?this.readBlockTiff:this.readBlockPng;this.str=e;this.dict=e.dict;const i=this.colors=a.get("Colors")||1,n=this.bits=a.get("BPC","BitsPerComponent")||8,s=this.columns=a.get("Columns")||1;this.pixBytes=i*n+7>>3;this.rowBytes=s*i*n+7>>3;return this}readBlockTiff(){const e=this.rowBytes,t=this.bufferLength,a=this.ensureBuffer(t+e),r=this.bits,i=this.colors,n=this.str.getBytes(e);this.eof=!n.length;if(this.eof)return;let s,o=0,c=0,l=0,h=0,u=t;if(1===r&&1===i)for(s=0;s>1;e^=e>>2;e^=e>>4;o=(1&e)<<7;a[u++]=e}else if(8===r){for(s=0;s>8&255;a[u++]=255&e}}else{const e=new Uint8Array(i+1),u=(1<>l-r)&u;l-=r;c=c<=8){a[f++]=c>>h-8&255;h-=8}}h>0&&(a[f++]=(c<<8-h)+(o&(1<<8-h)-1))}this.bufferLength+=e}readBlockPng(){const e=this.rowBytes,t=this.pixBytes,a=this.str.getByte(),r=this.str.getBytes(e);this.eof=!r.length;if(this.eof)return;const i=this.bufferLength,n=this.ensureBuffer(i+e);let s=n.subarray(i-e,i);0===s.length&&(s=new Uint8Array(e));let o,c,l,h=i;switch(a){case 0:for(o=0;o>1)+r[o];for(;o>1)+r[o]&255;h++}break;case 4:for(o=0;o0){const e=this.str.getBytes(r);t.set(e,a);a+=r}}else{r=257-r;t=this.ensureBuffer(a+r+1);t.fill(e[1],a,a+r);a+=r}this.bufferLength=a}}class Parser{constructor({lexer:e,xref:t,allowStreams:a=!1,recoveryMode:r=!1}){this.lexer=e;this.xref=t;this.allowStreams=a;this.recoveryMode=r;this.imageCache=Object.create(null);this._imageId=0;this.refill()}refill(){this.buf1=this.lexer.getObj();this.buf2=this.lexer.getObj()}shift(){if(this.buf2 instanceof Cmd&&"ID"===this.buf2.cmd){this.buf1=this.buf2;this.buf2=null}else{this.buf1=this.buf2;this.buf2=this.lexer.getObj()}}tryShift(){try{this.shift();return!0}catch(e){if(e instanceof MissingDataException)throw e;return!1}}getObj(e=null){const t=this.buf1;this.shift();if(t instanceof Cmd)switch(t.cmd){case"BI":return this.makeInlineImage(e);case"[":const a=[];for(;!isCmd(this.buf1,"]")&&this.buf1!==wa;)a.push(this.getObj(e));if(this.buf1===wa){if(this.recoveryMode)return a;throw new ParserEOFException("End of file inside array.")}this.shift();return a;case"<<":const r=new Dict(this.xref);for(;!isCmd(this.buf1,">>")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name)){info("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r.set(t,this.getObj(e))}if(this.buf1===wa){if(this.recoveryMode)return r;throw new ParserEOFException("End of file inside dictionary.")}if(isCmd(this.buf2,"stream"))return this.allowStreams?this.makeStream(r,e):r;this.shift();return r;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&isCmd(this.buf2,"R")){const e=Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,a=e.pos;let r,i,n=0;for(;-1!==(r=e.getByte());)if(0===n)n=69===r?1:0;else if(1===n)n=73===r?2:0;else if(32===r||10===r||13===r){i=e.pos;const a=e.peekBytes(15),s=a.length;if(0===s)break;for(let e=0;e127))){n=0;break}}if(2!==n)continue;if(!t){warn("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const o=new Lexer(new Stream(e.peekBytes(75)),t);o._hexStringWarn=()=>{};let c=0;for(;;){const e=o.getObj();if(e===wa){n=0;break}if(e instanceof Cmd){const a=t[e.cmd];if(!a){n=0;break}if(a.variableArgs?c<=a.numArgs:c===a.numArgs)break;c=0}else c++}if(2===n)break}else n=0;if(-1===r){warn("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(i){warn('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-i))}}let s=4;e.skip(-s);r=e.peekByte();e.skip(s);isWhiteSpace(r)||s--;return e.pos-s-a}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let a,r,i=!1;for(;-1!==(a=e.getByte());)if(255===a){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:i=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:r=e.getUint16();r>2?e.skip(r-2):e.skip(-2)}if(i)break}const n=e.pos-t;if(-1===a){warn("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte());)if(126===a){const t=e.pos;a=e.peekByte();for(;isWhiteSpace(a);){e.skip();a=e.peekByte()}if(62===a){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const r=e.pos-t;if(-1===a){warn("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte())&&62!==a;);const r=e.pos-t;if(-1===a){warn("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-r);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return r}inlineStreamSkipEI(e){let t,a=0;for(;-1!==(t=e.getByte());)if(0===a)a=69===t?1:0;else if(1===a)a=73===t?2:0;else if(2===a)break}makeInlineImage(e){const t=this.lexer,a=t.stream,r=Object.create(null);let i;for(;!isCmd(this.buf1,"ID")&&this.buf1!==wa;){if(!(this.buf1 instanceof Name))throw new FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===wa)break;r[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(i=a.pos-t.beginInlineImagePos);const n=this.xref.fetchIfRef(r.F||r.Filter);let s;if(n instanceof Name)s=n.name;else if(Array.isArray(n)){const e=this.xref.fetchIfRef(n[0]);e instanceof Name&&(s=e.name)}const o=a.pos;let c,l;switch(s){case"DCT":case"DCTDecode":c=this.findDCTDecodeInlineStreamEnd(a);break;case"A85":case"ASCII85Decode":c=this.findASCII85DecodeInlineStreamEnd(a);break;case"AHx":case"ASCIIHexDecode":c=this.findASCIIHexDecodeInlineStreamEnd(a);break;default:c=this.findDefaultInlineStreamEnd(a)}if(c<1e3&&i>0){const e=a.pos;a.pos=t.beginInlineImagePos;l=function getInlineImageCacheKey(e){const t=[],a=e.length;let r=0;for(;r=r){let r=!1;for(const e of i){const t=e.length;let i=0;for(;i=n){r=!0;break}if(i>=t){if(isWhiteSpace(s[c+o+i])){info(`Found "${bytesToString([...a,...e])}" when searching for endstream command.`);r=!0}break}}if(r){t.pos+=c;return t.pos-e}}c++}t.pos+=o}return-1}makeStream(e,t){const a=this.lexer;let r=a.stream;a.skipToNextLine();const i=r.pos-1;let n=e.get("Length");if(!Number.isInteger(n)){info(`Bad length "${n&&n.toString()}" in stream.`);n=0}r.pos=i+n;a.nextChar();if(this.tryShift()&&isCmd(this.buf2,"endstream"))this.shift();else{n=this.#q(i);if(n<0)throw new FormatError("Missing endstream command.");a.nextChar();this.shift();this.shift()}this.shift();r=r.makeSubStream(i,n,e);t&&(r=t.createStream(r,n));r=this.filter(r,e,n);r.dict=e;return r}filter(e,t,a){let r=t.get("F","Filter"),i=t.get("DP","DecodeParms");if(r instanceof Name){Array.isArray(i)&&warn("/DecodeParms should not be an Array, when /Filter is a Name.");return this.makeFilter(e,r.name,a,i)}let n=a;if(Array.isArray(r)){const t=r,a=i;for(let s=0,o=t.length;s=48&&e<=57?15&e:e>=65&&e<=70||e>=97&&e<=102?9+(15&e):-1}class Lexer{constructor(e,t=null){this.stream=e;this.nextChar();this.strBuf=[];this.knownCommands=t;this._hexStringNumWarn=0;this.beginInlineImagePos=-1}nextChar(){return this.currentChar=this.stream.getByte()}peekChar(){return this.stream.peekByte()}getNumber(){let e=this.currentChar,t=!1,a=0,r=1;if(45===e){r=-1;e=this.nextChar();45===e&&(e=this.nextChar())}else 43===e&&(e=this.nextChar());if(10===e||13===e)do{e=this.nextChar()}while(10===e||13===e);if(46===e){a=10;e=this.nextChar()}if(e<48||e>57){const t=`Invalid number: ${String.fromCharCode(e)} (charCode ${e})`;if(isWhiteSpace(e)||40===e||60===e||-1===e){info(`Lexer.getNumber - "${t}".`);return 0}throw new FormatError(t)}let i=e-48,n=0,s=1;for(;(e=this.nextChar())>=0;)if(e>=48&&e<=57){const r=e-48;if(t)n=10*n+r;else{0!==a&&(a*=10);i=10*i+r}}else if(46===e){if(0!==a)break;a=1}else if(45===e)warn("Badly formatted number: minus sign in the middle");else{if(69!==e&&101!==e)break;e=this.peekChar();if(43===e||45===e){s=45===e?-1:1;this.nextChar()}else if(e<48||e>57)break;t=!0}0!==a&&(i/=a);t&&(i*=10**(s*n));return r*i}getString(){let e=1,t=!1;const a=this.strBuf;a.length=0;let r=this.nextChar();for(;;){let i=!1;switch(0|r){case-1:warn("Unterminated string");t=!0;break;case 40:++e;a.push("(");break;case 41:if(0==--e){this.nextChar();t=!0}else a.push(")");break;case 92:r=this.nextChar();switch(r){case-1:warn("Unterminated string");t=!0;break;case 110:a.push("\n");break;case 114:a.push("\r");break;case 116:a.push("\t");break;case 98:a.push("\b");break;case 102:a.push("\f");break;case 92:case 40:case 41:a.push(String.fromCharCode(r));break;case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:let e=15&r;r=this.nextChar();i=!0;if(r>=48&&r<=55){e=(e<<3)+(15&r);r=this.nextChar();if(r>=48&&r<=55){i=!1;e=(e<<3)+(15&r)}}a.push(String.fromCharCode(e));break;case 13:10===this.peekChar()&&this.nextChar();break;case 10:break;default:a.push(String.fromCharCode(r))}break;default:a.push(String.fromCharCode(r))}if(t)break;i||(r=this.nextChar())}return a.join("")}getName(){let e,t;const a=this.strBuf;a.length=0;for(;(e=this.nextChar())>=0&&!mr[e];)if(35===e){e=this.nextChar();if(mr[e]){warn("Lexer_getName: NUMBER SIGN (#) should be followed by a hexadecimal number.");a.push("#");break}const r=toHexDigit(e);if(-1!==r){t=e;e=this.nextChar();const i=toHexDigit(e);if(-1===i){warn(`Lexer_getName: Illegal digit (${String.fromCharCode(e)}) in hexadecimal number.`);a.push("#",String.fromCharCode(t));if(mr[e])break;a.push(String.fromCharCode(e));continue}a.push(String.fromCharCode(r<<4|i))}else a.push("#",String.fromCharCode(e))}else a.push(String.fromCharCode(e));a.length>127&&warn(`Name token is longer than allowed by the spec: ${a.length}`);return Name.get(a.join(""))}_hexStringWarn(e){5!=this._hexStringNumWarn++?this._hexStringNumWarn>5||warn(`getHexString - ignoring invalid character: ${e}`):warn("getHexString - ignoring additional invalid characters.")}getHexString(){const e=this.strBuf;e.length=0;let t=this.currentChar,a=-1,r=-1;this._hexStringNumWarn=0;for(;;){if(t<0){warn("Unterminated hex string");break}if(62===t){this.nextChar();break}if(1!==mr[t]){r=toHexDigit(t);if(-1===r)this._hexStringWarn(t);else if(-1===a)a=r;else{e.push(String.fromCharCode(a<<4|r));a=-1}t=this.nextChar()}else t=this.nextChar()}-1!==a&&e.push(String.fromCharCode(a<<4));return e.join("")}getObj(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(1!==mr[t])break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return this.getNumber();case 40:return this.getString();case 47:return this.getName();case 91:this.nextChar();return Cmd.get("[");case 93:this.nextChar();return Cmd.get("]");case 60:t=this.nextChar();if(60===t){this.nextChar();return Cmd.get("<<")}return this.getHexString();case 62:t=this.nextChar();if(62===t){this.nextChar();return Cmd.get(">>")}return Cmd.get(">");case 123:this.nextChar();return Cmd.get("{");case 125:this.nextChar();return Cmd.get("}");case 41:this.nextChar();throw new FormatError(`Illegal character: ${t}`)}let a=String.fromCharCode(t);if(t<32||t>127){const e=this.peekChar();if(e>=32&&e<=127){this.nextChar();return Cmd.get(a)}}const r=this.knownCommands;let i=void 0!==r?.[a];for(;(t=this.nextChar())>=0&&!mr[t];){const e=a+String.fromCharCode(t);if(i&&void 0===r[e])break;if(128===a.length)throw new FormatError(`Command token too long: ${a.length}`);a=e;i=void 0!==r?.[a]}if("true"===a)return!0;if("false"===a)return!1;if("null"===a)return null;"BI"===a&&(this.beginInlineImagePos=this.stream.pos);return Cmd.get(a)}skipToNextLine(){let e=this.currentChar;for(;e>=0;){if(13===e){e=this.nextChar();10===e&&this.nextChar();break}if(10===e){this.nextChar();break}e=this.nextChar()}}}class Linearization{static create(e){function getInt(e,t,a=!1){const r=e.get(t);if(Number.isInteger(r)&&(a?r>=0:r>0))return r;throw new Error(`The "${t}" parameter in the linearization dictionary is invalid.`)}const t=new Parser({lexer:new Lexer(e),xref:null}),a=t.getObj(),r=t.getObj(),i=t.getObj(),n=t.getObj();let s,o;if(!(Number.isInteger(a)&&Number.isInteger(r)&&isCmd(i,"obj")&&n instanceof Dict&&"number"==typeof(s=n.get("Linearized"))&&s>0))return null;if((o=getInt(n,"L"))!==e.length)throw new Error('The "L" parameter in the linearization dictionary does not equal the stream length.');return{length:o,hints:function getHints(e){const t=e.get("H");let a;if(Array.isArray(t)&&(2===(a=t.length)||4===a)){for(let e=0;e0))throw new Error(`Hint (${e}) in the linearization dictionary is invalid.`)}return t}throw new Error("Hint array in the linearization dictionary is invalid.")}(n),objectNumberFirst:getInt(n,"O"),endFirst:getInt(n,"E"),numPages:getInt(n,"N"),mainXRefEntriesOffset:getInt(n,"T"),pageFirst:n.has("P")?getInt(n,"P",!0):0}}}const br=["Adobe-GB1-UCS2","Adobe-CNS1-UCS2","Adobe-Japan1-UCS2","Adobe-Korea1-UCS2","78-EUC-H","78-EUC-V","78-H","78-RKSJ-H","78-RKSJ-V","78-V","78ms-RKSJ-H","78ms-RKSJ-V","83pv-RKSJ-H","90ms-RKSJ-H","90ms-RKSJ-V","90msp-RKSJ-H","90msp-RKSJ-V","90pv-RKSJ-H","90pv-RKSJ-V","Add-H","Add-RKSJ-H","Add-RKSJ-V","Add-V","Adobe-CNS1-0","Adobe-CNS1-1","Adobe-CNS1-2","Adobe-CNS1-3","Adobe-CNS1-4","Adobe-CNS1-5","Adobe-CNS1-6","Adobe-GB1-0","Adobe-GB1-1","Adobe-GB1-2","Adobe-GB1-3","Adobe-GB1-4","Adobe-GB1-5","Adobe-Japan1-0","Adobe-Japan1-1","Adobe-Japan1-2","Adobe-Japan1-3","Adobe-Japan1-4","Adobe-Japan1-5","Adobe-Japan1-6","Adobe-Korea1-0","Adobe-Korea1-1","Adobe-Korea1-2","B5-H","B5-V","B5pc-H","B5pc-V","CNS-EUC-H","CNS-EUC-V","CNS1-H","CNS1-V","CNS2-H","CNS2-V","ETHK-B5-H","ETHK-B5-V","ETen-B5-H","ETen-B5-V","ETenms-B5-H","ETenms-B5-V","EUC-H","EUC-V","Ext-H","Ext-RKSJ-H","Ext-RKSJ-V","Ext-V","GB-EUC-H","GB-EUC-V","GB-H","GB-V","GBK-EUC-H","GBK-EUC-V","GBK2K-H","GBK2K-V","GBKp-EUC-H","GBKp-EUC-V","GBT-EUC-H","GBT-EUC-V","GBT-H","GBT-V","GBTpc-EUC-H","GBTpc-EUC-V","GBpc-EUC-H","GBpc-EUC-V","H","HKdla-B5-H","HKdla-B5-V","HKdlb-B5-H","HKdlb-B5-V","HKgccs-B5-H","HKgccs-B5-V","HKm314-B5-H","HKm314-B5-V","HKm471-B5-H","HKm471-B5-V","HKscs-B5-H","HKscs-B5-V","Hankaku","Hiragana","KSC-EUC-H","KSC-EUC-V","KSC-H","KSC-Johab-H","KSC-Johab-V","KSC-V","KSCms-UHC-H","KSCms-UHC-HW-H","KSCms-UHC-HW-V","KSCms-UHC-V","KSCpc-EUC-H","KSCpc-EUC-V","Katakana","NWP-H","NWP-V","RKSJ-H","RKSJ-V","Roman","UniCNS-UCS2-H","UniCNS-UCS2-V","UniCNS-UTF16-H","UniCNS-UTF16-V","UniCNS-UTF32-H","UniCNS-UTF32-V","UniCNS-UTF8-H","UniCNS-UTF8-V","UniGB-UCS2-H","UniGB-UCS2-V","UniGB-UTF16-H","UniGB-UTF16-V","UniGB-UTF32-H","UniGB-UTF32-V","UniGB-UTF8-H","UniGB-UTF8-V","UniJIS-UCS2-H","UniJIS-UCS2-HW-H","UniJIS-UCS2-HW-V","UniJIS-UCS2-V","UniJIS-UTF16-H","UniJIS-UTF16-V","UniJIS-UTF32-H","UniJIS-UTF32-V","UniJIS-UTF8-H","UniJIS-UTF8-V","UniJIS2004-UTF16-H","UniJIS2004-UTF16-V","UniJIS2004-UTF32-H","UniJIS2004-UTF32-V","UniJIS2004-UTF8-H","UniJIS2004-UTF8-V","UniJISPro-UCS2-HW-V","UniJISPro-UCS2-V","UniJISPro-UTF8-V","UniJISX0213-UTF32-H","UniJISX0213-UTF32-V","UniJISX02132004-UTF32-H","UniJISX02132004-UTF32-V","UniKS-UCS2-H","UniKS-UCS2-V","UniKS-UTF16-H","UniKS-UTF16-V","UniKS-UTF32-H","UniKS-UTF32-V","UniKS-UTF8-H","UniKS-UTF8-V","V","WP-Symbol"],yr=2**24-1;class CMap{constructor(e=!1){this.codespaceRanges=[[],[],[],[]];this.numCodespaceRanges=0;this._map=[];this.name="";this.vertical=!1;this.useCMap=null;this.builtInCMap=e}addCodespaceRange(e,t,a){this.codespaceRanges[e-1].push(t,a);this.numCodespaceRanges++}mapCidRange(e,t,a){if(t-e>yr)throw new Error("mapCidRange - ignoring data above MAX_MAP_RANGE.");for(;e<=t;)this._map[e++]=a++}mapBfRange(e,t,a){if(t-e>yr)throw new Error("mapBfRange - ignoring data above MAX_MAP_RANGE.");const r=a.length-1;for(;e<=t;){this._map[e++]=a;const t=a.charCodeAt(r)+1;t>255?a=a.substring(0,r-1)+String.fromCharCode(a.charCodeAt(r-1)+1)+"\0":a=a.substring(0,r)+String.fromCharCode(t)}}mapBfRangeToArray(e,t,a){if(t-e>yr)throw new Error("mapBfRangeToArray - ignoring data above MAX_MAP_RANGE.");const r=a.length;let i=0;for(;e<=t&&i>>0;const s=i[n];for(let e=0,t=s.length;e=t&&r<=i){a.charcode=r;a.length=n+1;return}}}a.charcode=0;a.length=1}getCharCodeLength(e){const t=this.codespaceRanges;for(let a=0,r=t.length;a=i&&e<=n)return a+1}}return 1}get length(){return this._map.length}get isIdentityCMap(){if("Identity-H"!==this.name&&"Identity-V"!==this.name)return!1;if(65536!==this._map.length)return!1;for(let e=0;e<65536;e++)if(this._map[e]!==e)return!1;return!0}}class IdentityCMap extends CMap{constructor(e,t){super();this.vertical=e;this.addCodespaceRange(t,0,65535)}mapCidRange(e,t,a){unreachable("should not call mapCidRange")}mapBfRange(e,t,a){unreachable("should not call mapBfRange")}mapBfRangeToArray(e,t,a){unreachable("should not call mapBfRangeToArray")}mapOne(e,t){unreachable("should not call mapCidOne")}lookup(e){return Number.isInteger(e)&&e<=65535?e:void 0}contains(e){return Number.isInteger(e)&&e<=65535}forEach(e){for(let t=0;t<=65535;t++)e(t,t)}charCodeOf(e){return Number.isInteger(e)&&e<=65535?e:-1}getMap(){const e=new Array(65536);for(let t=0;t<=65535;t++)e[t]=t;return e}get length(){return 65536}get isIdentityCMap(){unreachable("should not access .isIdentityCMap")}}function strToInt(e){let t=0;for(let a=0;a>>0}function expectString(e){if("string"!=typeof e)throw new FormatError("Malformed CMap: expected string.")}function expectInt(e){if(!Number.isInteger(e))throw new FormatError("Malformed CMap: expected int.")}function parseBfChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=a;e.mapOne(r,i)}}function parseBfRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endbfrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();if(Number.isInteger(a)||"string"==typeof a){const t=Number.isInteger(a)?String.fromCharCode(a):a;e.mapBfRange(r,i,t)}else{if(!isCmd(a,"["))break;{a=t.getObj();const n=[];for(;!isCmd(a,"]")&&a!==wa;){n.push(a);a=t.getObj()}e.mapBfRangeToArray(r,i,n)}}}throw new FormatError("Invalid bf range.")}function parseCidChar(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidchar"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectInt(a);const i=a;e.mapOne(r,i)}}function parseCidRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcidrange"))return;expectString(a);const r=strToInt(a);a=t.getObj();expectString(a);const i=strToInt(a);a=t.getObj();expectInt(a);const n=a;e.mapCidRange(r,i,n)}}function parseCodespaceRange(e,t){for(;;){let a=t.getObj();if(a===wa)break;if(isCmd(a,"endcodespacerange"))return;if("string"!=typeof a)break;const r=strToInt(a);a=t.getObj();if("string"!=typeof a)break;const i=strToInt(a);e.addCodespaceRange(a.length,r,i)}throw new FormatError("Invalid codespace range.")}function parseWMode(e,t){const a=t.getObj();Number.isInteger(a)&&(e.vertical=!!a)}function parseCMapName(e,t){const a=t.getObj();a instanceof Name&&(e.name=a.name)}async function parseCMap(e,t,a,r){let i,n;e:for(;;)try{const a=t.getObj();if(a===wa)break;if(a instanceof Name){"WMode"===a.name?parseWMode(e,t):"CMapName"===a.name&&parseCMapName(e,t);i=a}else if(a instanceof Cmd)switch(a.cmd){case"endcmap":break e;case"usecmap":i instanceof Name&&(n=i.name);break;case"begincodespacerange":parseCodespaceRange(e,t);break;case"beginbfchar":parseBfChar(e,t);break;case"begincidchar":parseCidChar(e,t);break;case"beginbfrange":parseBfRange(e,t);break;case"begincidrange":parseCidRange(e,t)}}catch(e){if(e instanceof MissingDataException)throw e;warn("Invalid cMap data: "+e);continue}!r&&n&&(r=n);return r?extendCMap(e,a,r):e}async function extendCMap(e,t,a){e.useCMap=await createBuiltInCMap(a,t);if(0===e.numCodespaceRanges){const t=e.useCMap.codespaceRanges;for(let a=0;aextendCMap(i,t,e)));const n=new Lexer(new Stream(a));return parseCMap(i,n,t,null)}class CMapFactory{static async create({encoding:e,fetchBuiltInCMap:t,useCMap:a}){if(e instanceof Name)return createBuiltInCMap(e.name,t);if(e instanceof BaseStream){const r=await parseCMap(new CMap,new Lexer(e),t,a);return r.isIdentityCMap?createBuiltInCMap(r.name,t):r}throw new Error("Encoding required.")}}const wr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","","asuperior","bsuperior","centsuperior","dsuperior","esuperior","","","","isuperior","","","lsuperior","msuperior","nsuperior","osuperior","","","rsuperior","ssuperior","tsuperior","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdownsmall","centoldstyle","Lslashsmall","","","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","","Dotaccentsmall","","","Macronsmall","","","figuredash","hypheninferior","","","Ogoneksmall","Ringsmall","Cedillasmall","","","","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],Sr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclamsmall","Hungarumlautsmall","centoldstyle","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","","threequartersemdash","","questionsmall","","","","","Ethsmall","","","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","","","","","","","ff","fi","fl","ffi","ffl","parenleftinferior","","parenrightinferior","Circumflexsmall","hypheninferior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","","","asuperior","centsuperior","","","","","Aacutesmall","Agravesmall","Acircumflexsmall","Adieresissmall","Atildesmall","Aringsmall","Ccedillasmall","Eacutesmall","Egravesmall","Ecircumflexsmall","Edieresissmall","Iacutesmall","Igravesmall","Icircumflexsmall","Idieresissmall","Ntildesmall","Oacutesmall","Ogravesmall","Ocircumflexsmall","Odieresissmall","Otildesmall","Uacutesmall","Ugravesmall","Ucircumflexsmall","Udieresissmall","","eightsuperior","fourinferior","threeinferior","sixinferior","eightinferior","seveninferior","Scaronsmall","","centinferior","twoinferior","","Dieresissmall","","Caronsmall","osuperior","fiveinferior","","commainferior","periodinferior","Yacutesmall","","dollarinferior","","","Thornsmall","","nineinferior","zeroinferior","Zcaronsmall","AEsmall","Oslashsmall","questiondownsmall","oneinferior","Lslashsmall","","","","","","","Cedillasmall","","","","","","OEsmall","figuredash","hyphensuperior","","","","","exclamdownsmall","","Ydieresissmall","","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","ninesuperior","zerosuperior","","esuperior","rsuperior","tsuperior","","","isuperior","ssuperior","dsuperior","","","","","","lsuperior","Ogoneksmall","Brevesmall","Macronsmall","bsuperior","nsuperior","msuperior","commasuperior","periodsuperior","Dotaccentsmall","Ringsmall","","","",""],xr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","space","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron"],Ar=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","","endash","dagger","daggerdbl","periodcentered","","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","","questiondown","","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","","ring","cedilla","","hungarumlaut","ogonek","caron","emdash","","","","","","","","","","","","","","","","","AE","","ordfeminine","","","","","Lslash","Oslash","OE","ordmasculine","","","","","","ae","","","","dotlessi","","","lslash","oslash","oe","germandbls","","","",""],kr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","bullet","Euro","bullet","quotesinglbase","florin","quotedblbase","ellipsis","dagger","daggerdbl","circumflex","perthousand","Scaron","guilsinglleft","OE","bullet","Zcaron","bullet","bullet","quoteleft","quoteright","quotedblleft","quotedblright","bullet","endash","emdash","tilde","trademark","scaron","guilsinglright","oe","bullet","zcaron","Ydieresis","space","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis","copyright","ordfeminine","guillemotleft","logicalnot","hyphen","registered","macron","degree","plusminus","twosuperior","threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright","onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring","AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth","Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex","Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae","ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde","ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis","yacute","thorn","ydieresis"],Cr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","exclam","universal","numbersign","existential","percent","ampersand","suchthat","parenleft","parenright","asteriskmath","plus","comma","minus","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","congruent","Alpha","Beta","Chi","Delta","Epsilon","Phi","Gamma","Eta","Iota","theta1","Kappa","Lambda","Mu","Nu","Omicron","Pi","Theta","Rho","Sigma","Tau","Upsilon","sigma1","Omega","Xi","Psi","Zeta","bracketleft","therefore","bracketright","perpendicular","underscore","radicalex","alpha","beta","chi","delta","epsilon","phi","gamma","eta","iota","phi1","kappa","lambda","mu","nu","omicron","pi","theta","rho","sigma","tau","upsilon","omega1","omega","xi","psi","zeta","braceleft","bar","braceright","similar","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","Euro","Upsilon1","minute","lessequal","fraction","infinity","florin","club","diamond","heart","spade","arrowboth","arrowleft","arrowup","arrowright","arrowdown","degree","plusminus","second","greaterequal","multiply","proportional","partialdiff","bullet","divide","notequal","equivalence","approxequal","ellipsis","arrowvertex","arrowhorizex","carriagereturn","aleph","Ifraktur","Rfraktur","weierstrass","circlemultiply","circleplus","emptyset","intersection","union","propersuperset","reflexsuperset","notsubset","propersubset","reflexsubset","element","notelement","angle","gradient","registerserif","copyrightserif","trademarkserif","product","radical","dotmath","logicalnot","logicaland","logicalor","arrowdblboth","arrowdblleft","arrowdblup","arrowdblright","arrowdbldown","lozenge","angleleft","registersans","copyrightsans","trademarksans","summation","parenlefttp","parenleftex","parenleftbt","bracketlefttp","bracketleftex","bracketleftbt","bracelefttp","braceleftmid","braceleftbt","braceex","","angleright","integral","integraltp","integralex","integralbt","parenrighttp","parenrightex","parenrightbt","bracketrighttp","bracketrightex","bracketrightbt","bracerighttp","bracerightmid","bracerightbt",""],vr=["","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","space","a1","a2","a202","a3","a4","a5","a119","a118","a117","a11","a12","a13","a14","a15","a16","a105","a17","a18","a19","a20","a21","a22","a23","a24","a25","a26","a27","a28","a6","a7","a8","a9","a10","a29","a30","a31","a32","a33","a34","a35","a36","a37","a38","a39","a40","a41","a42","a43","a44","a45","a46","a47","a48","a49","a50","a51","a52","a53","a54","a55","a56","a57","a58","a59","a60","a61","a62","a63","a64","a65","a66","a67","a68","a69","a70","a71","a72","a73","a74","a203","a75","a204","a76","a77","a78","a79","a81","a82","a83","a84","a97","a98","a99","a100","","a89","a90","a93","a94","a91","a92","a205","a85","a206","a86","a87","a88","a95","a96","","","","","","","","","","","","","","","","","","","","a101","a102","a103","a104","a106","a107","a108","a112","a111","a110","a109","a120","a121","a122","a123","a124","a125","a126","a127","a128","a129","a130","a131","a132","a133","a134","a135","a136","a137","a138","a139","a140","a141","a142","a143","a144","a145","a146","a147","a148","a149","a150","a151","a152","a153","a154","a155","a156","a157","a158","a159","a160","a161","a163","a164","a196","a165","a192","a166","a167","a168","a169","a170","a171","a172","a173","a162","a174","a175","a176","a177","a178","a179","a193","a180","a199","a181","a200","a182","","a201","a183","a184","a197","a185","a194","a198","a186","a195","a187","a188","a189","a190","a191",""];function getEncoding(e){switch(e){case"WinAnsiEncoding":return kr;case"StandardEncoding":return Ar;case"MacRomanEncoding":return xr;case"SymbolSetEncoding":return Cr;case"ZapfDingbatsEncoding":return vr;case"ExpertEncoding":return wr;case"MacExpertEncoding":return Sr;default:return null}}const Fr=getLookupTableFactory((function(e){e.A=65;e.AE=198;e.AEacute=508;e.AEmacron=482;e.AEsmall=63462;e.Aacute=193;e.Aacutesmall=63457;e.Abreve=258;e.Abreveacute=7854;e.Abrevecyrillic=1232;e.Abrevedotbelow=7862;e.Abrevegrave=7856;e.Abrevehookabove=7858;e.Abrevetilde=7860;e.Acaron=461;e.Acircle=9398;e.Acircumflex=194;e.Acircumflexacute=7844;e.Acircumflexdotbelow=7852;e.Acircumflexgrave=7846;e.Acircumflexhookabove=7848;e.Acircumflexsmall=63458;e.Acircumflextilde=7850;e.Acute=63177;e.Acutesmall=63412;e.Acyrillic=1040;e.Adblgrave=512;e.Adieresis=196;e.Adieresiscyrillic=1234;e.Adieresismacron=478;e.Adieresissmall=63460;e.Adotbelow=7840;e.Adotmacron=480;e.Agrave=192;e.Agravesmall=63456;e.Ahookabove=7842;e.Aiecyrillic=1236;e.Ainvertedbreve=514;e.Alpha=913;e.Alphatonos=902;e.Amacron=256;e.Amonospace=65313;e.Aogonek=260;e.Aring=197;e.Aringacute=506;e.Aringbelow=7680;e.Aringsmall=63461;e.Asmall=63329;e.Atilde=195;e.Atildesmall=63459;e.Aybarmenian=1329;e.B=66;e.Bcircle=9399;e.Bdotaccent=7682;e.Bdotbelow=7684;e.Becyrillic=1041;e.Benarmenian=1330;e.Beta=914;e.Bhook=385;e.Blinebelow=7686;e.Bmonospace=65314;e.Brevesmall=63220;e.Bsmall=63330;e.Btopbar=386;e.C=67;e.Caarmenian=1342;e.Cacute=262;e.Caron=63178;e.Caronsmall=63221;e.Ccaron=268;e.Ccedilla=199;e.Ccedillaacute=7688;e.Ccedillasmall=63463;e.Ccircle=9400;e.Ccircumflex=264;e.Cdot=266;e.Cdotaccent=266;e.Cedillasmall=63416;e.Chaarmenian=1353;e.Cheabkhasiancyrillic=1212;e.Checyrillic=1063;e.Chedescenderabkhasiancyrillic=1214;e.Chedescendercyrillic=1206;e.Chedieresiscyrillic=1268;e.Cheharmenian=1347;e.Chekhakassiancyrillic=1227;e.Cheverticalstrokecyrillic=1208;e.Chi=935;e.Chook=391;e.Circumflexsmall=63222;e.Cmonospace=65315;e.Coarmenian=1361;e.Csmall=63331;e.D=68;e.DZ=497;e.DZcaron=452;e.Daarmenian=1332;e.Dafrican=393;e.Dcaron=270;e.Dcedilla=7696;e.Dcircle=9401;e.Dcircumflexbelow=7698;e.Dcroat=272;e.Ddotaccent=7690;e.Ddotbelow=7692;e.Decyrillic=1044;e.Deicoptic=1006;e.Delta=8710;e.Deltagreek=916;e.Dhook=394;e.Dieresis=63179;e.DieresisAcute=63180;e.DieresisGrave=63181;e.Dieresissmall=63400;e.Digammagreek=988;e.Djecyrillic=1026;e.Dlinebelow=7694;e.Dmonospace=65316;e.Dotaccentsmall=63223;e.Dslash=272;e.Dsmall=63332;e.Dtopbar=395;e.Dz=498;e.Dzcaron=453;e.Dzeabkhasiancyrillic=1248;e.Dzecyrillic=1029;e.Dzhecyrillic=1039;e.E=69;e.Eacute=201;e.Eacutesmall=63465;e.Ebreve=276;e.Ecaron=282;e.Ecedillabreve=7708;e.Echarmenian=1333;e.Ecircle=9402;e.Ecircumflex=202;e.Ecircumflexacute=7870;e.Ecircumflexbelow=7704;e.Ecircumflexdotbelow=7878;e.Ecircumflexgrave=7872;e.Ecircumflexhookabove=7874;e.Ecircumflexsmall=63466;e.Ecircumflextilde=7876;e.Ecyrillic=1028;e.Edblgrave=516;e.Edieresis=203;e.Edieresissmall=63467;e.Edot=278;e.Edotaccent=278;e.Edotbelow=7864;e.Efcyrillic=1060;e.Egrave=200;e.Egravesmall=63464;e.Eharmenian=1335;e.Ehookabove=7866;e.Eightroman=8551;e.Einvertedbreve=518;e.Eiotifiedcyrillic=1124;e.Elcyrillic=1051;e.Elevenroman=8554;e.Emacron=274;e.Emacronacute=7702;e.Emacrongrave=7700;e.Emcyrillic=1052;e.Emonospace=65317;e.Encyrillic=1053;e.Endescendercyrillic=1186;e.Eng=330;e.Enghecyrillic=1188;e.Enhookcyrillic=1223;e.Eogonek=280;e.Eopen=400;e.Epsilon=917;e.Epsilontonos=904;e.Ercyrillic=1056;e.Ereversed=398;e.Ereversedcyrillic=1069;e.Escyrillic=1057;e.Esdescendercyrillic=1194;e.Esh=425;e.Esmall=63333;e.Eta=919;e.Etarmenian=1336;e.Etatonos=905;e.Eth=208;e.Ethsmall=63472;e.Etilde=7868;e.Etildebelow=7706;e.Euro=8364;e.Ezh=439;e.Ezhcaron=494;e.Ezhreversed=440;e.F=70;e.Fcircle=9403;e.Fdotaccent=7710;e.Feharmenian=1366;e.Feicoptic=996;e.Fhook=401;e.Fitacyrillic=1138;e.Fiveroman=8548;e.Fmonospace=65318;e.Fourroman=8547;e.Fsmall=63334;e.G=71;e.GBsquare=13191;e.Gacute=500;e.Gamma=915;e.Gammaafrican=404;e.Gangiacoptic=1002;e.Gbreve=286;e.Gcaron=486;e.Gcedilla=290;e.Gcircle=9404;e.Gcircumflex=284;e.Gcommaaccent=290;e.Gdot=288;e.Gdotaccent=288;e.Gecyrillic=1043;e.Ghadarmenian=1346;e.Ghemiddlehookcyrillic=1172;e.Ghestrokecyrillic=1170;e.Gheupturncyrillic=1168;e.Ghook=403;e.Gimarmenian=1331;e.Gjecyrillic=1027;e.Gmacron=7712;e.Gmonospace=65319;e.Grave=63182;e.Gravesmall=63328;e.Gsmall=63335;e.Gsmallhook=667;e.Gstroke=484;e.H=72;e.H18533=9679;e.H18543=9642;e.H18551=9643;e.H22073=9633;e.HPsquare=13259;e.Haabkhasiancyrillic=1192;e.Hadescendercyrillic=1202;e.Hardsigncyrillic=1066;e.Hbar=294;e.Hbrevebelow=7722;e.Hcedilla=7720;e.Hcircle=9405;e.Hcircumflex=292;e.Hdieresis=7718;e.Hdotaccent=7714;e.Hdotbelow=7716;e.Hmonospace=65320;e.Hoarmenian=1344;e.Horicoptic=1e3;e.Hsmall=63336;e.Hungarumlaut=63183;e.Hungarumlautsmall=63224;e.Hzsquare=13200;e.I=73;e.IAcyrillic=1071;e.IJ=306;e.IUcyrillic=1070;e.Iacute=205;e.Iacutesmall=63469;e.Ibreve=300;e.Icaron=463;e.Icircle=9406;e.Icircumflex=206;e.Icircumflexsmall=63470;e.Icyrillic=1030;e.Idblgrave=520;e.Idieresis=207;e.Idieresisacute=7726;e.Idieresiscyrillic=1252;e.Idieresissmall=63471;e.Idot=304;e.Idotaccent=304;e.Idotbelow=7882;e.Iebrevecyrillic=1238;e.Iecyrillic=1045;e.Ifraktur=8465;e.Igrave=204;e.Igravesmall=63468;e.Ihookabove=7880;e.Iicyrillic=1048;e.Iinvertedbreve=522;e.Iishortcyrillic=1049;e.Imacron=298;e.Imacroncyrillic=1250;e.Imonospace=65321;e.Iniarmenian=1339;e.Iocyrillic=1025;e.Iogonek=302;e.Iota=921;e.Iotaafrican=406;e.Iotadieresis=938;e.Iotatonos=906;e.Ismall=63337;e.Istroke=407;e.Itilde=296;e.Itildebelow=7724;e.Izhitsacyrillic=1140;e.Izhitsadblgravecyrillic=1142;e.J=74;e.Jaarmenian=1345;e.Jcircle=9407;e.Jcircumflex=308;e.Jecyrillic=1032;e.Jheharmenian=1355;e.Jmonospace=65322;e.Jsmall=63338;e.K=75;e.KBsquare=13189;e.KKsquare=13261;e.Kabashkircyrillic=1184;e.Kacute=7728;e.Kacyrillic=1050;e.Kadescendercyrillic=1178;e.Kahookcyrillic=1219;e.Kappa=922;e.Kastrokecyrillic=1182;e.Kaverticalstrokecyrillic=1180;e.Kcaron=488;e.Kcedilla=310;e.Kcircle=9408;e.Kcommaaccent=310;e.Kdotbelow=7730;e.Keharmenian=1364;e.Kenarmenian=1343;e.Khacyrillic=1061;e.Kheicoptic=998;e.Khook=408;e.Kjecyrillic=1036;e.Klinebelow=7732;e.Kmonospace=65323;e.Koppacyrillic=1152;e.Koppagreek=990;e.Ksicyrillic=1134;e.Ksmall=63339;e.L=76;e.LJ=455;e.LL=63167;e.Lacute=313;e.Lambda=923;e.Lcaron=317;e.Lcedilla=315;e.Lcircle=9409;e.Lcircumflexbelow=7740;e.Lcommaaccent=315;e.Ldot=319;e.Ldotaccent=319;e.Ldotbelow=7734;e.Ldotbelowmacron=7736;e.Liwnarmenian=1340;e.Lj=456;e.Ljecyrillic=1033;e.Llinebelow=7738;e.Lmonospace=65324;e.Lslash=321;e.Lslashsmall=63225;e.Lsmall=63340;e.M=77;e.MBsquare=13190;e.Macron=63184;e.Macronsmall=63407;e.Macute=7742;e.Mcircle=9410;e.Mdotaccent=7744;e.Mdotbelow=7746;e.Menarmenian=1348;e.Mmonospace=65325;e.Msmall=63341;e.Mturned=412;e.Mu=924;e.N=78;e.NJ=458;e.Nacute=323;e.Ncaron=327;e.Ncedilla=325;e.Ncircle=9411;e.Ncircumflexbelow=7754;e.Ncommaaccent=325;e.Ndotaccent=7748;e.Ndotbelow=7750;e.Nhookleft=413;e.Nineroman=8552;e.Nj=459;e.Njecyrillic=1034;e.Nlinebelow=7752;e.Nmonospace=65326;e.Nowarmenian=1350;e.Nsmall=63342;e.Ntilde=209;e.Ntildesmall=63473;e.Nu=925;e.O=79;e.OE=338;e.OEsmall=63226;e.Oacute=211;e.Oacutesmall=63475;e.Obarredcyrillic=1256;e.Obarreddieresiscyrillic=1258;e.Obreve=334;e.Ocaron=465;e.Ocenteredtilde=415;e.Ocircle=9412;e.Ocircumflex=212;e.Ocircumflexacute=7888;e.Ocircumflexdotbelow=7896;e.Ocircumflexgrave=7890;e.Ocircumflexhookabove=7892;e.Ocircumflexsmall=63476;e.Ocircumflextilde=7894;e.Ocyrillic=1054;e.Odblacute=336;e.Odblgrave=524;e.Odieresis=214;e.Odieresiscyrillic=1254;e.Odieresissmall=63478;e.Odotbelow=7884;e.Ogoneksmall=63227;e.Ograve=210;e.Ogravesmall=63474;e.Oharmenian=1365;e.Ohm=8486;e.Ohookabove=7886;e.Ohorn=416;e.Ohornacute=7898;e.Ohorndotbelow=7906;e.Ohorngrave=7900;e.Ohornhookabove=7902;e.Ohorntilde=7904;e.Ohungarumlaut=336;e.Oi=418;e.Oinvertedbreve=526;e.Omacron=332;e.Omacronacute=7762;e.Omacrongrave=7760;e.Omega=8486;e.Omegacyrillic=1120;e.Omegagreek=937;e.Omegaroundcyrillic=1146;e.Omegatitlocyrillic=1148;e.Omegatonos=911;e.Omicron=927;e.Omicrontonos=908;e.Omonospace=65327;e.Oneroman=8544;e.Oogonek=490;e.Oogonekmacron=492;e.Oopen=390;e.Oslash=216;e.Oslashacute=510;e.Oslashsmall=63480;e.Osmall=63343;e.Ostrokeacute=510;e.Otcyrillic=1150;e.Otilde=213;e.Otildeacute=7756;e.Otildedieresis=7758;e.Otildesmall=63477;e.P=80;e.Pacute=7764;e.Pcircle=9413;e.Pdotaccent=7766;e.Pecyrillic=1055;e.Peharmenian=1354;e.Pemiddlehookcyrillic=1190;e.Phi=934;e.Phook=420;e.Pi=928;e.Piwrarmenian=1363;e.Pmonospace=65328;e.Psi=936;e.Psicyrillic=1136;e.Psmall=63344;e.Q=81;e.Qcircle=9414;e.Qmonospace=65329;e.Qsmall=63345;e.R=82;e.Raarmenian=1356;e.Racute=340;e.Rcaron=344;e.Rcedilla=342;e.Rcircle=9415;e.Rcommaaccent=342;e.Rdblgrave=528;e.Rdotaccent=7768;e.Rdotbelow=7770;e.Rdotbelowmacron=7772;e.Reharmenian=1360;e.Rfraktur=8476;e.Rho=929;e.Ringsmall=63228;e.Rinvertedbreve=530;e.Rlinebelow=7774;e.Rmonospace=65330;e.Rsmall=63346;e.Rsmallinverted=641;e.Rsmallinvertedsuperior=694;e.S=83;e.SF010000=9484;e.SF020000=9492;e.SF030000=9488;e.SF040000=9496;e.SF050000=9532;e.SF060000=9516;e.SF070000=9524;e.SF080000=9500;e.SF090000=9508;e.SF100000=9472;e.SF110000=9474;e.SF190000=9569;e.SF200000=9570;e.SF210000=9558;e.SF220000=9557;e.SF230000=9571;e.SF240000=9553;e.SF250000=9559;e.SF260000=9565;e.SF270000=9564;e.SF280000=9563;e.SF360000=9566;e.SF370000=9567;e.SF380000=9562;e.SF390000=9556;e.SF400000=9577;e.SF410000=9574;e.SF420000=9568;e.SF430000=9552;e.SF440000=9580;e.SF450000=9575;e.SF460000=9576;e.SF470000=9572;e.SF480000=9573;e.SF490000=9561;e.SF500000=9560;e.SF510000=9554;e.SF520000=9555;e.SF530000=9579;e.SF540000=9578;e.Sacute=346;e.Sacutedotaccent=7780;e.Sampigreek=992;e.Scaron=352;e.Scarondotaccent=7782;e.Scaronsmall=63229;e.Scedilla=350;e.Schwa=399;e.Schwacyrillic=1240;e.Schwadieresiscyrillic=1242;e.Scircle=9416;e.Scircumflex=348;e.Scommaaccent=536;e.Sdotaccent=7776;e.Sdotbelow=7778;e.Sdotbelowdotaccent=7784;e.Seharmenian=1357;e.Sevenroman=8550;e.Shaarmenian=1351;e.Shacyrillic=1064;e.Shchacyrillic=1065;e.Sheicoptic=994;e.Shhacyrillic=1210;e.Shimacoptic=1004;e.Sigma=931;e.Sixroman=8549;e.Smonospace=65331;e.Softsigncyrillic=1068;e.Ssmall=63347;e.Stigmagreek=986;e.T=84;e.Tau=932;e.Tbar=358;e.Tcaron=356;e.Tcedilla=354;e.Tcircle=9417;e.Tcircumflexbelow=7792;e.Tcommaaccent=354;e.Tdotaccent=7786;e.Tdotbelow=7788;e.Tecyrillic=1058;e.Tedescendercyrillic=1196;e.Tenroman=8553;e.Tetsecyrillic=1204;e.Theta=920;e.Thook=428;e.Thorn=222;e.Thornsmall=63486;e.Threeroman=8546;e.Tildesmall=63230;e.Tiwnarmenian=1359;e.Tlinebelow=7790;e.Tmonospace=65332;e.Toarmenian=1337;e.Tonefive=444;e.Tonesix=388;e.Tonetwo=423;e.Tretroflexhook=430;e.Tsecyrillic=1062;e.Tshecyrillic=1035;e.Tsmall=63348;e.Twelveroman=8555;e.Tworoman=8545;e.U=85;e.Uacute=218;e.Uacutesmall=63482;e.Ubreve=364;e.Ucaron=467;e.Ucircle=9418;e.Ucircumflex=219;e.Ucircumflexbelow=7798;e.Ucircumflexsmall=63483;e.Ucyrillic=1059;e.Udblacute=368;e.Udblgrave=532;e.Udieresis=220;e.Udieresisacute=471;e.Udieresisbelow=7794;e.Udieresiscaron=473;e.Udieresiscyrillic=1264;e.Udieresisgrave=475;e.Udieresismacron=469;e.Udieresissmall=63484;e.Udotbelow=7908;e.Ugrave=217;e.Ugravesmall=63481;e.Uhookabove=7910;e.Uhorn=431;e.Uhornacute=7912;e.Uhorndotbelow=7920;e.Uhorngrave=7914;e.Uhornhookabove=7916;e.Uhorntilde=7918;e.Uhungarumlaut=368;e.Uhungarumlautcyrillic=1266;e.Uinvertedbreve=534;e.Ukcyrillic=1144;e.Umacron=362;e.Umacroncyrillic=1262;e.Umacrondieresis=7802;e.Umonospace=65333;e.Uogonek=370;e.Upsilon=933;e.Upsilon1=978;e.Upsilonacutehooksymbolgreek=979;e.Upsilonafrican=433;e.Upsilondieresis=939;e.Upsilondieresishooksymbolgreek=980;e.Upsilonhooksymbol=978;e.Upsilontonos=910;e.Uring=366;e.Ushortcyrillic=1038;e.Usmall=63349;e.Ustraightcyrillic=1198;e.Ustraightstrokecyrillic=1200;e.Utilde=360;e.Utildeacute=7800;e.Utildebelow=7796;e.V=86;e.Vcircle=9419;e.Vdotbelow=7806;e.Vecyrillic=1042;e.Vewarmenian=1358;e.Vhook=434;e.Vmonospace=65334;e.Voarmenian=1352;e.Vsmall=63350;e.Vtilde=7804;e.W=87;e.Wacute=7810;e.Wcircle=9420;e.Wcircumflex=372;e.Wdieresis=7812;e.Wdotaccent=7814;e.Wdotbelow=7816;e.Wgrave=7808;e.Wmonospace=65335;e.Wsmall=63351;e.X=88;e.Xcircle=9421;e.Xdieresis=7820;e.Xdotaccent=7818;e.Xeharmenian=1341;e.Xi=926;e.Xmonospace=65336;e.Xsmall=63352;e.Y=89;e.Yacute=221;e.Yacutesmall=63485;e.Yatcyrillic=1122;e.Ycircle=9422;e.Ycircumflex=374;e.Ydieresis=376;e.Ydieresissmall=63487;e.Ydotaccent=7822;e.Ydotbelow=7924;e.Yericyrillic=1067;e.Yerudieresiscyrillic=1272;e.Ygrave=7922;e.Yhook=435;e.Yhookabove=7926;e.Yiarmenian=1349;e.Yicyrillic=1031;e.Yiwnarmenian=1362;e.Ymonospace=65337;e.Ysmall=63353;e.Ytilde=7928;e.Yusbigcyrillic=1130;e.Yusbigiotifiedcyrillic=1132;e.Yuslittlecyrillic=1126;e.Yuslittleiotifiedcyrillic=1128;e.Z=90;e.Zaarmenian=1334;e.Zacute=377;e.Zcaron=381;e.Zcaronsmall=63231;e.Zcircle=9423;e.Zcircumflex=7824;e.Zdot=379;e.Zdotaccent=379;e.Zdotbelow=7826;e.Zecyrillic=1047;e.Zedescendercyrillic=1176;e.Zedieresiscyrillic=1246;e.Zeta=918;e.Zhearmenian=1338;e.Zhebrevecyrillic=1217;e.Zhecyrillic=1046;e.Zhedescendercyrillic=1174;e.Zhedieresiscyrillic=1244;e.Zlinebelow=7828;e.Zmonospace=65338;e.Zsmall=63354;e.Zstroke=437;e.a=97;e.aabengali=2438;e.aacute=225;e.aadeva=2310;e.aagujarati=2694;e.aagurmukhi=2566;e.aamatragurmukhi=2622;e.aarusquare=13059;e.aavowelsignbengali=2494;e.aavowelsigndeva=2366;e.aavowelsigngujarati=2750;e.abbreviationmarkarmenian=1375;e.abbreviationsigndeva=2416;e.abengali=2437;e.abopomofo=12570;e.abreve=259;e.abreveacute=7855;e.abrevecyrillic=1233;e.abrevedotbelow=7863;e.abrevegrave=7857;e.abrevehookabove=7859;e.abrevetilde=7861;e.acaron=462;e.acircle=9424;e.acircumflex=226;e.acircumflexacute=7845;e.acircumflexdotbelow=7853;e.acircumflexgrave=7847;e.acircumflexhookabove=7849;e.acircumflextilde=7851;e.acute=180;e.acutebelowcmb=791;e.acutecmb=769;e.acutecomb=769;e.acutedeva=2388;e.acutelowmod=719;e.acutetonecmb=833;e.acyrillic=1072;e.adblgrave=513;e.addakgurmukhi=2673;e.adeva=2309;e.adieresis=228;e.adieresiscyrillic=1235;e.adieresismacron=479;e.adotbelow=7841;e.adotmacron=481;e.ae=230;e.aeacute=509;e.aekorean=12624;e.aemacron=483;e.afii00208=8213;e.afii08941=8356;e.afii10017=1040;e.afii10018=1041;e.afii10019=1042;e.afii10020=1043;e.afii10021=1044;e.afii10022=1045;e.afii10023=1025;e.afii10024=1046;e.afii10025=1047;e.afii10026=1048;e.afii10027=1049;e.afii10028=1050;e.afii10029=1051;e.afii10030=1052;e.afii10031=1053;e.afii10032=1054;e.afii10033=1055;e.afii10034=1056;e.afii10035=1057;e.afii10036=1058;e.afii10037=1059;e.afii10038=1060;e.afii10039=1061;e.afii10040=1062;e.afii10041=1063;e.afii10042=1064;e.afii10043=1065;e.afii10044=1066;e.afii10045=1067;e.afii10046=1068;e.afii10047=1069;e.afii10048=1070;e.afii10049=1071;e.afii10050=1168;e.afii10051=1026;e.afii10052=1027;e.afii10053=1028;e.afii10054=1029;e.afii10055=1030;e.afii10056=1031;e.afii10057=1032;e.afii10058=1033;e.afii10059=1034;e.afii10060=1035;e.afii10061=1036;e.afii10062=1038;e.afii10063=63172;e.afii10064=63173;e.afii10065=1072;e.afii10066=1073;e.afii10067=1074;e.afii10068=1075;e.afii10069=1076;e.afii10070=1077;e.afii10071=1105;e.afii10072=1078;e.afii10073=1079;e.afii10074=1080;e.afii10075=1081;e.afii10076=1082;e.afii10077=1083;e.afii10078=1084;e.afii10079=1085;e.afii10080=1086;e.afii10081=1087;e.afii10082=1088;e.afii10083=1089;e.afii10084=1090;e.afii10085=1091;e.afii10086=1092;e.afii10087=1093;e.afii10088=1094;e.afii10089=1095;e.afii10090=1096;e.afii10091=1097;e.afii10092=1098;e.afii10093=1099;e.afii10094=1100;e.afii10095=1101;e.afii10096=1102;e.afii10097=1103;e.afii10098=1169;e.afii10099=1106;e.afii10100=1107;e.afii10101=1108;e.afii10102=1109;e.afii10103=1110;e.afii10104=1111;e.afii10105=1112;e.afii10106=1113;e.afii10107=1114;e.afii10108=1115;e.afii10109=1116;e.afii10110=1118;e.afii10145=1039;e.afii10146=1122;e.afii10147=1138;e.afii10148=1140;e.afii10192=63174;e.afii10193=1119;e.afii10194=1123;e.afii10195=1139;e.afii10196=1141;e.afii10831=63175;e.afii10832=63176;e.afii10846=1241;e.afii299=8206;e.afii300=8207;e.afii301=8205;e.afii57381=1642;e.afii57388=1548;e.afii57392=1632;e.afii57393=1633;e.afii57394=1634;e.afii57395=1635;e.afii57396=1636;e.afii57397=1637;e.afii57398=1638;e.afii57399=1639;e.afii57400=1640;e.afii57401=1641;e.afii57403=1563;e.afii57407=1567;e.afii57409=1569;e.afii57410=1570;e.afii57411=1571;e.afii57412=1572;e.afii57413=1573;e.afii57414=1574;e.afii57415=1575;e.afii57416=1576;e.afii57417=1577;e.afii57418=1578;e.afii57419=1579;e.afii57420=1580;e.afii57421=1581;e.afii57422=1582;e.afii57423=1583;e.afii57424=1584;e.afii57425=1585;e.afii57426=1586;e.afii57427=1587;e.afii57428=1588;e.afii57429=1589;e.afii57430=1590;e.afii57431=1591;e.afii57432=1592;e.afii57433=1593;e.afii57434=1594;e.afii57440=1600;e.afii57441=1601;e.afii57442=1602;e.afii57443=1603;e.afii57444=1604;e.afii57445=1605;e.afii57446=1606;e.afii57448=1608;e.afii57449=1609;e.afii57450=1610;e.afii57451=1611;e.afii57452=1612;e.afii57453=1613;e.afii57454=1614;e.afii57455=1615;e.afii57456=1616;e.afii57457=1617;e.afii57458=1618;e.afii57470=1607;e.afii57505=1700;e.afii57506=1662;e.afii57507=1670;e.afii57508=1688;e.afii57509=1711;e.afii57511=1657;e.afii57512=1672;e.afii57513=1681;e.afii57514=1722;e.afii57519=1746;e.afii57534=1749;e.afii57636=8362;e.afii57645=1470;e.afii57658=1475;e.afii57664=1488;e.afii57665=1489;e.afii57666=1490;e.afii57667=1491;e.afii57668=1492;e.afii57669=1493;e.afii57670=1494;e.afii57671=1495;e.afii57672=1496;e.afii57673=1497;e.afii57674=1498;e.afii57675=1499;e.afii57676=1500;e.afii57677=1501;e.afii57678=1502;e.afii57679=1503;e.afii57680=1504;e.afii57681=1505;e.afii57682=1506;e.afii57683=1507;e.afii57684=1508;e.afii57685=1509;e.afii57686=1510;e.afii57687=1511;e.afii57688=1512;e.afii57689=1513;e.afii57690=1514;e.afii57694=64298;e.afii57695=64299;e.afii57700=64331;e.afii57705=64287;e.afii57716=1520;e.afii57717=1521;e.afii57718=1522;e.afii57723=64309;e.afii57793=1460;e.afii57794=1461;e.afii57795=1462;e.afii57796=1467;e.afii57797=1464;e.afii57798=1463;e.afii57799=1456;e.afii57800=1458;e.afii57801=1457;e.afii57802=1459;e.afii57803=1474;e.afii57804=1473;e.afii57806=1465;e.afii57807=1468;e.afii57839=1469;e.afii57841=1471;e.afii57842=1472;e.afii57929=700;e.afii61248=8453;e.afii61289=8467;e.afii61352=8470;e.afii61573=8236;e.afii61574=8237;e.afii61575=8238;e.afii61664=8204;e.afii63167=1645;e.afii64937=701;e.agrave=224;e.agujarati=2693;e.agurmukhi=2565;e.ahiragana=12354;e.ahookabove=7843;e.aibengali=2448;e.aibopomofo=12574;e.aideva=2320;e.aiecyrillic=1237;e.aigujarati=2704;e.aigurmukhi=2576;e.aimatragurmukhi=2632;e.ainarabic=1593;e.ainfinalarabic=65226;e.aininitialarabic=65227;e.ainmedialarabic=65228;e.ainvertedbreve=515;e.aivowelsignbengali=2504;e.aivowelsigndeva=2376;e.aivowelsigngujarati=2760;e.akatakana=12450;e.akatakanahalfwidth=65393;e.akorean=12623;e.alef=1488;e.alefarabic=1575;e.alefdageshhebrew=64304;e.aleffinalarabic=65166;e.alefhamzaabovearabic=1571;e.alefhamzaabovefinalarabic=65156;e.alefhamzabelowarabic=1573;e.alefhamzabelowfinalarabic=65160;e.alefhebrew=1488;e.aleflamedhebrew=64335;e.alefmaddaabovearabic=1570;e.alefmaddaabovefinalarabic=65154;e.alefmaksuraarabic=1609;e.alefmaksurafinalarabic=65264;e.alefmaksurainitialarabic=65267;e.alefmaksuramedialarabic=65268;e.alefpatahhebrew=64302;e.alefqamatshebrew=64303;e.aleph=8501;e.allequal=8780;e.alpha=945;e.alphatonos=940;e.amacron=257;e.amonospace=65345;e.ampersand=38;e.ampersandmonospace=65286;e.ampersandsmall=63270;e.amsquare=13250;e.anbopomofo=12578;e.angbopomofo=12580;e.angbracketleft=12296;e.angbracketright=12297;e.angkhankhuthai=3674;e.angle=8736;e.anglebracketleft=12296;e.anglebracketleftvertical=65087;e.anglebracketright=12297;e.anglebracketrightvertical=65088;e.angleleft=9001;e.angleright=9002;e.angstrom=8491;e.anoteleia=903;e.anudattadeva=2386;e.anusvarabengali=2434;e.anusvaradeva=2306;e.anusvaragujarati=2690;e.aogonek=261;e.apaatosquare=13056;e.aparen=9372;e.apostrophearmenian=1370;e.apostrophemod=700;e.apple=63743;e.approaches=8784;e.approxequal=8776;e.approxequalorimage=8786;e.approximatelyequal=8773;e.araeaekorean=12686;e.araeakorean=12685;e.arc=8978;e.arighthalfring=7834;e.aring=229;e.aringacute=507;e.aringbelow=7681;e.arrowboth=8596;e.arrowdashdown=8675;e.arrowdashleft=8672;e.arrowdashright=8674;e.arrowdashup=8673;e.arrowdblboth=8660;e.arrowdbldown=8659;e.arrowdblleft=8656;e.arrowdblright=8658;e.arrowdblup=8657;e.arrowdown=8595;e.arrowdownleft=8601;e.arrowdownright=8600;e.arrowdownwhite=8681;e.arrowheaddownmod=709;e.arrowheadleftmod=706;e.arrowheadrightmod=707;e.arrowheadupmod=708;e.arrowhorizex=63719;e.arrowleft=8592;e.arrowleftdbl=8656;e.arrowleftdblstroke=8653;e.arrowleftoverright=8646;e.arrowleftwhite=8678;e.arrowright=8594;e.arrowrightdblstroke=8655;e.arrowrightheavy=10142;e.arrowrightoverleft=8644;e.arrowrightwhite=8680;e.arrowtableft=8676;e.arrowtabright=8677;e.arrowup=8593;e.arrowupdn=8597;e.arrowupdnbse=8616;e.arrowupdownbase=8616;e.arrowupleft=8598;e.arrowupleftofdown=8645;e.arrowupright=8599;e.arrowupwhite=8679;e.arrowvertex=63718;e.asciicircum=94;e.asciicircummonospace=65342;e.asciitilde=126;e.asciitildemonospace=65374;e.ascript=593;e.ascriptturned=594;e.asmallhiragana=12353;e.asmallkatakana=12449;e.asmallkatakanahalfwidth=65383;e.asterisk=42;e.asteriskaltonearabic=1645;e.asteriskarabic=1645;e.asteriskmath=8727;e.asteriskmonospace=65290;e.asterisksmall=65121;e.asterism=8258;e.asuperior=63209;e.asymptoticallyequal=8771;e.at=64;e.atilde=227;e.atmonospace=65312;e.atsmall=65131;e.aturned=592;e.aubengali=2452;e.aubopomofo=12576;e.audeva=2324;e.augujarati=2708;e.augurmukhi=2580;e.aulengthmarkbengali=2519;e.aumatragurmukhi=2636;e.auvowelsignbengali=2508;e.auvowelsigndeva=2380;e.auvowelsigngujarati=2764;e.avagrahadeva=2365;e.aybarmenian=1377;e.ayin=1506;e.ayinaltonehebrew=64288;e.ayinhebrew=1506;e.b=98;e.babengali=2476;e.backslash=92;e.backslashmonospace=65340;e.badeva=2348;e.bagujarati=2732;e.bagurmukhi=2604;e.bahiragana=12400;e.bahtthai=3647;e.bakatakana=12496;e.bar=124;e.barmonospace=65372;e.bbopomofo=12549;e.bcircle=9425;e.bdotaccent=7683;e.bdotbelow=7685;e.beamedsixteenthnotes=9836;e.because=8757;e.becyrillic=1073;e.beharabic=1576;e.behfinalarabic=65168;e.behinitialarabic=65169;e.behiragana=12409;e.behmedialarabic=65170;e.behmeeminitialarabic=64671;e.behmeemisolatedarabic=64520;e.behnoonfinalarabic=64621;e.bekatakana=12505;e.benarmenian=1378;e.bet=1489;e.beta=946;e.betasymbolgreek=976;e.betdagesh=64305;e.betdageshhebrew=64305;e.bethebrew=1489;e.betrafehebrew=64332;e.bhabengali=2477;e.bhadeva=2349;e.bhagujarati=2733;e.bhagurmukhi=2605;e.bhook=595;e.bihiragana=12403;e.bikatakana=12499;e.bilabialclick=664;e.bindigurmukhi=2562;e.birusquare=13105;e.blackcircle=9679;e.blackdiamond=9670;e.blackdownpointingtriangle=9660;e.blackleftpointingpointer=9668;e.blackleftpointingtriangle=9664;e.blacklenticularbracketleft=12304;e.blacklenticularbracketleftvertical=65083;e.blacklenticularbracketright=12305;e.blacklenticularbracketrightvertical=65084;e.blacklowerlefttriangle=9699;e.blacklowerrighttriangle=9698;e.blackrectangle=9644;e.blackrightpointingpointer=9658;e.blackrightpointingtriangle=9654;e.blacksmallsquare=9642;e.blacksmilingface=9787;e.blacksquare=9632;e.blackstar=9733;e.blackupperlefttriangle=9700;e.blackupperrighttriangle=9701;e.blackuppointingsmalltriangle=9652;e.blackuppointingtriangle=9650;e.blank=9251;e.blinebelow=7687;e.block=9608;e.bmonospace=65346;e.bobaimaithai=3610;e.bohiragana=12412;e.bokatakana=12508;e.bparen=9373;e.bqsquare=13251;e.braceex=63732;e.braceleft=123;e.braceleftbt=63731;e.braceleftmid=63730;e.braceleftmonospace=65371;e.braceleftsmall=65115;e.bracelefttp=63729;e.braceleftvertical=65079;e.braceright=125;e.bracerightbt=63742;e.bracerightmid=63741;e.bracerightmonospace=65373;e.bracerightsmall=65116;e.bracerighttp=63740;e.bracerightvertical=65080;e.bracketleft=91;e.bracketleftbt=63728;e.bracketleftex=63727;e.bracketleftmonospace=65339;e.bracketlefttp=63726;e.bracketright=93;e.bracketrightbt=63739;e.bracketrightex=63738;e.bracketrightmonospace=65341;e.bracketrighttp=63737;e.breve=728;e.brevebelowcmb=814;e.brevecmb=774;e.breveinvertedbelowcmb=815;e.breveinvertedcmb=785;e.breveinverteddoublecmb=865;e.bridgebelowcmb=810;e.bridgeinvertedbelowcmb=826;e.brokenbar=166;e.bstroke=384;e.bsuperior=63210;e.btopbar=387;e.buhiragana=12406;e.bukatakana=12502;e.bullet=8226;e.bulletinverse=9688;e.bulletoperator=8729;e.bullseye=9678;e.c=99;e.caarmenian=1390;e.cabengali=2458;e.cacute=263;e.cadeva=2330;e.cagujarati=2714;e.cagurmukhi=2586;e.calsquare=13192;e.candrabindubengali=2433;e.candrabinducmb=784;e.candrabindudeva=2305;e.candrabindugujarati=2689;e.capslock=8682;e.careof=8453;e.caron=711;e.caronbelowcmb=812;e.caroncmb=780;e.carriagereturn=8629;e.cbopomofo=12568;e.ccaron=269;e.ccedilla=231;e.ccedillaacute=7689;e.ccircle=9426;e.ccircumflex=265;e.ccurl=597;e.cdot=267;e.cdotaccent=267;e.cdsquare=13253;e.cedilla=184;e.cedillacmb=807;e.cent=162;e.centigrade=8451;e.centinferior=63199;e.centmonospace=65504;e.centoldstyle=63394;e.centsuperior=63200;e.chaarmenian=1401;e.chabengali=2459;e.chadeva=2331;e.chagujarati=2715;e.chagurmukhi=2587;e.chbopomofo=12564;e.cheabkhasiancyrillic=1213;e.checkmark=10003;e.checyrillic=1095;e.chedescenderabkhasiancyrillic=1215;e.chedescendercyrillic=1207;e.chedieresiscyrillic=1269;e.cheharmenian=1395;e.chekhakassiancyrillic=1228;e.cheverticalstrokecyrillic=1209;e.chi=967;e.chieuchacirclekorean=12919;e.chieuchaparenkorean=12823;e.chieuchcirclekorean=12905;e.chieuchkorean=12618;e.chieuchparenkorean=12809;e.chochangthai=3594;e.chochanthai=3592;e.chochingthai=3593;e.chochoethai=3596;e.chook=392;e.cieucacirclekorean=12918;e.cieucaparenkorean=12822;e.cieuccirclekorean=12904;e.cieuckorean=12616;e.cieucparenkorean=12808;e.cieucuparenkorean=12828;e.circle=9675;e.circlecopyrt=169;e.circlemultiply=8855;e.circleot=8857;e.circleplus=8853;e.circlepostalmark=12342;e.circlewithlefthalfblack=9680;e.circlewithrighthalfblack=9681;e.circumflex=710;e.circumflexbelowcmb=813;e.circumflexcmb=770;e.clear=8999;e.clickalveolar=450;e.clickdental=448;e.clicklateral=449;e.clickretroflex=451;e.club=9827;e.clubsuitblack=9827;e.clubsuitwhite=9831;e.cmcubedsquare=13220;e.cmonospace=65347;e.cmsquaredsquare=13216;e.coarmenian=1409;e.colon=58;e.colonmonetary=8353;e.colonmonospace=65306;e.colonsign=8353;e.colonsmall=65109;e.colontriangularhalfmod=721;e.colontriangularmod=720;e.comma=44;e.commaabovecmb=787;e.commaaboverightcmb=789;e.commaaccent=63171;e.commaarabic=1548;e.commaarmenian=1373;e.commainferior=63201;e.commamonospace=65292;e.commareversedabovecmb=788;e.commareversedmod=701;e.commasmall=65104;e.commasuperior=63202;e.commaturnedabovecmb=786;e.commaturnedmod=699;e.compass=9788;e.congruent=8773;e.contourintegral=8750;e.control=8963;e.controlACK=6;e.controlBEL=7;e.controlBS=8;e.controlCAN=24;e.controlCR=13;e.controlDC1=17;e.controlDC2=18;e.controlDC3=19;e.controlDC4=20;e.controlDEL=127;e.controlDLE=16;e.controlEM=25;e.controlENQ=5;e.controlEOT=4;e.controlESC=27;e.controlETB=23;e.controlETX=3;e.controlFF=12;e.controlFS=28;e.controlGS=29;e.controlHT=9;e.controlLF=10;e.controlNAK=21;e.controlNULL=0;e.controlRS=30;e.controlSI=15;e.controlSO=14;e.controlSOT=2;e.controlSTX=1;e.controlSUB=26;e.controlSYN=22;e.controlUS=31;e.controlVT=11;e.copyright=169;e.copyrightsans=63721;e.copyrightserif=63193;e.cornerbracketleft=12300;e.cornerbracketlefthalfwidth=65378;e.cornerbracketleftvertical=65089;e.cornerbracketright=12301;e.cornerbracketrighthalfwidth=65379;e.cornerbracketrightvertical=65090;e.corporationsquare=13183;e.cosquare=13255;e.coverkgsquare=13254;e.cparen=9374;e.cruzeiro=8354;e.cstretched=663;e.curlyand=8911;e.curlyor=8910;e.currency=164;e.cyrBreve=63185;e.cyrFlex=63186;e.cyrbreve=63188;e.cyrflex=63189;e.d=100;e.daarmenian=1380;e.dabengali=2470;e.dadarabic=1590;e.dadeva=2342;e.dadfinalarabic=65214;e.dadinitialarabic=65215;e.dadmedialarabic=65216;e.dagesh=1468;e.dageshhebrew=1468;e.dagger=8224;e.daggerdbl=8225;e.dagujarati=2726;e.dagurmukhi=2598;e.dahiragana=12384;e.dakatakana=12480;e.dalarabic=1583;e.dalet=1491;e.daletdagesh=64307;e.daletdageshhebrew=64307;e.dalethebrew=1491;e.dalfinalarabic=65194;e.dammaarabic=1615;e.dammalowarabic=1615;e.dammatanaltonearabic=1612;e.dammatanarabic=1612;e.danda=2404;e.dargahebrew=1447;e.dargalefthebrew=1447;e.dasiapneumatacyrilliccmb=1157;e.dblGrave=63187;e.dblanglebracketleft=12298;e.dblanglebracketleftvertical=65085;e.dblanglebracketright=12299;e.dblanglebracketrightvertical=65086;e.dblarchinvertedbelowcmb=811;e.dblarrowleft=8660;e.dblarrowright=8658;e.dbldanda=2405;e.dblgrave=63190;e.dblgravecmb=783;e.dblintegral=8748;e.dbllowline=8215;e.dbllowlinecmb=819;e.dbloverlinecmb=831;e.dblprimemod=698;e.dblverticalbar=8214;e.dblverticallineabovecmb=782;e.dbopomofo=12553;e.dbsquare=13256;e.dcaron=271;e.dcedilla=7697;e.dcircle=9427;e.dcircumflexbelow=7699;e.dcroat=273;e.ddabengali=2465;e.ddadeva=2337;e.ddagujarati=2721;e.ddagurmukhi=2593;e.ddalarabic=1672;e.ddalfinalarabic=64393;e.dddhadeva=2396;e.ddhabengali=2466;e.ddhadeva=2338;e.ddhagujarati=2722;e.ddhagurmukhi=2594;e.ddotaccent=7691;e.ddotbelow=7693;e.decimalseparatorarabic=1643;e.decimalseparatorpersian=1643;e.decyrillic=1076;e.degree=176;e.dehihebrew=1453;e.dehiragana=12391;e.deicoptic=1007;e.dekatakana=12487;e.deleteleft=9003;e.deleteright=8998;e.delta=948;e.deltaturned=397;e.denominatorminusonenumeratorbengali=2552;e.dezh=676;e.dhabengali=2471;e.dhadeva=2343;e.dhagujarati=2727;e.dhagurmukhi=2599;e.dhook=599;e.dialytikatonos=901;e.dialytikatonoscmb=836;e.diamond=9830;e.diamondsuitwhite=9826;e.dieresis=168;e.dieresisacute=63191;e.dieresisbelowcmb=804;e.dieresiscmb=776;e.dieresisgrave=63192;e.dieresistonos=901;e.dihiragana=12386;e.dikatakana=12482;e.dittomark=12291;e.divide=247;e.divides=8739;e.divisionslash=8725;e.djecyrillic=1106;e.dkshade=9619;e.dlinebelow=7695;e.dlsquare=13207;e.dmacron=273;e.dmonospace=65348;e.dnblock=9604;e.dochadathai=3598;e.dodekthai=3604;e.dohiragana=12393;e.dokatakana=12489;e.dollar=36;e.dollarinferior=63203;e.dollarmonospace=65284;e.dollaroldstyle=63268;e.dollarsmall=65129;e.dollarsuperior=63204;e.dong=8363;e.dorusquare=13094;e.dotaccent=729;e.dotaccentcmb=775;e.dotbelowcmb=803;e.dotbelowcomb=803;e.dotkatakana=12539;e.dotlessi=305;e.dotlessj=63166;e.dotlessjstrokehook=644;e.dotmath=8901;e.dottedcircle=9676;e.doubleyodpatah=64287;e.doubleyodpatahhebrew=64287;e.downtackbelowcmb=798;e.downtackmod=725;e.dparen=9375;e.dsuperior=63211;e.dtail=598;e.dtopbar=396;e.duhiragana=12389;e.dukatakana=12485;e.dz=499;e.dzaltone=675;e.dzcaron=454;e.dzcurl=677;e.dzeabkhasiancyrillic=1249;e.dzecyrillic=1109;e.dzhecyrillic=1119;e.e=101;e.eacute=233;e.earth=9793;e.ebengali=2447;e.ebopomofo=12572;e.ebreve=277;e.ecandradeva=2317;e.ecandragujarati=2701;e.ecandravowelsigndeva=2373;e.ecandravowelsigngujarati=2757;e.ecaron=283;e.ecedillabreve=7709;e.echarmenian=1381;e.echyiwnarmenian=1415;e.ecircle=9428;e.ecircumflex=234;e.ecircumflexacute=7871;e.ecircumflexbelow=7705;e.ecircumflexdotbelow=7879;e.ecircumflexgrave=7873;e.ecircumflexhookabove=7875;e.ecircumflextilde=7877;e.ecyrillic=1108;e.edblgrave=517;e.edeva=2319;e.edieresis=235;e.edot=279;e.edotaccent=279;e.edotbelow=7865;e.eegurmukhi=2575;e.eematragurmukhi=2631;e.efcyrillic=1092;e.egrave=232;e.egujarati=2703;e.eharmenian=1383;e.ehbopomofo=12573;e.ehiragana=12360;e.ehookabove=7867;e.eibopomofo=12575;e.eight=56;e.eightarabic=1640;e.eightbengali=2542;e.eightcircle=9319;e.eightcircleinversesansserif=10129;e.eightdeva=2414;e.eighteencircle=9329;e.eighteenparen=9349;e.eighteenperiod=9369;e.eightgujarati=2798;e.eightgurmukhi=2670;e.eighthackarabic=1640;e.eighthangzhou=12328;e.eighthnotebeamed=9835;e.eightideographicparen=12839;e.eightinferior=8328;e.eightmonospace=65304;e.eightoldstyle=63288;e.eightparen=9339;e.eightperiod=9359;e.eightpersian=1784;e.eightroman=8567;e.eightsuperior=8312;e.eightthai=3672;e.einvertedbreve=519;e.eiotifiedcyrillic=1125;e.ekatakana=12456;e.ekatakanahalfwidth=65396;e.ekonkargurmukhi=2676;e.ekorean=12628;e.elcyrillic=1083;e.element=8712;e.elevencircle=9322;e.elevenparen=9342;e.elevenperiod=9362;e.elevenroman=8570;e.ellipsis=8230;e.ellipsisvertical=8942;e.emacron=275;e.emacronacute=7703;e.emacrongrave=7701;e.emcyrillic=1084;e.emdash=8212;e.emdashvertical=65073;e.emonospace=65349;e.emphasismarkarmenian=1371;e.emptyset=8709;e.enbopomofo=12579;e.encyrillic=1085;e.endash=8211;e.endashvertical=65074;e.endescendercyrillic=1187;e.eng=331;e.engbopomofo=12581;e.enghecyrillic=1189;e.enhookcyrillic=1224;e.enspace=8194;e.eogonek=281;e.eokorean=12627;e.eopen=603;e.eopenclosed=666;e.eopenreversed=604;e.eopenreversedclosed=606;e.eopenreversedhook=605;e.eparen=9376;e.epsilon=949;e.epsilontonos=941;e.equal=61;e.equalmonospace=65309;e.equalsmall=65126;e.equalsuperior=8316;e.equivalence=8801;e.erbopomofo=12582;e.ercyrillic=1088;e.ereversed=600;e.ereversedcyrillic=1101;e.escyrillic=1089;e.esdescendercyrillic=1195;e.esh=643;e.eshcurl=646;e.eshortdeva=2318;e.eshortvowelsigndeva=2374;e.eshreversedloop=426;e.eshsquatreversed=645;e.esmallhiragana=12359;e.esmallkatakana=12455;e.esmallkatakanahalfwidth=65386;e.estimated=8494;e.esuperior=63212;e.eta=951;e.etarmenian=1384;e.etatonos=942;e.eth=240;e.etilde=7869;e.etildebelow=7707;e.etnahtafoukhhebrew=1425;e.etnahtafoukhlefthebrew=1425;e.etnahtahebrew=1425;e.etnahtalefthebrew=1425;e.eturned=477;e.eukorean=12641;e.euro=8364;e.evowelsignbengali=2503;e.evowelsigndeva=2375;e.evowelsigngujarati=2759;e.exclam=33;e.exclamarmenian=1372;e.exclamdbl=8252;e.exclamdown=161;e.exclamdownsmall=63393;e.exclammonospace=65281;e.exclamsmall=63265;e.existential=8707;e.ezh=658;e.ezhcaron=495;e.ezhcurl=659;e.ezhreversed=441;e.ezhtail=442;e.f=102;e.fadeva=2398;e.fagurmukhi=2654;e.fahrenheit=8457;e.fathaarabic=1614;e.fathalowarabic=1614;e.fathatanarabic=1611;e.fbopomofo=12552;e.fcircle=9429;e.fdotaccent=7711;e.feharabic=1601;e.feharmenian=1414;e.fehfinalarabic=65234;e.fehinitialarabic=65235;e.fehmedialarabic=65236;e.feicoptic=997;e.female=9792;e.ff=64256;e.f_f=64256;e.ffi=64259;e.f_f_i=64259;e.ffl=64260;e.f_f_l=64260;e.fi=64257;e.f_i=64257;e.fifteencircle=9326;e.fifteenparen=9346;e.fifteenperiod=9366;e.figuredash=8210;e.filledbox=9632;e.filledrect=9644;e.finalkaf=1498;e.finalkafdagesh=64314;e.finalkafdageshhebrew=64314;e.finalkafhebrew=1498;e.finalmem=1501;e.finalmemhebrew=1501;e.finalnun=1503;e.finalnunhebrew=1503;e.finalpe=1507;e.finalpehebrew=1507;e.finaltsadi=1509;e.finaltsadihebrew=1509;e.firsttonechinese=713;e.fisheye=9673;e.fitacyrillic=1139;e.five=53;e.fivearabic=1637;e.fivebengali=2539;e.fivecircle=9316;e.fivecircleinversesansserif=10126;e.fivedeva=2411;e.fiveeighths=8541;e.fivegujarati=2795;e.fivegurmukhi=2667;e.fivehackarabic=1637;e.fivehangzhou=12325;e.fiveideographicparen=12836;e.fiveinferior=8325;e.fivemonospace=65301;e.fiveoldstyle=63285;e.fiveparen=9336;e.fiveperiod=9356;e.fivepersian=1781;e.fiveroman=8564;e.fivesuperior=8309;e.fivethai=3669;e.fl=64258;e.f_l=64258;e.florin=402;e.fmonospace=65350;e.fmsquare=13209;e.fofanthai=3615;e.fofathai=3613;e.fongmanthai=3663;e.forall=8704;e.four=52;e.fourarabic=1636;e.fourbengali=2538;e.fourcircle=9315;e.fourcircleinversesansserif=10125;e.fourdeva=2410;e.fourgujarati=2794;e.fourgurmukhi=2666;e.fourhackarabic=1636;e.fourhangzhou=12324;e.fourideographicparen=12835;e.fourinferior=8324;e.fourmonospace=65300;e.fournumeratorbengali=2551;e.fouroldstyle=63284;e.fourparen=9335;e.fourperiod=9355;e.fourpersian=1780;e.fourroman=8563;e.foursuperior=8308;e.fourteencircle=9325;e.fourteenparen=9345;e.fourteenperiod=9365;e.fourthai=3668;e.fourthtonechinese=715;e.fparen=9377;e.fraction=8260;e.franc=8355;e.g=103;e.gabengali=2455;e.gacute=501;e.gadeva=2327;e.gafarabic=1711;e.gaffinalarabic=64403;e.gafinitialarabic=64404;e.gafmedialarabic=64405;e.gagujarati=2711;e.gagurmukhi=2583;e.gahiragana=12364;e.gakatakana=12460;e.gamma=947;e.gammalatinsmall=611;e.gammasuperior=736;e.gangiacoptic=1003;e.gbopomofo=12557;e.gbreve=287;e.gcaron=487;e.gcedilla=291;e.gcircle=9430;e.gcircumflex=285;e.gcommaaccent=291;e.gdot=289;e.gdotaccent=289;e.gecyrillic=1075;e.gehiragana=12370;e.gekatakana=12466;e.geometricallyequal=8785;e.gereshaccenthebrew=1436;e.gereshhebrew=1523;e.gereshmuqdamhebrew=1437;e.germandbls=223;e.gershayimaccenthebrew=1438;e.gershayimhebrew=1524;e.getamark=12307;e.ghabengali=2456;e.ghadarmenian=1394;e.ghadeva=2328;e.ghagujarati=2712;e.ghagurmukhi=2584;e.ghainarabic=1594;e.ghainfinalarabic=65230;e.ghaininitialarabic=65231;e.ghainmedialarabic=65232;e.ghemiddlehookcyrillic=1173;e.ghestrokecyrillic=1171;e.gheupturncyrillic=1169;e.ghhadeva=2394;e.ghhagurmukhi=2650;e.ghook=608;e.ghzsquare=13203;e.gihiragana=12366;e.gikatakana=12462;e.gimarmenian=1379;e.gimel=1490;e.gimeldagesh=64306;e.gimeldageshhebrew=64306;e.gimelhebrew=1490;e.gjecyrillic=1107;e.glottalinvertedstroke=446;e.glottalstop=660;e.glottalstopinverted=662;e.glottalstopmod=704;e.glottalstopreversed=661;e.glottalstopreversedmod=705;e.glottalstopreversedsuperior=740;e.glottalstopstroke=673;e.glottalstopstrokereversed=674;e.gmacron=7713;e.gmonospace=65351;e.gohiragana=12372;e.gokatakana=12468;e.gparen=9378;e.gpasquare=13228;e.gradient=8711;e.grave=96;e.gravebelowcmb=790;e.gravecmb=768;e.gravecomb=768;e.gravedeva=2387;e.gravelowmod=718;e.gravemonospace=65344;e.gravetonecmb=832;e.greater=62;e.greaterequal=8805;e.greaterequalorless=8923;e.greatermonospace=65310;e.greaterorequivalent=8819;e.greaterorless=8823;e.greateroverequal=8807;e.greatersmall=65125;e.gscript=609;e.gstroke=485;e.guhiragana=12368;e.guillemotleft=171;e.guillemotright=187;e.guilsinglleft=8249;e.guilsinglright=8250;e.gukatakana=12464;e.guramusquare=13080;e.gysquare=13257;e.h=104;e.haabkhasiancyrillic=1193;e.haaltonearabic=1729;e.habengali=2489;e.hadescendercyrillic=1203;e.hadeva=2361;e.hagujarati=2745;e.hagurmukhi=2617;e.haharabic=1581;e.hahfinalarabic=65186;e.hahinitialarabic=65187;e.hahiragana=12399;e.hahmedialarabic=65188;e.haitusquare=13098;e.hakatakana=12495;e.hakatakanahalfwidth=65418;e.halantgurmukhi=2637;e.hamzaarabic=1569;e.hamzalowarabic=1569;e.hangulfiller=12644;e.hardsigncyrillic=1098;e.harpoonleftbarbup=8636;e.harpoonrightbarbup=8640;e.hasquare=13258;e.hatafpatah=1458;e.hatafpatah16=1458;e.hatafpatah23=1458;e.hatafpatah2f=1458;e.hatafpatahhebrew=1458;e.hatafpatahnarrowhebrew=1458;e.hatafpatahquarterhebrew=1458;e.hatafpatahwidehebrew=1458;e.hatafqamats=1459;e.hatafqamats1b=1459;e.hatafqamats28=1459;e.hatafqamats34=1459;e.hatafqamatshebrew=1459;e.hatafqamatsnarrowhebrew=1459;e.hatafqamatsquarterhebrew=1459;e.hatafqamatswidehebrew=1459;e.hatafsegol=1457;e.hatafsegol17=1457;e.hatafsegol24=1457;e.hatafsegol30=1457;e.hatafsegolhebrew=1457;e.hatafsegolnarrowhebrew=1457;e.hatafsegolquarterhebrew=1457;e.hatafsegolwidehebrew=1457;e.hbar=295;e.hbopomofo=12559;e.hbrevebelow=7723;e.hcedilla=7721;e.hcircle=9431;e.hcircumflex=293;e.hdieresis=7719;e.hdotaccent=7715;e.hdotbelow=7717;e.he=1492;e.heart=9829;e.heartsuitblack=9829;e.heartsuitwhite=9825;e.hedagesh=64308;e.hedageshhebrew=64308;e.hehaltonearabic=1729;e.heharabic=1607;e.hehebrew=1492;e.hehfinalaltonearabic=64423;e.hehfinalalttwoarabic=65258;e.hehfinalarabic=65258;e.hehhamzaabovefinalarabic=64421;e.hehhamzaaboveisolatedarabic=64420;e.hehinitialaltonearabic=64424;e.hehinitialarabic=65259;e.hehiragana=12408;e.hehmedialaltonearabic=64425;e.hehmedialarabic=65260;e.heiseierasquare=13179;e.hekatakana=12504;e.hekatakanahalfwidth=65421;e.hekutaarusquare=13110;e.henghook=615;e.herutusquare=13113;e.het=1495;e.hethebrew=1495;e.hhook=614;e.hhooksuperior=689;e.hieuhacirclekorean=12923;e.hieuhaparenkorean=12827;e.hieuhcirclekorean=12909;e.hieuhkorean=12622;e.hieuhparenkorean=12813;e.hihiragana=12402;e.hikatakana=12498;e.hikatakanahalfwidth=65419;e.hiriq=1460;e.hiriq14=1460;e.hiriq21=1460;e.hiriq2d=1460;e.hiriqhebrew=1460;e.hiriqnarrowhebrew=1460;e.hiriqquarterhebrew=1460;e.hiriqwidehebrew=1460;e.hlinebelow=7830;e.hmonospace=65352;e.hoarmenian=1392;e.hohipthai=3627;e.hohiragana=12411;e.hokatakana=12507;e.hokatakanahalfwidth=65422;e.holam=1465;e.holam19=1465;e.holam26=1465;e.holam32=1465;e.holamhebrew=1465;e.holamnarrowhebrew=1465;e.holamquarterhebrew=1465;e.holamwidehebrew=1465;e.honokhukthai=3630;e.hookabovecomb=777;e.hookcmb=777;e.hookpalatalizedbelowcmb=801;e.hookretroflexbelowcmb=802;e.hoonsquare=13122;e.horicoptic=1001;e.horizontalbar=8213;e.horncmb=795;e.hotsprings=9832;e.house=8962;e.hparen=9379;e.hsuperior=688;e.hturned=613;e.huhiragana=12405;e.huiitosquare=13107;e.hukatakana=12501;e.hukatakanahalfwidth=65420;e.hungarumlaut=733;e.hungarumlautcmb=779;e.hv=405;e.hyphen=45;e.hypheninferior=63205;e.hyphenmonospace=65293;e.hyphensmall=65123;e.hyphensuperior=63206;e.hyphentwo=8208;e.i=105;e.iacute=237;e.iacyrillic=1103;e.ibengali=2439;e.ibopomofo=12583;e.ibreve=301;e.icaron=464;e.icircle=9432;e.icircumflex=238;e.icyrillic=1110;e.idblgrave=521;e.ideographearthcircle=12943;e.ideographfirecircle=12939;e.ideographicallianceparen=12863;e.ideographiccallparen=12858;e.ideographiccentrecircle=12965;e.ideographicclose=12294;e.ideographiccomma=12289;e.ideographiccommaleft=65380;e.ideographiccongratulationparen=12855;e.ideographiccorrectcircle=12963;e.ideographicearthparen=12847;e.ideographicenterpriseparen=12861;e.ideographicexcellentcircle=12957;e.ideographicfestivalparen=12864;e.ideographicfinancialcircle=12950;e.ideographicfinancialparen=12854;e.ideographicfireparen=12843;e.ideographichaveparen=12850;e.ideographichighcircle=12964;e.ideographiciterationmark=12293;e.ideographiclaborcircle=12952;e.ideographiclaborparen=12856;e.ideographicleftcircle=12967;e.ideographiclowcircle=12966;e.ideographicmedicinecircle=12969;e.ideographicmetalparen=12846;e.ideographicmoonparen=12842;e.ideographicnameparen=12852;e.ideographicperiod=12290;e.ideographicprintcircle=12958;e.ideographicreachparen=12867;e.ideographicrepresentparen=12857;e.ideographicresourceparen=12862;e.ideographicrightcircle=12968;e.ideographicsecretcircle=12953;e.ideographicselfparen=12866;e.ideographicsocietyparen=12851;e.ideographicspace=12288;e.ideographicspecialparen=12853;e.ideographicstockparen=12849;e.ideographicstudyparen=12859;e.ideographicsunparen=12848;e.ideographicsuperviseparen=12860;e.ideographicwaterparen=12844;e.ideographicwoodparen=12845;e.ideographiczero=12295;e.ideographmetalcircle=12942;e.ideographmooncircle=12938;e.ideographnamecircle=12948;e.ideographsuncircle=12944;e.ideographwatercircle=12940;e.ideographwoodcircle=12941;e.ideva=2311;e.idieresis=239;e.idieresisacute=7727;e.idieresiscyrillic=1253;e.idotbelow=7883;e.iebrevecyrillic=1239;e.iecyrillic=1077;e.ieungacirclekorean=12917;e.ieungaparenkorean=12821;e.ieungcirclekorean=12903;e.ieungkorean=12615;e.ieungparenkorean=12807;e.igrave=236;e.igujarati=2695;e.igurmukhi=2567;e.ihiragana=12356;e.ihookabove=7881;e.iibengali=2440;e.iicyrillic=1080;e.iideva=2312;e.iigujarati=2696;e.iigurmukhi=2568;e.iimatragurmukhi=2624;e.iinvertedbreve=523;e.iishortcyrillic=1081;e.iivowelsignbengali=2496;e.iivowelsigndeva=2368;e.iivowelsigngujarati=2752;e.ij=307;e.ikatakana=12452;e.ikatakanahalfwidth=65394;e.ikorean=12643;e.ilde=732;e.iluyhebrew=1452;e.imacron=299;e.imacroncyrillic=1251;e.imageorapproximatelyequal=8787;e.imatragurmukhi=2623;e.imonospace=65353;e.increment=8710;e.infinity=8734;e.iniarmenian=1387;e.integral=8747;e.integralbottom=8993;e.integralbt=8993;e.integralex=63733;e.integraltop=8992;e.integraltp=8992;e.intersection=8745;e.intisquare=13061;e.invbullet=9688;e.invcircle=9689;e.invsmileface=9787;e.iocyrillic=1105;e.iogonek=303;e.iota=953;e.iotadieresis=970;e.iotadieresistonos=912;e.iotalatin=617;e.iotatonos=943;e.iparen=9380;e.irigurmukhi=2674;e.ismallhiragana=12355;e.ismallkatakana=12451;e.ismallkatakanahalfwidth=65384;e.issharbengali=2554;e.istroke=616;e.isuperior=63213;e.iterationhiragana=12445;e.iterationkatakana=12541;e.itilde=297;e.itildebelow=7725;e.iubopomofo=12585;e.iucyrillic=1102;e.ivowelsignbengali=2495;e.ivowelsigndeva=2367;e.ivowelsigngujarati=2751;e.izhitsacyrillic=1141;e.izhitsadblgravecyrillic=1143;e.j=106;e.jaarmenian=1393;e.jabengali=2460;e.jadeva=2332;e.jagujarati=2716;e.jagurmukhi=2588;e.jbopomofo=12560;e.jcaron=496;e.jcircle=9433;e.jcircumflex=309;e.jcrossedtail=669;e.jdotlessstroke=607;e.jecyrillic=1112;e.jeemarabic=1580;e.jeemfinalarabic=65182;e.jeeminitialarabic=65183;e.jeemmedialarabic=65184;e.jeharabic=1688;e.jehfinalarabic=64395;e.jhabengali=2461;e.jhadeva=2333;e.jhagujarati=2717;e.jhagurmukhi=2589;e.jheharmenian=1403;e.jis=12292;e.jmonospace=65354;e.jparen=9381;e.jsuperior=690;e.k=107;e.kabashkircyrillic=1185;e.kabengali=2453;e.kacute=7729;e.kacyrillic=1082;e.kadescendercyrillic=1179;e.kadeva=2325;e.kaf=1499;e.kafarabic=1603;e.kafdagesh=64315;e.kafdageshhebrew=64315;e.kaffinalarabic=65242;e.kafhebrew=1499;e.kafinitialarabic=65243;e.kafmedialarabic=65244;e.kafrafehebrew=64333;e.kagujarati=2709;e.kagurmukhi=2581;e.kahiragana=12363;e.kahookcyrillic=1220;e.kakatakana=12459;e.kakatakanahalfwidth=65398;e.kappa=954;e.kappasymbolgreek=1008;e.kapyeounmieumkorean=12657;e.kapyeounphieuphkorean=12676;e.kapyeounpieupkorean=12664;e.kapyeounssangpieupkorean=12665;e.karoriisquare=13069;e.kashidaautoarabic=1600;e.kashidaautonosidebearingarabic=1600;e.kasmallkatakana=12533;e.kasquare=13188;e.kasraarabic=1616;e.kasratanarabic=1613;e.kastrokecyrillic=1183;e.katahiraprolongmarkhalfwidth=65392;e.kaverticalstrokecyrillic=1181;e.kbopomofo=12558;e.kcalsquare=13193;e.kcaron=489;e.kcedilla=311;e.kcircle=9434;e.kcommaaccent=311;e.kdotbelow=7731;e.keharmenian=1412;e.kehiragana=12369;e.kekatakana=12465;e.kekatakanahalfwidth=65401;e.kenarmenian=1391;e.kesmallkatakana=12534;e.kgreenlandic=312;e.khabengali=2454;e.khacyrillic=1093;e.khadeva=2326;e.khagujarati=2710;e.khagurmukhi=2582;e.khaharabic=1582;e.khahfinalarabic=65190;e.khahinitialarabic=65191;e.khahmedialarabic=65192;e.kheicoptic=999;e.khhadeva=2393;e.khhagurmukhi=2649;e.khieukhacirclekorean=12920;e.khieukhaparenkorean=12824;e.khieukhcirclekorean=12906;e.khieukhkorean=12619;e.khieukhparenkorean=12810;e.khokhaithai=3586;e.khokhonthai=3589;e.khokhuatthai=3587;e.khokhwaithai=3588;e.khomutthai=3675;e.khook=409;e.khorakhangthai=3590;e.khzsquare=13201;e.kihiragana=12365;e.kikatakana=12461;e.kikatakanahalfwidth=65399;e.kiroguramusquare=13077;e.kiromeetorusquare=13078;e.kirosquare=13076;e.kiyeokacirclekorean=12910;e.kiyeokaparenkorean=12814;e.kiyeokcirclekorean=12896;e.kiyeokkorean=12593;e.kiyeokparenkorean=12800;e.kiyeoksioskorean=12595;e.kjecyrillic=1116;e.klinebelow=7733;e.klsquare=13208;e.kmcubedsquare=13222;e.kmonospace=65355;e.kmsquaredsquare=13218;e.kohiragana=12371;e.kohmsquare=13248;e.kokaithai=3585;e.kokatakana=12467;e.kokatakanahalfwidth=65402;e.kooposquare=13086;e.koppacyrillic=1153;e.koreanstandardsymbol=12927;e.koroniscmb=835;e.kparen=9382;e.kpasquare=13226;e.ksicyrillic=1135;e.ktsquare=13263;e.kturned=670;e.kuhiragana=12367;e.kukatakana=12463;e.kukatakanahalfwidth=65400;e.kvsquare=13240;e.kwsquare=13246;e.l=108;e.labengali=2482;e.lacute=314;e.ladeva=2354;e.lagujarati=2738;e.lagurmukhi=2610;e.lakkhangyaothai=3653;e.lamaleffinalarabic=65276;e.lamalefhamzaabovefinalarabic=65272;e.lamalefhamzaaboveisolatedarabic=65271;e.lamalefhamzabelowfinalarabic=65274;e.lamalefhamzabelowisolatedarabic=65273;e.lamalefisolatedarabic=65275;e.lamalefmaddaabovefinalarabic=65270;e.lamalefmaddaaboveisolatedarabic=65269;e.lamarabic=1604;e.lambda=955;e.lambdastroke=411;e.lamed=1500;e.lameddagesh=64316;e.lameddageshhebrew=64316;e.lamedhebrew=1500;e.lamfinalarabic=65246;e.lamhahinitialarabic=64714;e.laminitialarabic=65247;e.lamjeeminitialarabic=64713;e.lamkhahinitialarabic=64715;e.lamlamhehisolatedarabic=65010;e.lammedialarabic=65248;e.lammeemhahinitialarabic=64904;e.lammeeminitialarabic=64716;e.largecircle=9711;e.lbar=410;e.lbelt=620;e.lbopomofo=12556;e.lcaron=318;e.lcedilla=316;e.lcircle=9435;e.lcircumflexbelow=7741;e.lcommaaccent=316;e.ldot=320;e.ldotaccent=320;e.ldotbelow=7735;e.ldotbelowmacron=7737;e.leftangleabovecmb=794;e.lefttackbelowcmb=792;e.less=60;e.lessequal=8804;e.lessequalorgreater=8922;e.lessmonospace=65308;e.lessorequivalent=8818;e.lessorgreater=8822;e.lessoverequal=8806;e.lesssmall=65124;e.lezh=622;e.lfblock=9612;e.lhookretroflex=621;e.lira=8356;e.liwnarmenian=1388;e.lj=457;e.ljecyrillic=1113;e.ll=63168;e.lladeva=2355;e.llagujarati=2739;e.llinebelow=7739;e.llladeva=2356;e.llvocalicbengali=2529;e.llvocalicdeva=2401;e.llvocalicvowelsignbengali=2531;e.llvocalicvowelsigndeva=2403;e.lmiddletilde=619;e.lmonospace=65356;e.lmsquare=13264;e.lochulathai=3628;e.logicaland=8743;e.logicalnot=172;e.logicalnotreversed=8976;e.logicalor=8744;e.lolingthai=3621;e.longs=383;e.lowlinecenterline=65102;e.lowlinecmb=818;e.lowlinedashed=65101;e.lozenge=9674;e.lparen=9383;e.lslash=322;e.lsquare=8467;e.lsuperior=63214;e.ltshade=9617;e.luthai=3622;e.lvocalicbengali=2444;e.lvocalicdeva=2316;e.lvocalicvowelsignbengali=2530;e.lvocalicvowelsigndeva=2402;e.lxsquare=13267;e.m=109;e.mabengali=2478;e.macron=175;e.macronbelowcmb=817;e.macroncmb=772;e.macronlowmod=717;e.macronmonospace=65507;e.macute=7743;e.madeva=2350;e.magujarati=2734;e.magurmukhi=2606;e.mahapakhhebrew=1444;e.mahapakhlefthebrew=1444;e.mahiragana=12414;e.maichattawalowleftthai=63637;e.maichattawalowrightthai=63636;e.maichattawathai=3659;e.maichattawaupperleftthai=63635;e.maieklowleftthai=63628;e.maieklowrightthai=63627;e.maiekthai=3656;e.maiekupperleftthai=63626;e.maihanakatleftthai=63620;e.maihanakatthai=3633;e.maitaikhuleftthai=63625;e.maitaikhuthai=3655;e.maitholowleftthai=63631;e.maitholowrightthai=63630;e.maithothai=3657;e.maithoupperleftthai=63629;e.maitrilowleftthai=63634;e.maitrilowrightthai=63633;e.maitrithai=3658;e.maitriupperleftthai=63632;e.maiyamokthai=3654;e.makatakana=12510;e.makatakanahalfwidth=65423;e.male=9794;e.mansyonsquare=13127;e.maqafhebrew=1470;e.mars=9794;e.masoracirclehebrew=1455;e.masquare=13187;e.mbopomofo=12551;e.mbsquare=13268;e.mcircle=9436;e.mcubedsquare=13221;e.mdotaccent=7745;e.mdotbelow=7747;e.meemarabic=1605;e.meemfinalarabic=65250;e.meeminitialarabic=65251;e.meemmedialarabic=65252;e.meemmeeminitialarabic=64721;e.meemmeemisolatedarabic=64584;e.meetorusquare=13133;e.mehiragana=12417;e.meizierasquare=13182;e.mekatakana=12513;e.mekatakanahalfwidth=65426;e.mem=1502;e.memdagesh=64318;e.memdageshhebrew=64318;e.memhebrew=1502;e.menarmenian=1396;e.merkhahebrew=1445;e.merkhakefulahebrew=1446;e.merkhakefulalefthebrew=1446;e.merkhalefthebrew=1445;e.mhook=625;e.mhzsquare=13202;e.middledotkatakanahalfwidth=65381;e.middot=183;e.mieumacirclekorean=12914;e.mieumaparenkorean=12818;e.mieumcirclekorean=12900;e.mieumkorean=12609;e.mieumpansioskorean=12656;e.mieumparenkorean=12804;e.mieumpieupkorean=12654;e.mieumsioskorean=12655;e.mihiragana=12415;e.mikatakana=12511;e.mikatakanahalfwidth=65424;e.minus=8722;e.minusbelowcmb=800;e.minuscircle=8854;e.minusmod=727;e.minusplus=8723;e.minute=8242;e.miribaarusquare=13130;e.mirisquare=13129;e.mlonglegturned=624;e.mlsquare=13206;e.mmcubedsquare=13219;e.mmonospace=65357;e.mmsquaredsquare=13215;e.mohiragana=12418;e.mohmsquare=13249;e.mokatakana=12514;e.mokatakanahalfwidth=65427;e.molsquare=13270;e.momathai=3617;e.moverssquare=13223;e.moverssquaredsquare=13224;e.mparen=9384;e.mpasquare=13227;e.mssquare=13235;e.msuperior=63215;e.mturned=623;e.mu=181;e.mu1=181;e.muasquare=13186;e.muchgreater=8811;e.muchless=8810;e.mufsquare=13196;e.mugreek=956;e.mugsquare=13197;e.muhiragana=12416;e.mukatakana=12512;e.mukatakanahalfwidth=65425;e.mulsquare=13205;e.multiply=215;e.mumsquare=13211;e.munahhebrew=1443;e.munahlefthebrew=1443;e.musicalnote=9834;e.musicalnotedbl=9835;e.musicflatsign=9837;e.musicsharpsign=9839;e.mussquare=13234;e.muvsquare=13238;e.muwsquare=13244;e.mvmegasquare=13241;e.mvsquare=13239;e.mwmegasquare=13247;e.mwsquare=13245;e.n=110;e.nabengali=2472;e.nabla=8711;e.nacute=324;e.nadeva=2344;e.nagujarati=2728;e.nagurmukhi=2600;e.nahiragana=12394;e.nakatakana=12490;e.nakatakanahalfwidth=65413;e.napostrophe=329;e.nasquare=13185;e.nbopomofo=12555;e.nbspace=160;e.ncaron=328;e.ncedilla=326;e.ncircle=9437;e.ncircumflexbelow=7755;e.ncommaaccent=326;e.ndotaccent=7749;e.ndotbelow=7751;e.nehiragana=12397;e.nekatakana=12493;e.nekatakanahalfwidth=65416;e.newsheqelsign=8362;e.nfsquare=13195;e.ngabengali=2457;e.ngadeva=2329;e.ngagujarati=2713;e.ngagurmukhi=2585;e.ngonguthai=3591;e.nhiragana=12435;e.nhookleft=626;e.nhookretroflex=627;e.nieunacirclekorean=12911;e.nieunaparenkorean=12815;e.nieuncieuckorean=12597;e.nieuncirclekorean=12897;e.nieunhieuhkorean=12598;e.nieunkorean=12596;e.nieunpansioskorean=12648;e.nieunparenkorean=12801;e.nieunsioskorean=12647;e.nieuntikeutkorean=12646;e.nihiragana=12395;e.nikatakana=12491;e.nikatakanahalfwidth=65414;e.nikhahitleftthai=63641;e.nikhahitthai=3661;e.nine=57;e.ninearabic=1641;e.ninebengali=2543;e.ninecircle=9320;e.ninecircleinversesansserif=10130;e.ninedeva=2415;e.ninegujarati=2799;e.ninegurmukhi=2671;e.ninehackarabic=1641;e.ninehangzhou=12329;e.nineideographicparen=12840;e.nineinferior=8329;e.ninemonospace=65305;e.nineoldstyle=63289;e.nineparen=9340;e.nineperiod=9360;e.ninepersian=1785;e.nineroman=8568;e.ninesuperior=8313;e.nineteencircle=9330;e.nineteenparen=9350;e.nineteenperiod=9370;e.ninethai=3673;e.nj=460;e.njecyrillic=1114;e.nkatakana=12531;e.nkatakanahalfwidth=65437;e.nlegrightlong=414;e.nlinebelow=7753;e.nmonospace=65358;e.nmsquare=13210;e.nnabengali=2467;e.nnadeva=2339;e.nnagujarati=2723;e.nnagurmukhi=2595;e.nnnadeva=2345;e.nohiragana=12398;e.nokatakana=12494;e.nokatakanahalfwidth=65417;e.nonbreakingspace=160;e.nonenthai=3603;e.nonuthai=3609;e.noonarabic=1606;e.noonfinalarabic=65254;e.noonghunnaarabic=1722;e.noonghunnafinalarabic=64415;e.nooninitialarabic=65255;e.noonjeeminitialarabic=64722;e.noonjeemisolatedarabic=64587;e.noonmedialarabic=65256;e.noonmeeminitialarabic=64725;e.noonmeemisolatedarabic=64590;e.noonnoonfinalarabic=64653;e.notcontains=8716;e.notelement=8713;e.notelementof=8713;e.notequal=8800;e.notgreater=8815;e.notgreaternorequal=8817;e.notgreaternorless=8825;e.notidentical=8802;e.notless=8814;e.notlessnorequal=8816;e.notparallel=8742;e.notprecedes=8832;e.notsubset=8836;e.notsucceeds=8833;e.notsuperset=8837;e.nowarmenian=1398;e.nparen=9385;e.nssquare=13233;e.nsuperior=8319;e.ntilde=241;e.nu=957;e.nuhiragana=12396;e.nukatakana=12492;e.nukatakanahalfwidth=65415;e.nuktabengali=2492;e.nuktadeva=2364;e.nuktagujarati=2748;e.nuktagurmukhi=2620;e.numbersign=35;e.numbersignmonospace=65283;e.numbersignsmall=65119;e.numeralsigngreek=884;e.numeralsignlowergreek=885;e.numero=8470;e.nun=1504;e.nundagesh=64320;e.nundageshhebrew=64320;e.nunhebrew=1504;e.nvsquare=13237;e.nwsquare=13243;e.nyabengali=2462;e.nyadeva=2334;e.nyagujarati=2718;e.nyagurmukhi=2590;e.o=111;e.oacute=243;e.oangthai=3629;e.obarred=629;e.obarredcyrillic=1257;e.obarreddieresiscyrillic=1259;e.obengali=2451;e.obopomofo=12571;e.obreve=335;e.ocandradeva=2321;e.ocandragujarati=2705;e.ocandravowelsigndeva=2377;e.ocandravowelsigngujarati=2761;e.ocaron=466;e.ocircle=9438;e.ocircumflex=244;e.ocircumflexacute=7889;e.ocircumflexdotbelow=7897;e.ocircumflexgrave=7891;e.ocircumflexhookabove=7893;e.ocircumflextilde=7895;e.ocyrillic=1086;e.odblacute=337;e.odblgrave=525;e.odeva=2323;e.odieresis=246;e.odieresiscyrillic=1255;e.odotbelow=7885;e.oe=339;e.oekorean=12634;e.ogonek=731;e.ogonekcmb=808;e.ograve=242;e.ogujarati=2707;e.oharmenian=1413;e.ohiragana=12362;e.ohookabove=7887;e.ohorn=417;e.ohornacute=7899;e.ohorndotbelow=7907;e.ohorngrave=7901;e.ohornhookabove=7903;e.ohorntilde=7905;e.ohungarumlaut=337;e.oi=419;e.oinvertedbreve=527;e.okatakana=12458;e.okatakanahalfwidth=65397;e.okorean=12631;e.olehebrew=1451;e.omacron=333;e.omacronacute=7763;e.omacrongrave=7761;e.omdeva=2384;e.omega=969;e.omega1=982;e.omegacyrillic=1121;e.omegalatinclosed=631;e.omegaroundcyrillic=1147;e.omegatitlocyrillic=1149;e.omegatonos=974;e.omgujarati=2768;e.omicron=959;e.omicrontonos=972;e.omonospace=65359;e.one=49;e.onearabic=1633;e.onebengali=2535;e.onecircle=9312;e.onecircleinversesansserif=10122;e.onedeva=2407;e.onedotenleader=8228;e.oneeighth=8539;e.onefitted=63196;e.onegujarati=2791;e.onegurmukhi=2663;e.onehackarabic=1633;e.onehalf=189;e.onehangzhou=12321;e.oneideographicparen=12832;e.oneinferior=8321;e.onemonospace=65297;e.onenumeratorbengali=2548;e.oneoldstyle=63281;e.oneparen=9332;e.oneperiod=9352;e.onepersian=1777;e.onequarter=188;e.oneroman=8560;e.onesuperior=185;e.onethai=3665;e.onethird=8531;e.oogonek=491;e.oogonekmacron=493;e.oogurmukhi=2579;e.oomatragurmukhi=2635;e.oopen=596;e.oparen=9386;e.openbullet=9702;e.option=8997;e.ordfeminine=170;e.ordmasculine=186;e.orthogonal=8735;e.oshortdeva=2322;e.oshortvowelsigndeva=2378;e.oslash=248;e.oslashacute=511;e.osmallhiragana=12361;e.osmallkatakana=12457;e.osmallkatakanahalfwidth=65387;e.ostrokeacute=511;e.osuperior=63216;e.otcyrillic=1151;e.otilde=245;e.otildeacute=7757;e.otildedieresis=7759;e.oubopomofo=12577;e.overline=8254;e.overlinecenterline=65098;e.overlinecmb=773;e.overlinedashed=65097;e.overlinedblwavy=65100;e.overlinewavy=65099;e.overscore=175;e.ovowelsignbengali=2507;e.ovowelsigndeva=2379;e.ovowelsigngujarati=2763;e.p=112;e.paampssquare=13184;e.paasentosquare=13099;e.pabengali=2474;e.pacute=7765;e.padeva=2346;e.pagedown=8671;e.pageup=8670;e.pagujarati=2730;e.pagurmukhi=2602;e.pahiragana=12401;e.paiyannoithai=3631;e.pakatakana=12497;e.palatalizationcyrilliccmb=1156;e.palochkacyrillic=1216;e.pansioskorean=12671;e.paragraph=182;e.parallel=8741;e.parenleft=40;e.parenleftaltonearabic=64830;e.parenleftbt=63725;e.parenleftex=63724;e.parenleftinferior=8333;e.parenleftmonospace=65288;e.parenleftsmall=65113;e.parenleftsuperior=8317;e.parenlefttp=63723;e.parenleftvertical=65077;e.parenright=41;e.parenrightaltonearabic=64831;e.parenrightbt=63736;e.parenrightex=63735;e.parenrightinferior=8334;e.parenrightmonospace=65289;e.parenrightsmall=65114;e.parenrightsuperior=8318;e.parenrighttp=63734;e.parenrightvertical=65078;e.partialdiff=8706;e.paseqhebrew=1472;e.pashtahebrew=1433;e.pasquare=13225;e.patah=1463;e.patah11=1463;e.patah1d=1463;e.patah2a=1463;e.patahhebrew=1463;e.patahnarrowhebrew=1463;e.patahquarterhebrew=1463;e.patahwidehebrew=1463;e.pazerhebrew=1441;e.pbopomofo=12550;e.pcircle=9439;e.pdotaccent=7767;e.pe=1508;e.pecyrillic=1087;e.pedagesh=64324;e.pedageshhebrew=64324;e.peezisquare=13115;e.pefinaldageshhebrew=64323;e.peharabic=1662;e.peharmenian=1402;e.pehebrew=1508;e.pehfinalarabic=64343;e.pehinitialarabic=64344;e.pehiragana=12410;e.pehmedialarabic=64345;e.pekatakana=12506;e.pemiddlehookcyrillic=1191;e.perafehebrew=64334;e.percent=37;e.percentarabic=1642;e.percentmonospace=65285;e.percentsmall=65130;e.period=46;e.periodarmenian=1417;e.periodcentered=183;e.periodhalfwidth=65377;e.periodinferior=63207;e.periodmonospace=65294;e.periodsmall=65106;e.periodsuperior=63208;e.perispomenigreekcmb=834;e.perpendicular=8869;e.perthousand=8240;e.peseta=8359;e.pfsquare=13194;e.phabengali=2475;e.phadeva=2347;e.phagujarati=2731;e.phagurmukhi=2603;e.phi=966;e.phi1=981;e.phieuphacirclekorean=12922;e.phieuphaparenkorean=12826;e.phieuphcirclekorean=12908;e.phieuphkorean=12621;e.phieuphparenkorean=12812;e.philatin=632;e.phinthuthai=3642;e.phisymbolgreek=981;e.phook=421;e.phophanthai=3614;e.phophungthai=3612;e.phosamphaothai=3616;e.pi=960;e.pieupacirclekorean=12915;e.pieupaparenkorean=12819;e.pieupcieuckorean=12662;e.pieupcirclekorean=12901;e.pieupkiyeokkorean=12658;e.pieupkorean=12610;e.pieupparenkorean=12805;e.pieupsioskiyeokkorean=12660;e.pieupsioskorean=12612;e.pieupsiostikeutkorean=12661;e.pieupthieuthkorean=12663;e.pieuptikeutkorean=12659;e.pihiragana=12404;e.pikatakana=12500;e.pisymbolgreek=982;e.piwrarmenian=1411;e.planckover2pi=8463;e.planckover2pi1=8463;e.plus=43;e.plusbelowcmb=799;e.pluscircle=8853;e.plusminus=177;e.plusmod=726;e.plusmonospace=65291;e.plussmall=65122;e.plussuperior=8314;e.pmonospace=65360;e.pmsquare=13272;e.pohiragana=12413;e.pointingindexdownwhite=9759;e.pointingindexleftwhite=9756;e.pointingindexrightwhite=9758;e.pointingindexupwhite=9757;e.pokatakana=12509;e.poplathai=3611;e.postalmark=12306;e.postalmarkface=12320;e.pparen=9387;e.precedes=8826;e.prescription=8478;e.primemod=697;e.primereversed=8245;e.product=8719;e.projective=8965;e.prolongedkana=12540;e.propellor=8984;e.propersubset=8834;e.propersuperset=8835;e.proportion=8759;e.proportional=8733;e.psi=968;e.psicyrillic=1137;e.psilipneumatacyrilliccmb=1158;e.pssquare=13232;e.puhiragana=12407;e.pukatakana=12503;e.pvsquare=13236;e.pwsquare=13242;e.q=113;e.qadeva=2392;e.qadmahebrew=1448;e.qafarabic=1602;e.qaffinalarabic=65238;e.qafinitialarabic=65239;e.qafmedialarabic=65240;e.qamats=1464;e.qamats10=1464;e.qamats1a=1464;e.qamats1c=1464;e.qamats27=1464;e.qamats29=1464;e.qamats33=1464;e.qamatsde=1464;e.qamatshebrew=1464;e.qamatsnarrowhebrew=1464;e.qamatsqatanhebrew=1464;e.qamatsqatannarrowhebrew=1464;e.qamatsqatanquarterhebrew=1464;e.qamatsqatanwidehebrew=1464;e.qamatsquarterhebrew=1464;e.qamatswidehebrew=1464;e.qarneyparahebrew=1439;e.qbopomofo=12561;e.qcircle=9440;e.qhook=672;e.qmonospace=65361;e.qof=1511;e.qofdagesh=64327;e.qofdageshhebrew=64327;e.qofhebrew=1511;e.qparen=9388;e.quarternote=9833;e.qubuts=1467;e.qubuts18=1467;e.qubuts25=1467;e.qubuts31=1467;e.qubutshebrew=1467;e.qubutsnarrowhebrew=1467;e.qubutsquarterhebrew=1467;e.qubutswidehebrew=1467;e.question=63;e.questionarabic=1567;e.questionarmenian=1374;e.questiondown=191;e.questiondownsmall=63423;e.questiongreek=894;e.questionmonospace=65311;e.questionsmall=63295;e.quotedbl=34;e.quotedblbase=8222;e.quotedblleft=8220;e.quotedblmonospace=65282;e.quotedblprime=12318;e.quotedblprimereversed=12317;e.quotedblright=8221;e.quoteleft=8216;e.quoteleftreversed=8219;e.quotereversed=8219;e.quoteright=8217;e.quoterightn=329;e.quotesinglbase=8218;e.quotesingle=39;e.quotesinglemonospace=65287;e.r=114;e.raarmenian=1404;e.rabengali=2480;e.racute=341;e.radeva=2352;e.radical=8730;e.radicalex=63717;e.radoverssquare=13230;e.radoverssquaredsquare=13231;e.radsquare=13229;e.rafe=1471;e.rafehebrew=1471;e.ragujarati=2736;e.ragurmukhi=2608;e.rahiragana=12425;e.rakatakana=12521;e.rakatakanahalfwidth=65431;e.ralowerdiagonalbengali=2545;e.ramiddlediagonalbengali=2544;e.ramshorn=612;e.ratio=8758;e.rbopomofo=12566;e.rcaron=345;e.rcedilla=343;e.rcircle=9441;e.rcommaaccent=343;e.rdblgrave=529;e.rdotaccent=7769;e.rdotbelow=7771;e.rdotbelowmacron=7773;e.referencemark=8251;e.reflexsubset=8838;e.reflexsuperset=8839;e.registered=174;e.registersans=63720;e.registerserif=63194;e.reharabic=1585;e.reharmenian=1408;e.rehfinalarabic=65198;e.rehiragana=12428;e.rekatakana=12524;e.rekatakanahalfwidth=65434;e.resh=1512;e.reshdageshhebrew=64328;e.reshhebrew=1512;e.reversedtilde=8765;e.reviahebrew=1431;e.reviamugrashhebrew=1431;e.revlogicalnot=8976;e.rfishhook=638;e.rfishhookreversed=639;e.rhabengali=2525;e.rhadeva=2397;e.rho=961;e.rhook=637;e.rhookturned=635;e.rhookturnedsuperior=693;e.rhosymbolgreek=1009;e.rhotichookmod=734;e.rieulacirclekorean=12913;e.rieulaparenkorean=12817;e.rieulcirclekorean=12899;e.rieulhieuhkorean=12608;e.rieulkiyeokkorean=12602;e.rieulkiyeoksioskorean=12649;e.rieulkorean=12601;e.rieulmieumkorean=12603;e.rieulpansioskorean=12652;e.rieulparenkorean=12803;e.rieulphieuphkorean=12607;e.rieulpieupkorean=12604;e.rieulpieupsioskorean=12651;e.rieulsioskorean=12605;e.rieulthieuthkorean=12606;e.rieultikeutkorean=12650;e.rieulyeorinhieuhkorean=12653;e.rightangle=8735;e.righttackbelowcmb=793;e.righttriangle=8895;e.rihiragana=12426;e.rikatakana=12522;e.rikatakanahalfwidth=65432;e.ring=730;e.ringbelowcmb=805;e.ringcmb=778;e.ringhalfleft=703;e.ringhalfleftarmenian=1369;e.ringhalfleftbelowcmb=796;e.ringhalfleftcentered=723;e.ringhalfright=702;e.ringhalfrightbelowcmb=825;e.ringhalfrightcentered=722;e.rinvertedbreve=531;e.rittorusquare=13137;e.rlinebelow=7775;e.rlongleg=636;e.rlonglegturned=634;e.rmonospace=65362;e.rohiragana=12429;e.rokatakana=12525;e.rokatakanahalfwidth=65435;e.roruathai=3619;e.rparen=9389;e.rrabengali=2524;e.rradeva=2353;e.rragurmukhi=2652;e.rreharabic=1681;e.rrehfinalarabic=64397;e.rrvocalicbengali=2528;e.rrvocalicdeva=2400;e.rrvocalicgujarati=2784;e.rrvocalicvowelsignbengali=2500;e.rrvocalicvowelsigndeva=2372;e.rrvocalicvowelsigngujarati=2756;e.rsuperior=63217;e.rtblock=9616;e.rturned=633;e.rturnedsuperior=692;e.ruhiragana=12427;e.rukatakana=12523;e.rukatakanahalfwidth=65433;e.rupeemarkbengali=2546;e.rupeesignbengali=2547;e.rupiah=63197;e.ruthai=3620;e.rvocalicbengali=2443;e.rvocalicdeva=2315;e.rvocalicgujarati=2699;e.rvocalicvowelsignbengali=2499;e.rvocalicvowelsigndeva=2371;e.rvocalicvowelsigngujarati=2755;e.s=115;e.sabengali=2488;e.sacute=347;e.sacutedotaccent=7781;e.sadarabic=1589;e.sadeva=2360;e.sadfinalarabic=65210;e.sadinitialarabic=65211;e.sadmedialarabic=65212;e.sagujarati=2744;e.sagurmukhi=2616;e.sahiragana=12373;e.sakatakana=12469;e.sakatakanahalfwidth=65403;e.sallallahoualayhewasallamarabic=65018;e.samekh=1505;e.samekhdagesh=64321;e.samekhdageshhebrew=64321;e.samekhhebrew=1505;e.saraaathai=3634;e.saraaethai=3649;e.saraaimaimalaithai=3652;e.saraaimaimuanthai=3651;e.saraamthai=3635;e.saraathai=3632;e.saraethai=3648;e.saraiileftthai=63622;e.saraiithai=3637;e.saraileftthai=63621;e.saraithai=3636;e.saraothai=3650;e.saraueeleftthai=63624;e.saraueethai=3639;e.saraueleftthai=63623;e.sarauethai=3638;e.sarauthai=3640;e.sarauuthai=3641;e.sbopomofo=12569;e.scaron=353;e.scarondotaccent=7783;e.scedilla=351;e.schwa=601;e.schwacyrillic=1241;e.schwadieresiscyrillic=1243;e.schwahook=602;e.scircle=9442;e.scircumflex=349;e.scommaaccent=537;e.sdotaccent=7777;e.sdotbelow=7779;e.sdotbelowdotaccent=7785;e.seagullbelowcmb=828;e.second=8243;e.secondtonechinese=714;e.section=167;e.seenarabic=1587;e.seenfinalarabic=65202;e.seeninitialarabic=65203;e.seenmedialarabic=65204;e.segol=1462;e.segol13=1462;e.segol1f=1462;e.segol2c=1462;e.segolhebrew=1462;e.segolnarrowhebrew=1462;e.segolquarterhebrew=1462;e.segoltahebrew=1426;e.segolwidehebrew=1462;e.seharmenian=1405;e.sehiragana=12379;e.sekatakana=12475;e.sekatakanahalfwidth=65406;e.semicolon=59;e.semicolonarabic=1563;e.semicolonmonospace=65307;e.semicolonsmall=65108;e.semivoicedmarkkana=12444;e.semivoicedmarkkanahalfwidth=65439;e.sentisquare=13090;e.sentosquare=13091;e.seven=55;e.sevenarabic=1639;e.sevenbengali=2541;e.sevencircle=9318;e.sevencircleinversesansserif=10128;e.sevendeva=2413;e.seveneighths=8542;e.sevengujarati=2797;e.sevengurmukhi=2669;e.sevenhackarabic=1639;e.sevenhangzhou=12327;e.sevenideographicparen=12838;e.seveninferior=8327;e.sevenmonospace=65303;e.sevenoldstyle=63287;e.sevenparen=9338;e.sevenperiod=9358;e.sevenpersian=1783;e.sevenroman=8566;e.sevensuperior=8311;e.seventeencircle=9328;e.seventeenparen=9348;e.seventeenperiod=9368;e.seventhai=3671;e.sfthyphen=173;e.shaarmenian=1399;e.shabengali=2486;e.shacyrillic=1096;e.shaddaarabic=1617;e.shaddadammaarabic=64609;e.shaddadammatanarabic=64606;e.shaddafathaarabic=64608;e.shaddakasraarabic=64610;e.shaddakasratanarabic=64607;e.shade=9618;e.shadedark=9619;e.shadelight=9617;e.shademedium=9618;e.shadeva=2358;e.shagujarati=2742;e.shagurmukhi=2614;e.shalshelethebrew=1427;e.shbopomofo=12565;e.shchacyrillic=1097;e.sheenarabic=1588;e.sheenfinalarabic=65206;e.sheeninitialarabic=65207;e.sheenmedialarabic=65208;e.sheicoptic=995;e.sheqel=8362;e.sheqelhebrew=8362;e.sheva=1456;e.sheva115=1456;e.sheva15=1456;e.sheva22=1456;e.sheva2e=1456;e.shevahebrew=1456;e.shevanarrowhebrew=1456;e.shevaquarterhebrew=1456;e.shevawidehebrew=1456;e.shhacyrillic=1211;e.shimacoptic=1005;e.shin=1513;e.shindagesh=64329;e.shindageshhebrew=64329;e.shindageshshindot=64300;e.shindageshshindothebrew=64300;e.shindageshsindot=64301;e.shindageshsindothebrew=64301;e.shindothebrew=1473;e.shinhebrew=1513;e.shinshindot=64298;e.shinshindothebrew=64298;e.shinsindot=64299;e.shinsindothebrew=64299;e.shook=642;e.sigma=963;e.sigma1=962;e.sigmafinal=962;e.sigmalunatesymbolgreek=1010;e.sihiragana=12375;e.sikatakana=12471;e.sikatakanahalfwidth=65404;e.siluqhebrew=1469;e.siluqlefthebrew=1469;e.similar=8764;e.sindothebrew=1474;e.siosacirclekorean=12916;e.siosaparenkorean=12820;e.sioscieuckorean=12670;e.sioscirclekorean=12902;e.sioskiyeokkorean=12666;e.sioskorean=12613;e.siosnieunkorean=12667;e.siosparenkorean=12806;e.siospieupkorean=12669;e.siostikeutkorean=12668;e.six=54;e.sixarabic=1638;e.sixbengali=2540;e.sixcircle=9317;e.sixcircleinversesansserif=10127;e.sixdeva=2412;e.sixgujarati=2796;e.sixgurmukhi=2668;e.sixhackarabic=1638;e.sixhangzhou=12326;e.sixideographicparen=12837;e.sixinferior=8326;e.sixmonospace=65302;e.sixoldstyle=63286;e.sixparen=9337;e.sixperiod=9357;e.sixpersian=1782;e.sixroman=8565;e.sixsuperior=8310;e.sixteencircle=9327;e.sixteencurrencydenominatorbengali=2553;e.sixteenparen=9347;e.sixteenperiod=9367;e.sixthai=3670;e.slash=47;e.slashmonospace=65295;e.slong=383;e.slongdotaccent=7835;e.smileface=9786;e.smonospace=65363;e.sofpasuqhebrew=1475;e.softhyphen=173;e.softsigncyrillic=1100;e.sohiragana=12381;e.sokatakana=12477;e.sokatakanahalfwidth=65407;e.soliduslongoverlaycmb=824;e.solidusshortoverlaycmb=823;e.sorusithai=3625;e.sosalathai=3624;e.sosothai=3595;e.sosuathai=3626;e.space=32;e.spacehackarabic=32;e.spade=9824;e.spadesuitblack=9824;e.spadesuitwhite=9828;e.sparen=9390;e.squarebelowcmb=827;e.squarecc=13252;e.squarecm=13213;e.squarediagonalcrosshatchfill=9641;e.squarehorizontalfill=9636;e.squarekg=13199;e.squarekm=13214;e.squarekmcapital=13262;e.squareln=13265;e.squarelog=13266;e.squaremg=13198;e.squaremil=13269;e.squaremm=13212;e.squaremsquared=13217;e.squareorthogonalcrosshatchfill=9638;e.squareupperlefttolowerrightfill=9639;e.squareupperrighttolowerleftfill=9640;e.squareverticalfill=9637;e.squarewhitewithsmallblack=9635;e.srsquare=13275;e.ssabengali=2487;e.ssadeva=2359;e.ssagujarati=2743;e.ssangcieuckorean=12617;e.ssanghieuhkorean=12677;e.ssangieungkorean=12672;e.ssangkiyeokkorean=12594;e.ssangnieunkorean=12645;e.ssangpieupkorean=12611;e.ssangsioskorean=12614;e.ssangtikeutkorean=12600;e.ssuperior=63218;e.sterling=163;e.sterlingmonospace=65505;e.strokelongoverlaycmb=822;e.strokeshortoverlaycmb=821;e.subset=8834;e.subsetnotequal=8842;e.subsetorequal=8838;e.succeeds=8827;e.suchthat=8715;e.suhiragana=12377;e.sukatakana=12473;e.sukatakanahalfwidth=65405;e.sukunarabic=1618;e.summation=8721;e.sun=9788;e.superset=8835;e.supersetnotequal=8843;e.supersetorequal=8839;e.svsquare=13276;e.syouwaerasquare=13180;e.t=116;e.tabengali=2468;e.tackdown=8868;e.tackleft=8867;e.tadeva=2340;e.tagujarati=2724;e.tagurmukhi=2596;e.taharabic=1591;e.tahfinalarabic=65218;e.tahinitialarabic=65219;e.tahiragana=12383;e.tahmedialarabic=65220;e.taisyouerasquare=13181;e.takatakana=12479;e.takatakanahalfwidth=65408;e.tatweelarabic=1600;e.tau=964;e.tav=1514;e.tavdages=64330;e.tavdagesh=64330;e.tavdageshhebrew=64330;e.tavhebrew=1514;e.tbar=359;e.tbopomofo=12554;e.tcaron=357;e.tccurl=680;e.tcedilla=355;e.tcheharabic=1670;e.tchehfinalarabic=64379;e.tchehinitialarabic=64380;e.tchehmedialarabic=64381;e.tcircle=9443;e.tcircumflexbelow=7793;e.tcommaaccent=355;e.tdieresis=7831;e.tdotaccent=7787;e.tdotbelow=7789;e.tecyrillic=1090;e.tedescendercyrillic=1197;e.teharabic=1578;e.tehfinalarabic=65174;e.tehhahinitialarabic=64674;e.tehhahisolatedarabic=64524;e.tehinitialarabic=65175;e.tehiragana=12390;e.tehjeeminitialarabic=64673;e.tehjeemisolatedarabic=64523;e.tehmarbutaarabic=1577;e.tehmarbutafinalarabic=65172;e.tehmedialarabic=65176;e.tehmeeminitialarabic=64676;e.tehmeemisolatedarabic=64526;e.tehnoonfinalarabic=64627;e.tekatakana=12486;e.tekatakanahalfwidth=65411;e.telephone=8481;e.telephoneblack=9742;e.telishagedolahebrew=1440;e.telishaqetanahebrew=1449;e.tencircle=9321;e.tenideographicparen=12841;e.tenparen=9341;e.tenperiod=9361;e.tenroman=8569;e.tesh=679;e.tet=1496;e.tetdagesh=64312;e.tetdageshhebrew=64312;e.tethebrew=1496;e.tetsecyrillic=1205;e.tevirhebrew=1435;e.tevirlefthebrew=1435;e.thabengali=2469;e.thadeva=2341;e.thagujarati=2725;e.thagurmukhi=2597;e.thalarabic=1584;e.thalfinalarabic=65196;e.thanthakhatlowleftthai=63640;e.thanthakhatlowrightthai=63639;e.thanthakhatthai=3660;e.thanthakhatupperleftthai=63638;e.theharabic=1579;e.thehfinalarabic=65178;e.thehinitialarabic=65179;e.thehmedialarabic=65180;e.thereexists=8707;e.therefore=8756;e.theta=952;e.theta1=977;e.thetasymbolgreek=977;e.thieuthacirclekorean=12921;e.thieuthaparenkorean=12825;e.thieuthcirclekorean=12907;e.thieuthkorean=12620;e.thieuthparenkorean=12811;e.thirteencircle=9324;e.thirteenparen=9344;e.thirteenperiod=9364;e.thonangmonthothai=3601;e.thook=429;e.thophuthaothai=3602;e.thorn=254;e.thothahanthai=3607;e.thothanthai=3600;e.thothongthai=3608;e.thothungthai=3606;e.thousandcyrillic=1154;e.thousandsseparatorarabic=1644;e.thousandsseparatorpersian=1644;e.three=51;e.threearabic=1635;e.threebengali=2537;e.threecircle=9314;e.threecircleinversesansserif=10124;e.threedeva=2409;e.threeeighths=8540;e.threegujarati=2793;e.threegurmukhi=2665;e.threehackarabic=1635;e.threehangzhou=12323;e.threeideographicparen=12834;e.threeinferior=8323;e.threemonospace=65299;e.threenumeratorbengali=2550;e.threeoldstyle=63283;e.threeparen=9334;e.threeperiod=9354;e.threepersian=1779;e.threequarters=190;e.threequartersemdash=63198;e.threeroman=8562;e.threesuperior=179;e.threethai=3667;e.thzsquare=13204;e.tihiragana=12385;e.tikatakana=12481;e.tikatakanahalfwidth=65409;e.tikeutacirclekorean=12912;e.tikeutaparenkorean=12816;e.tikeutcirclekorean=12898;e.tikeutkorean=12599;e.tikeutparenkorean=12802;e.tilde=732;e.tildebelowcmb=816;e.tildecmb=771;e.tildecomb=771;e.tildedoublecmb=864;e.tildeoperator=8764;e.tildeoverlaycmb=820;e.tildeverticalcmb=830;e.timescircle=8855;e.tipehahebrew=1430;e.tipehalefthebrew=1430;e.tippigurmukhi=2672;e.titlocyrilliccmb=1155;e.tiwnarmenian=1407;e.tlinebelow=7791;e.tmonospace=65364;e.toarmenian=1385;e.tohiragana=12392;e.tokatakana=12488;e.tokatakanahalfwidth=65412;e.tonebarextrahighmod=741;e.tonebarextralowmod=745;e.tonebarhighmod=742;e.tonebarlowmod=744;e.tonebarmidmod=743;e.tonefive=445;e.tonesix=389;e.tonetwo=424;e.tonos=900;e.tonsquare=13095;e.topatakthai=3599;e.tortoiseshellbracketleft=12308;e.tortoiseshellbracketleftsmall=65117;e.tortoiseshellbracketleftvertical=65081;e.tortoiseshellbracketright=12309;e.tortoiseshellbracketrightsmall=65118;e.tortoiseshellbracketrightvertical=65082;e.totaothai=3605;e.tpalatalhook=427;e.tparen=9391;e.trademark=8482;e.trademarksans=63722;e.trademarkserif=63195;e.tretroflexhook=648;e.triagdn=9660;e.triaglf=9668;e.triagrt=9658;e.triagup=9650;e.ts=678;e.tsadi=1510;e.tsadidagesh=64326;e.tsadidageshhebrew=64326;e.tsadihebrew=1510;e.tsecyrillic=1094;e.tsere=1461;e.tsere12=1461;e.tsere1e=1461;e.tsere2b=1461;e.tserehebrew=1461;e.tserenarrowhebrew=1461;e.tserequarterhebrew=1461;e.tserewidehebrew=1461;e.tshecyrillic=1115;e.tsuperior=63219;e.ttabengali=2463;e.ttadeva=2335;e.ttagujarati=2719;e.ttagurmukhi=2591;e.tteharabic=1657;e.ttehfinalarabic=64359;e.ttehinitialarabic=64360;e.ttehmedialarabic=64361;e.tthabengali=2464;e.tthadeva=2336;e.tthagujarati=2720;e.tthagurmukhi=2592;e.tturned=647;e.tuhiragana=12388;e.tukatakana=12484;e.tukatakanahalfwidth=65410;e.tusmallhiragana=12387;e.tusmallkatakana=12483;e.tusmallkatakanahalfwidth=65391;e.twelvecircle=9323;e.twelveparen=9343;e.twelveperiod=9363;e.twelveroman=8571;e.twentycircle=9331;e.twentyhangzhou=21316;e.twentyparen=9351;e.twentyperiod=9371;e.two=50;e.twoarabic=1634;e.twobengali=2536;e.twocircle=9313;e.twocircleinversesansserif=10123;e.twodeva=2408;e.twodotenleader=8229;e.twodotleader=8229;e.twodotleadervertical=65072;e.twogujarati=2792;e.twogurmukhi=2664;e.twohackarabic=1634;e.twohangzhou=12322;e.twoideographicparen=12833;e.twoinferior=8322;e.twomonospace=65298;e.twonumeratorbengali=2549;e.twooldstyle=63282;e.twoparen=9333;e.twoperiod=9353;e.twopersian=1778;e.tworoman=8561;e.twostroke=443;e.twosuperior=178;e.twothai=3666;e.twothirds=8532;e.u=117;e.uacute=250;e.ubar=649;e.ubengali=2441;e.ubopomofo=12584;e.ubreve=365;e.ucaron=468;e.ucircle=9444;e.ucircumflex=251;e.ucircumflexbelow=7799;e.ucyrillic=1091;e.udattadeva=2385;e.udblacute=369;e.udblgrave=533;e.udeva=2313;e.udieresis=252;e.udieresisacute=472;e.udieresisbelow=7795;e.udieresiscaron=474;e.udieresiscyrillic=1265;e.udieresisgrave=476;e.udieresismacron=470;e.udotbelow=7909;e.ugrave=249;e.ugujarati=2697;e.ugurmukhi=2569;e.uhiragana=12358;e.uhookabove=7911;e.uhorn=432;e.uhornacute=7913;e.uhorndotbelow=7921;e.uhorngrave=7915;e.uhornhookabove=7917;e.uhorntilde=7919;e.uhungarumlaut=369;e.uhungarumlautcyrillic=1267;e.uinvertedbreve=535;e.ukatakana=12454;e.ukatakanahalfwidth=65395;e.ukcyrillic=1145;e.ukorean=12636;e.umacron=363;e.umacroncyrillic=1263;e.umacrondieresis=7803;e.umatragurmukhi=2625;e.umonospace=65365;e.underscore=95;e.underscoredbl=8215;e.underscoremonospace=65343;e.underscorevertical=65075;e.underscorewavy=65103;e.union=8746;e.universal=8704;e.uogonek=371;e.uparen=9392;e.upblock=9600;e.upperdothebrew=1476;e.upsilon=965;e.upsilondieresis=971;e.upsilondieresistonos=944;e.upsilonlatin=650;e.upsilontonos=973;e.uptackbelowcmb=797;e.uptackmod=724;e.uragurmukhi=2675;e.uring=367;e.ushortcyrillic=1118;e.usmallhiragana=12357;e.usmallkatakana=12453;e.usmallkatakanahalfwidth=65385;e.ustraightcyrillic=1199;e.ustraightstrokecyrillic=1201;e.utilde=361;e.utildeacute=7801;e.utildebelow=7797;e.uubengali=2442;e.uudeva=2314;e.uugujarati=2698;e.uugurmukhi=2570;e.uumatragurmukhi=2626;e.uuvowelsignbengali=2498;e.uuvowelsigndeva=2370;e.uuvowelsigngujarati=2754;e.uvowelsignbengali=2497;e.uvowelsigndeva=2369;e.uvowelsigngujarati=2753;e.v=118;e.vadeva=2357;e.vagujarati=2741;e.vagurmukhi=2613;e.vakatakana=12535;e.vav=1493;e.vavdagesh=64309;e.vavdagesh65=64309;e.vavdageshhebrew=64309;e.vavhebrew=1493;e.vavholam=64331;e.vavholamhebrew=64331;e.vavvavhebrew=1520;e.vavyodhebrew=1521;e.vcircle=9445;e.vdotbelow=7807;e.vecyrillic=1074;e.veharabic=1700;e.vehfinalarabic=64363;e.vehinitialarabic=64364;e.vehmedialarabic=64365;e.vekatakana=12537;e.venus=9792;e.verticalbar=124;e.verticallineabovecmb=781;e.verticallinebelowcmb=809;e.verticallinelowmod=716;e.verticallinemod=712;e.vewarmenian=1406;e.vhook=651;e.vikatakana=12536;e.viramabengali=2509;e.viramadeva=2381;e.viramagujarati=2765;e.visargabengali=2435;e.visargadeva=2307;e.visargagujarati=2691;e.vmonospace=65366;e.voarmenian=1400;e.voicediterationhiragana=12446;e.voicediterationkatakana=12542;e.voicedmarkkana=12443;e.voicedmarkkanahalfwidth=65438;e.vokatakana=12538;e.vparen=9393;e.vtilde=7805;e.vturned=652;e.vuhiragana=12436;e.vukatakana=12532;e.w=119;e.wacute=7811;e.waekorean=12633;e.wahiragana=12431;e.wakatakana=12527;e.wakatakanahalfwidth=65436;e.wakorean=12632;e.wasmallhiragana=12430;e.wasmallkatakana=12526;e.wattosquare=13143;e.wavedash=12316;e.wavyunderscorevertical=65076;e.wawarabic=1608;e.wawfinalarabic=65262;e.wawhamzaabovearabic=1572;e.wawhamzaabovefinalarabic=65158;e.wbsquare=13277;e.wcircle=9446;e.wcircumflex=373;e.wdieresis=7813;e.wdotaccent=7815;e.wdotbelow=7817;e.wehiragana=12433;e.weierstrass=8472;e.wekatakana=12529;e.wekorean=12638;e.weokorean=12637;e.wgrave=7809;e.whitebullet=9702;e.whitecircle=9675;e.whitecircleinverse=9689;e.whitecornerbracketleft=12302;e.whitecornerbracketleftvertical=65091;e.whitecornerbracketright=12303;e.whitecornerbracketrightvertical=65092;e.whitediamond=9671;e.whitediamondcontainingblacksmalldiamond=9672;e.whitedownpointingsmalltriangle=9663;e.whitedownpointingtriangle=9661;e.whiteleftpointingsmalltriangle=9667;e.whiteleftpointingtriangle=9665;e.whitelenticularbracketleft=12310;e.whitelenticularbracketright=12311;e.whiterightpointingsmalltriangle=9657;e.whiterightpointingtriangle=9655;e.whitesmallsquare=9643;e.whitesmilingface=9786;e.whitesquare=9633;e.whitestar=9734;e.whitetelephone=9743;e.whitetortoiseshellbracketleft=12312;e.whitetortoiseshellbracketright=12313;e.whiteuppointingsmalltriangle=9653;e.whiteuppointingtriangle=9651;e.wihiragana=12432;e.wikatakana=12528;e.wikorean=12639;e.wmonospace=65367;e.wohiragana=12434;e.wokatakana=12530;e.wokatakanahalfwidth=65382;e.won=8361;e.wonmonospace=65510;e.wowaenthai=3623;e.wparen=9394;e.wring=7832;e.wsuperior=695;e.wturned=653;e.wynn=447;e.x=120;e.xabovecmb=829;e.xbopomofo=12562;e.xcircle=9447;e.xdieresis=7821;e.xdotaccent=7819;e.xeharmenian=1389;e.xi=958;e.xmonospace=65368;e.xparen=9395;e.xsuperior=739;e.y=121;e.yaadosquare=13134;e.yabengali=2479;e.yacute=253;e.yadeva=2351;e.yaekorean=12626;e.yagujarati=2735;e.yagurmukhi=2607;e.yahiragana=12420;e.yakatakana=12516;e.yakatakanahalfwidth=65428;e.yakorean=12625;e.yamakkanthai=3662;e.yasmallhiragana=12419;e.yasmallkatakana=12515;e.yasmallkatakanahalfwidth=65388;e.yatcyrillic=1123;e.ycircle=9448;e.ycircumflex=375;e.ydieresis=255;e.ydotaccent=7823;e.ydotbelow=7925;e.yeharabic=1610;e.yehbarreearabic=1746;e.yehbarreefinalarabic=64431;e.yehfinalarabic=65266;e.yehhamzaabovearabic=1574;e.yehhamzaabovefinalarabic=65162;e.yehhamzaaboveinitialarabic=65163;e.yehhamzaabovemedialarabic=65164;e.yehinitialarabic=65267;e.yehmedialarabic=65268;e.yehmeeminitialarabic=64733;e.yehmeemisolatedarabic=64600;e.yehnoonfinalarabic=64660;e.yehthreedotsbelowarabic=1745;e.yekorean=12630;e.yen=165;e.yenmonospace=65509;e.yeokorean=12629;e.yeorinhieuhkorean=12678;e.yerahbenyomohebrew=1450;e.yerahbenyomolefthebrew=1450;e.yericyrillic=1099;e.yerudieresiscyrillic=1273;e.yesieungkorean=12673;e.yesieungpansioskorean=12675;e.yesieungsioskorean=12674;e.yetivhebrew=1434;e.ygrave=7923;e.yhook=436;e.yhookabove=7927;e.yiarmenian=1397;e.yicyrillic=1111;e.yikorean=12642;e.yinyang=9775;e.yiwnarmenian=1410;e.ymonospace=65369;e.yod=1497;e.yoddagesh=64313;e.yoddageshhebrew=64313;e.yodhebrew=1497;e.yodyodhebrew=1522;e.yodyodpatahhebrew=64287;e.yohiragana=12424;e.yoikorean=12681;e.yokatakana=12520;e.yokatakanahalfwidth=65430;e.yokorean=12635;e.yosmallhiragana=12423;e.yosmallkatakana=12519;e.yosmallkatakanahalfwidth=65390;e.yotgreek=1011;e.yoyaekorean=12680;e.yoyakorean=12679;e.yoyakthai=3618;e.yoyingthai=3597;e.yparen=9396;e.ypogegrammeni=890;e.ypogegrammenigreekcmb=837;e.yr=422;e.yring=7833;e.ysuperior=696;e.ytilde=7929;e.yturned=654;e.yuhiragana=12422;e.yuikorean=12684;e.yukatakana=12518;e.yukatakanahalfwidth=65429;e.yukorean=12640;e.yusbigcyrillic=1131;e.yusbigiotifiedcyrillic=1133;e.yuslittlecyrillic=1127;e.yuslittleiotifiedcyrillic=1129;e.yusmallhiragana=12421;e.yusmallkatakana=12517;e.yusmallkatakanahalfwidth=65389;e.yuyekorean=12683;e.yuyeokorean=12682;e.yyabengali=2527;e.yyadeva=2399;e.z=122;e.zaarmenian=1382;e.zacute=378;e.zadeva=2395;e.zagurmukhi=2651;e.zaharabic=1592;e.zahfinalarabic=65222;e.zahinitialarabic=65223;e.zahiragana=12374;e.zahmedialarabic=65224;e.zainarabic=1586;e.zainfinalarabic=65200;e.zakatakana=12470;e.zaqefgadolhebrew=1429;e.zaqefqatanhebrew=1428;e.zarqahebrew=1432;e.zayin=1494;e.zayindagesh=64310;e.zayindageshhebrew=64310;e.zayinhebrew=1494;e.zbopomofo=12567;e.zcaron=382;e.zcircle=9449;e.zcircumflex=7825;e.zcurl=657;e.zdot=380;e.zdotaccent=380;e.zdotbelow=7827;e.zecyrillic=1079;e.zedescendercyrillic=1177;e.zedieresiscyrillic=1247;e.zehiragana=12380;e.zekatakana=12476;e.zero=48;e.zeroarabic=1632;e.zerobengali=2534;e.zerodeva=2406;e.zerogujarati=2790;e.zerogurmukhi=2662;e.zerohackarabic=1632;e.zeroinferior=8320;e.zeromonospace=65296;e.zerooldstyle=63280;e.zeropersian=1776;e.zerosuperior=8304;e.zerothai=3664;e.zerowidthjoiner=65279;e.zerowidthnonjoiner=8204;e.zerowidthspace=8203;e.zeta=950;e.zhbopomofo=12563;e.zhearmenian=1386;e.zhebrevecyrillic=1218;e.zhecyrillic=1078;e.zhedescendercyrillic=1175;e.zhedieresiscyrillic=1245;e.zihiragana=12376;e.zikatakana=12472;e.zinorhebrew=1454;e.zlinebelow=7829;e.zmonospace=65370;e.zohiragana=12382;e.zokatakana=12478;e.zparen=9397;e.zretroflexhook=656;e.zstroke=438;e.zuhiragana=12378;e.zukatakana=12474;e[".notdef"]=0;e.angbracketleftbig=9001;e.angbracketleftBig=9001;e.angbracketleftbigg=9001;e.angbracketleftBigg=9001;e.angbracketrightBig=9002;e.angbracketrightbig=9002;e.angbracketrightBigg=9002;e.angbracketrightbigg=9002;e.arrowhookleft=8618;e.arrowhookright=8617;e.arrowlefttophalf=8636;e.arrowleftbothalf=8637;e.arrownortheast=8599;e.arrownorthwest=8598;e.arrowrighttophalf=8640;e.arrowrightbothalf=8641;e.arrowsoutheast=8600;e.arrowsouthwest=8601;e.backslashbig=8726;e.backslashBig=8726;e.backslashBigg=8726;e.backslashbigg=8726;e.bardbl=8214;e.bracehtipdownleft=65079;e.bracehtipdownright=65079;e.bracehtipupleft=65080;e.bracehtipupright=65080;e.braceleftBig=123;e.braceleftbig=123;e.braceleftbigg=123;e.braceleftBigg=123;e.bracerightBig=125;e.bracerightbig=125;e.bracerightbigg=125;e.bracerightBigg=125;e.bracketleftbig=91;e.bracketleftBig=91;e.bracketleftbigg=91;e.bracketleftBigg=91;e.bracketrightBig=93;e.bracketrightbig=93;e.bracketrightbigg=93;e.bracketrightBigg=93;e.ceilingleftbig=8968;e.ceilingleftBig=8968;e.ceilingleftBigg=8968;e.ceilingleftbigg=8968;e.ceilingrightbig=8969;e.ceilingrightBig=8969;e.ceilingrightbigg=8969;e.ceilingrightBigg=8969;e.circledotdisplay=8857;e.circledottext=8857;e.circlemultiplydisplay=8855;e.circlemultiplytext=8855;e.circleplusdisplay=8853;e.circleplustext=8853;e.contintegraldisplay=8750;e.contintegraltext=8750;e.coproductdisplay=8720;e.coproducttext=8720;e.floorleftBig=8970;e.floorleftbig=8970;e.floorleftbigg=8970;e.floorleftBigg=8970;e.floorrightbig=8971;e.floorrightBig=8971;e.floorrightBigg=8971;e.floorrightbigg=8971;e.hatwide=770;e.hatwider=770;e.hatwidest=770;e.intercal=7488;e.integraldisplay=8747;e.integraltext=8747;e.intersectiondisplay=8898;e.intersectiontext=8898;e.logicalanddisplay=8743;e.logicalandtext=8743;e.logicalordisplay=8744;e.logicalortext=8744;e.parenleftBig=40;e.parenleftbig=40;e.parenleftBigg=40;e.parenleftbigg=40;e.parenrightBig=41;e.parenrightbig=41;e.parenrightBigg=41;e.parenrightbigg=41;e.prime=8242;e.productdisplay=8719;e.producttext=8719;e.radicalbig=8730;e.radicalBig=8730;e.radicalBigg=8730;e.radicalbigg=8730;e.radicalbt=8730;e.radicaltp=8730;e.radicalvertex=8730;e.slashbig=47;e.slashBig=47;e.slashBigg=47;e.slashbigg=47;e.summationdisplay=8721;e.summationtext=8721;e.tildewide=732;e.tildewider=732;e.tildewidest=732;e.uniondisplay=8899;e.unionmultidisplay=8846;e.unionmultitext=8846;e.unionsqdisplay=8852;e.unionsqtext=8852;e.uniontext=8899;e.vextenddouble=8741;e.vextendsingle=8739})),Ir=getLookupTableFactory((function(e){e.space=32;e.a1=9985;e.a2=9986;e.a202=9987;e.a3=9988;e.a4=9742;e.a5=9990;e.a119=9991;e.a118=9992;e.a117=9993;e.a11=9755;e.a12=9758;e.a13=9996;e.a14=9997;e.a15=9998;e.a16=9999;e.a105=1e4;e.a17=10001;e.a18=10002;e.a19=10003;e.a20=10004;e.a21=10005;e.a22=10006;e.a23=10007;e.a24=10008;e.a25=10009;e.a26=10010;e.a27=10011;e.a28=10012;e.a6=10013;e.a7=10014;e.a8=10015;e.a9=10016;e.a10=10017;e.a29=10018;e.a30=10019;e.a31=10020;e.a32=10021;e.a33=10022;e.a34=10023;e.a35=9733;e.a36=10025;e.a37=10026;e.a38=10027;e.a39=10028;e.a40=10029;e.a41=10030;e.a42=10031;e.a43=10032;e.a44=10033;e.a45=10034;e.a46=10035;e.a47=10036;e.a48=10037;e.a49=10038;e.a50=10039;e.a51=10040;e.a52=10041;e.a53=10042;e.a54=10043;e.a55=10044;e.a56=10045;e.a57=10046;e.a58=10047;e.a59=10048;e.a60=10049;e.a61=10050;e.a62=10051;e.a63=10052;e.a64=10053;e.a65=10054;e.a66=10055;e.a67=10056;e.a68=10057;e.a69=10058;e.a70=10059;e.a71=9679;e.a72=10061;e.a73=9632;e.a74=10063;e.a203=10064;e.a75=10065;e.a204=10066;e.a76=9650;e.a77=9660;e.a78=9670;e.a79=10070;e.a81=9687;e.a82=10072;e.a83=10073;e.a84=10074;e.a97=10075;e.a98=10076;e.a99=10077;e.a100=10078;e.a101=10081;e.a102=10082;e.a103=10083;e.a104=10084;e.a106=10085;e.a107=10086;e.a108=10087;e.a112=9827;e.a111=9830;e.a110=9829;e.a109=9824;e.a120=9312;e.a121=9313;e.a122=9314;e.a123=9315;e.a124=9316;e.a125=9317;e.a126=9318;e.a127=9319;e.a128=9320;e.a129=9321;e.a130=10102;e.a131=10103;e.a132=10104;e.a133=10105;e.a134=10106;e.a135=10107;e.a136=10108;e.a137=10109;e.a138=10110;e.a139=10111;e.a140=10112;e.a141=10113;e.a142=10114;e.a143=10115;e.a144=10116;e.a145=10117;e.a146=10118;e.a147=10119;e.a148=10120;e.a149=10121;e.a150=10122;e.a151=10123;e.a152=10124;e.a153=10125;e.a154=10126;e.a155=10127;e.a156=10128;e.a157=10129;e.a158=10130;e.a159=10131;e.a160=10132;e.a161=8594;e.a163=8596;e.a164=8597;e.a196=10136;e.a165=10137;e.a192=10138;e.a166=10139;e.a167=10140;e.a168=10141;e.a169=10142;e.a170=10143;e.a171=10144;e.a172=10145;e.a173=10146;e.a162=10147;e.a174=10148;e.a175=10149;e.a176=10150;e.a177=10151;e.a178=10152;e.a179=10153;e.a193=10154;e.a180=10155;e.a199=10156;e.a181=10157;e.a200=10158;e.a182=10159;e.a201=10161;e.a183=10162;e.a184=10163;e.a197=10164;e.a185=10165;e.a194=10166;e.a198=10167;e.a186=10168;e.a195=10169;e.a187=10170;e.a188=10171;e.a189=10172;e.a190=10173;e.a191=10174;e.a89=10088;e.a90=10089;e.a93=10090;e.a94=10091;e.a91=10092;e.a92=10093;e.a205=10094;e.a85=10095;e.a206=10096;e.a86=10097;e.a87=10098;e.a88=10099;e.a95=10100;e.a96=10101;e[".notdef"]=0})),Or=getLookupTableFactory((function(e){e[63721]=169;e[63193]=169;e[63720]=174;e[63194]=174;e[63722]=8482;e[63195]=8482;e[63729]=9127;e[63730]=9128;e[63731]=9129;e[63740]=9131;e[63741]=9132;e[63742]=9133;e[63726]=9121;e[63727]=9122;e[63728]=9123;e[63737]=9124;e[63738]=9125;e[63739]=9126;e[63723]=9115;e[63724]=9116;e[63725]=9117;e[63734]=9118;e[63735]=9119;e[63736]=9120}));function getUnicodeForGlyph(e,t){let a=t[e];if(void 0!==a)return a;if(!e)return-1;if("u"===e[0]){const t=e.length;let r;if(7===t&&"n"===e[1]&&"i"===e[2])r=e.substring(3);else{if(!(t>=5&&t<=7))return-1;r=e.substring(1)}if(r===r.toUpperCase()){a=parseInt(r,16);if(a>=0)return a}}return-1}const Tr=[[0,127],[128,255],[256,383],[384,591],[592,687,7424,7551,7552,7615],[688,767,42752,42783],[768,879,7616,7679],[880,1023],[11392,11519],[1024,1279,1280,1327,11744,11775,42560,42655],[1328,1423],[1424,1535],[42240,42559],[1536,1791,1872,1919],[1984,2047],[2304,2431],[2432,2559],[2560,2687],[2688,2815],[2816,2943],[2944,3071],[3072,3199],[3200,3327],[3328,3455],[3584,3711],[3712,3839],[4256,4351,11520,11567],[6912,7039],[4352,4607],[7680,7935,11360,11391,42784,43007],[7936,8191],[8192,8303,11776,11903],[8304,8351],[8352,8399],[8400,8447],[8448,8527],[8528,8591],[8592,8703,10224,10239,10496,10623,11008,11263],[8704,8959,10752,11007,10176,10223,10624,10751],[8960,9215],[9216,9279],[9280,9311],[9312,9471],[9472,9599],[9600,9631],[9632,9727],[9728,9983],[9984,10175],[12288,12351],[12352,12447],[12448,12543,12784,12799],[12544,12591,12704,12735],[12592,12687],[43072,43135],[12800,13055],[13056,13311],[44032,55215],[55296,57343],[67840,67871],[19968,40959,11904,12031,12032,12255,12272,12287,13312,19903,131072,173791,12688,12703],[57344,63743],[12736,12783,63744,64255,194560,195103],[64256,64335],[64336,65023],[65056,65071],[65040,65055],[65104,65135],[65136,65279],[65280,65519],[65520,65535],[3840,4095],[1792,1871],[1920,1983],[3456,3583],[4096,4255],[4608,4991,4992,5023,11648,11743],[5024,5119],[5120,5759],[5760,5791],[5792,5887],[6016,6143],[6144,6319],[10240,10495],[40960,42127],[5888,5919,5920,5951,5952,5983,5984,6015],[66304,66351],[66352,66383],[66560,66639],[118784,119039,119040,119295,119296,119375],[119808,120831],[1044480,1048573],[65024,65039,917760,917999],[917504,917631],[6400,6479],[6480,6527],[6528,6623],[6656,6687],[11264,11359],[11568,11647],[19904,19967],[43008,43055],[65536,65663,65664,65791,65792,65855],[65856,65935],[66432,66463],[66464,66527],[66640,66687],[66688,66735],[67584,67647],[68096,68191],[119552,119647],[73728,74751,74752,74879],[119648,119679],[7040,7103],[7168,7247],[7248,7295],[43136,43231],[43264,43311],[43312,43359],[43520,43615],[65936,65999],[66e3,66047],[66208,66271,66176,66207,67872,67903],[127024,127135,126976,127023]];function getUnicodeRangeFor(e,t=-1){if(-1!==t){const a=Tr[t];for(let r=0,i=a.length;r=a[r]&&e<=a[r+1])return t}for(let t=0,a=Tr.length;t=a[r]&&e<=a[r+1])return t}return-1}const Mr=new RegExp("^(\\s)|(\\p{Mn})|(\\p{Cf})$","u"),Dr=new Map;const Rr=!0,Nr=1,Er=2,Pr=4,Lr=32,jr=[".notdef",".null","nonmarkingreturn","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","Adieresis","Aring","Ccedilla","Eacute","Ntilde","Odieresis","Udieresis","aacute","agrave","acircumflex","adieresis","atilde","aring","ccedilla","eacute","egrave","ecircumflex","edieresis","iacute","igrave","icircumflex","idieresis","ntilde","oacute","ograve","ocircumflex","odieresis","otilde","uacute","ugrave","ucircumflex","udieresis","dagger","degree","cent","sterling","section","bullet","paragraph","germandbls","registered","copyright","trademark","acute","dieresis","notequal","AE","Oslash","infinity","plusminus","lessequal","greaterequal","yen","mu","partialdiff","summation","product","pi","integral","ordfeminine","ordmasculine","Omega","ae","oslash","questiondown","exclamdown","logicalnot","radical","florin","approxequal","Delta","guillemotleft","guillemotright","ellipsis","nonbreakingspace","Agrave","Atilde","Otilde","OE","oe","endash","emdash","quotedblleft","quotedblright","quoteleft","quoteright","divide","lozenge","ydieresis","Ydieresis","fraction","currency","guilsinglleft","guilsinglright","fi","fl","daggerdbl","periodcentered","quotesinglbase","quotedblbase","perthousand","Acircumflex","Ecircumflex","Aacute","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Oacute","Ocircumflex","apple","Ograve","Uacute","Ucircumflex","Ugrave","dotlessi","circumflex","tilde","macron","breve","dotaccent","ring","cedilla","hungarumlaut","ogonek","caron","Lslash","lslash","Scaron","scaron","Zcaron","zcaron","brokenbar","Eth","eth","Yacute","yacute","Thorn","thorn","minus","multiply","onesuperior","twosuperior","threesuperior","onehalf","onequarter","threequarters","franc","Gbreve","gbreve","Idotaccent","Scedilla","scedilla","Cacute","cacute","Ccaron","ccaron","dcroat"];function recoverGlyphName(e,t){if(void 0!==t[e])return e;const a=getUnicodeForGlyph(e,t);if(-1!==a)for(const e in t)if(t[e]===a)return e;info("Unable to recover a standard glyph name for: "+e);return e}function type1FontGlyphMapping(e,t,a){const r=Object.create(null);let i,n,s;const o=!!(e.flags&Pr);if(e.isInternalFont){s=t;for(n=0;n=0?i:0}}else if(e.baseEncodingName){s=getEncoding(e.baseEncodingName);for(n=0;n=0?i:0}}else if(o)for(n in t)r[n]=t[n];else{s=Ar;for(n=0;n=0?i:0}}const c=e.differences;let l;if(c)for(n in c){const e=c[n];i=a.indexOf(e);if(-1===i){l||(l=Fr());const t=recoverGlyphName(e,l);t!==e&&(i=a.indexOf(t))}r[n]=i>=0?i:0}return r}function normalizeFontName(e){return e.replaceAll(/[,_]/g,"-").replaceAll(/\s/g,"")}const _r=getLookupTableFactory((e=>{e[8211]=65074;e[8212]=65073;e[8229]=65072;e[8230]=65049;e[12289]=65041;e[12290]=65042;e[12296]=65087;e[12297]=65088;e[12298]=65085;e[12299]=65086;e[12300]=65089;e[12301]=65090;e[12302]=65091;e[12303]=65092;e[12304]=65083;e[12305]=65084;e[12308]=65081;e[12309]=65082;e[12310]=65047;e[12311]=65048;e[65103]=65076;e[65281]=65045;e[65288]=65077;e[65289]=65078;e[65292]=65040;e[65306]=65043;e[65307]=65044;e[65311]=65046;e[65339]=65095;e[65341]=65096;e[65343]=65075;e[65371]=65079;e[65373]=65080}));const Ur=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron"],Xr=[".notdef","space","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","onequarter","onehalf","threequarters","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall"],qr=[".notdef","space","dollaroldstyle","dollarsuperior","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","comma","hyphen","period","fraction","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","colon","semicolon","commasuperior","threequartersemdash","periodsuperior","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","fi","fl","ffi","ffl","parenleftinferior","parenrightinferior","hyphensuperior","colonmonetary","onefitted","rupiah","centoldstyle","figuredash","hypheninferior","onequarter","onehalf","threequarters","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","onesuperior","twosuperior","threesuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior"],Hr=[".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright","asciitilde","exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft","guillemotleft","guilsinglleft","guilsinglright","fi","fl","endash","dagger","daggerdbl","periodcentered","paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand","questiondown","grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis","ring","cedilla","hungarumlaut","ogonek","caron","emdash","AE","ordfeminine","Lslash","Oslash","OE","ordmasculine","ae","dotlessi","lslash","oslash","oe","germandbls","onesuperior","logicalnot","mu","trademark","Eth","onehalf","plusminus","Thorn","onequarter","divide","brokenbar","degree","thorn","threequarters","twosuperior","registered","minus","eth","multiply","threesuperior","copyright","Aacute","Acircumflex","Adieresis","Agrave","Aring","Atilde","Ccedilla","Eacute","Ecircumflex","Edieresis","Egrave","Iacute","Icircumflex","Idieresis","Igrave","Ntilde","Oacute","Ocircumflex","Odieresis","Ograve","Otilde","Scaron","Uacute","Ucircumflex","Udieresis","Ugrave","Yacute","Ydieresis","Zcaron","aacute","acircumflex","adieresis","agrave","aring","atilde","ccedilla","eacute","ecircumflex","edieresis","egrave","iacute","icircumflex","idieresis","igrave","ntilde","oacute","ocircumflex","odieresis","ograve","otilde","scaron","uacute","ucircumflex","udieresis","ugrave","yacute","ydieresis","zcaron","exclamsmall","Hungarumlautsmall","dollaroldstyle","dollarsuperior","ampersandsmall","Acutesmall","parenleftsuperior","parenrightsuperior","twodotenleader","onedotenleader","zerooldstyle","oneoldstyle","twooldstyle","threeoldstyle","fouroldstyle","fiveoldstyle","sixoldstyle","sevenoldstyle","eightoldstyle","nineoldstyle","commasuperior","threequartersemdash","periodsuperior","questionsmall","asuperior","bsuperior","centsuperior","dsuperior","esuperior","isuperior","lsuperior","msuperior","nsuperior","osuperior","rsuperior","ssuperior","tsuperior","ff","ffi","ffl","parenleftinferior","parenrightinferior","Circumflexsmall","hyphensuperior","Gravesmall","Asmall","Bsmall","Csmall","Dsmall","Esmall","Fsmall","Gsmall","Hsmall","Ismall","Jsmall","Ksmall","Lsmall","Msmall","Nsmall","Osmall","Psmall","Qsmall","Rsmall","Ssmall","Tsmall","Usmall","Vsmall","Wsmall","Xsmall","Ysmall","Zsmall","colonmonetary","onefitted","rupiah","Tildesmall","exclamdownsmall","centoldstyle","Lslashsmall","Scaronsmall","Zcaronsmall","Dieresissmall","Brevesmall","Caronsmall","Dotaccentsmall","Macronsmall","figuredash","hypheninferior","Ogoneksmall","Ringsmall","Cedillasmall","questiondownsmall","oneeighth","threeeighths","fiveeighths","seveneighths","onethird","twothirds","zerosuperior","foursuperior","fivesuperior","sixsuperior","sevensuperior","eightsuperior","ninesuperior","zeroinferior","oneinferior","twoinferior","threeinferior","fourinferior","fiveinferior","sixinferior","seveninferior","eightinferior","nineinferior","centinferior","dollarinferior","periodinferior","commainferior","Agravesmall","Aacutesmall","Acircumflexsmall","Atildesmall","Adieresissmall","Aringsmall","AEsmall","Ccedillasmall","Egravesmall","Eacutesmall","Ecircumflexsmall","Edieresissmall","Igravesmall","Iacutesmall","Icircumflexsmall","Idieresissmall","Ethsmall","Ntildesmall","Ogravesmall","Oacutesmall","Ocircumflexsmall","Otildesmall","Odieresissmall","OEsmall","Oslashsmall","Ugravesmall","Uacutesmall","Ucircumflexsmall","Udieresissmall","Yacutesmall","Thornsmall","Ydieresissmall","001.000","001.001","001.002","001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"],Wr=391,zr=[null,{id:"hstem",min:2,stackClearing:!0,stem:!0},null,{id:"vstem",min:2,stackClearing:!0,stem:!0},{id:"vmoveto",min:1,stackClearing:!0},{id:"rlineto",min:2,resetStack:!0},{id:"hlineto",min:1,resetStack:!0},{id:"vlineto",min:1,resetStack:!0},{id:"rrcurveto",min:6,resetStack:!0},null,{id:"callsubr",min:1,undefStack:!0},{id:"return",min:0,undefStack:!0},null,null,{id:"endchar",min:0,stackClearing:!0},null,null,null,{id:"hstemhm",min:2,stackClearing:!0,stem:!0},{id:"hintmask",min:0,stackClearing:!0},{id:"cntrmask",min:0,stackClearing:!0},{id:"rmoveto",min:2,stackClearing:!0},{id:"hmoveto",min:1,stackClearing:!0},{id:"vstemhm",min:2,stackClearing:!0,stem:!0},{id:"rcurveline",min:8,resetStack:!0},{id:"rlinecurve",min:8,resetStack:!0},{id:"vvcurveto",min:4,resetStack:!0},{id:"hhcurveto",min:4,resetStack:!0},null,{id:"callgsubr",min:1,undefStack:!0},{id:"vhcurveto",min:4,resetStack:!0},{id:"hvcurveto",min:4,resetStack:!0}],$r=[null,null,null,{id:"and",min:2,stackDelta:-1},{id:"or",min:2,stackDelta:-1},{id:"not",min:1,stackDelta:0},null,null,null,{id:"abs",min:1,stackDelta:0},{id:"add",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]+e[t-1]}},{id:"sub",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]-e[t-1]}},{id:"div",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]/e[t-1]}},null,{id:"neg",min:1,stackDelta:0,stackFn(e,t){e[t-1]=-e[t-1]}},{id:"eq",min:2,stackDelta:-1},null,null,{id:"drop",min:1,stackDelta:-1},null,{id:"put",min:2,stackDelta:-2},{id:"get",min:1,stackDelta:0},{id:"ifelse",min:4,stackDelta:-3},{id:"random",min:0,stackDelta:1},{id:"mul",min:2,stackDelta:-1,stackFn(e,t){e[t-2]=e[t-2]*e[t-1]}},null,{id:"sqrt",min:1,stackDelta:0},{id:"dup",min:1,stackDelta:1},{id:"exch",min:2,stackDelta:0},{id:"index",min:2,stackDelta:0},{id:"roll",min:3,stackDelta:-2},null,null,null,{id:"hflex",min:7,resetStack:!0},{id:"flex",min:13,resetStack:!0},{id:"hflex1",min:9,resetStack:!0},{id:"flex1",min:11,resetStack:!0}];class CFFParser{constructor(e,t,a){this.bytes=e.getBytes();this.properties=t;this.seacAnalysisEnabled=!!a}parse(){const e=this.properties,t=new CFF;this.cff=t;const a=this.parseHeader(),r=this.parseIndex(a.endPos),i=this.parseIndex(r.endPos),n=this.parseIndex(i.endPos),s=this.parseIndex(n.endPos),o=this.parseDict(i.obj.get(0)),c=this.createDict(CFFTopDict,o,t.strings);t.header=a.obj;t.names=this.parseNameIndex(r.obj);t.strings=this.parseStringIndex(n.obj);t.topDict=c;t.globalSubrIndex=s.obj;this.parsePrivateDict(t.topDict);t.isCIDFont=c.hasName("ROS");const l=c.getByName("CharStrings"),h=this.parseIndex(l).obj,u=c.getByName("FontMatrix");u&&(e.fontMatrix=u);const d=c.getByName("FontBBox");if(d){e.ascent=Math.max(d[3],d[1]);e.descent=Math.min(d[1],d[3]);e.ascentScaled=!0}let f,g;if(t.isCIDFont){const e=this.parseIndex(c.getByName("FDArray")).obj;for(let a=0,r=e.count;a=t)throw new FormatError("Invalid CFF header");if(0!==a){info("cff data is shifted");e=e.subarray(a);this.bytes=e}const r=e[0],i=e[1],n=e[2],s=e[3];return{obj:new CFFHeader(r,i,n,s),endPos:n}}parseDict(e){let t=0;function parseOperand(){let a=e[t++];if(30===a)return function parseFloatOperand(){let a="";const r=15,i=["0","1","2","3","4","5","6","7","8","9",".","E","E-",null,"-"],n=e.length;for(;t>4,o=15&n;if(s===r)break;a+=i[s];if(o===r)break;a+=i[o]}return parseFloat(a)}();if(28===a){a=readInt16(e,t);t+=2;return a}if(29===a){a=e[t++];a=a<<8|e[t++];a=a<<8|e[t++];a=a<<8|e[t++];return a}if(a>=32&&a<=246)return a-139;if(a>=247&&a<=250)return 256*(a-247)+e[t++]+108;if(a>=251&&a<=254)return-256*(a-251)-e[t++]-108;warn('CFFParser_parseDict: "'+a+'" is a reserved command.');return NaN}let a=[];const r=[];t=0;const i=e.length;for(;t10)return!1;let i=e.stackSize;const n=e.stack;let s=t.length;for(let o=0;o=4){i-=4;if(this.seacAnalysisEnabled){e.seac=n.slice(i,i+4);return!1}}l=zr[c]}else if(c>=32&&c<=246){n[i]=c-139;i++}else if(c>=247&&c<=254){n[i]=c<251?(c-247<<8)+t[o]+108:-(c-251<<8)-t[o]-108;o++;i++}else if(255===c){n[i]=(t[o]<<24|t[o+1]<<16|t[o+2]<<8|t[o+3])/65536;o+=4;i++}else if(19===c||20===c){e.hints+=i>>1;if(0===e.hints){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}o+=e.hints+7>>3;i%=2;l=zr[c]}else{if(10===c||29===c){const t=10===c?a:r;if(!t){l=zr[c];warn("Missing subrsIndex for "+l.id);return!1}let s=32768;t.count<1240?s=107:t.count<33900&&(s=1131);const o=n[--i]+s;if(o<0||o>=t.count||isNaN(o)){l=zr[c];warn("Out of bounds subrIndex for "+l.id);return!1}e.stackSize=i;e.callDepth++;if(!this.parseCharString(e,t.get(o),a,r))return!1;e.callDepth--;i=e.stackSize;continue}if(11===c){e.stackSize=i;return!0}if(0===c&&o===t.length){t[o-1]=14;l=zr[14]}else{if(9===c){t.copyWithin(o-1,o,-1);o-=1;s-=1;continue}l=zr[c]}}if(l){if(l.stem){e.hints+=i>>1;if(3===c||23===c)e.hasVStems=!0;else if(e.hasVStems&&(1===c||18===c)){warn("CFF stem hints are in wrong order");t[o-1]=1===c?3:23}}if("min"in l&&!e.undefStack&&i=2&&l.stem?i%=2:i>1&&warn("Found too many parameters for stack-clearing command");i>0&&(e.width=n[i-1])}if("stackDelta"in l){"stackFn"in l&&l.stackFn(n,i);i+=l.stackDelta}else if(l.stackClearing)i=0;else if(l.resetStack){i=0;e.undefStack=!1}else if(l.undefStack){i=0;e.undefStack=!0;e.firstStackClearing=!1}}}s=i.length){warn("Invalid fd index for glyph index.");u=!1}if(u){f=i[e].privateDict;d=f.subrsIndex}}else t&&(d=t);u&&(u=this.parseCharString(h,c,d,a));if(null!==h.width){const e=f.getByName("nominalWidthX");o[l]=e+h.width}else{const e=f.getByName("defaultWidthX");o[l]=e}null!==h.seac&&(s[l]=h.seac);u||e.set(l,new Uint8Array([14]))}return{charStrings:e,seacs:s,widths:o}}emptyPrivateDictionary(e){const t=this.createDict(CFFPrivateDict,[],e.strings);e.setByKey(18,[0,0]);e.privateDict=t}parsePrivateDict(e){if(!e.hasName("Private")){this.emptyPrivateDictionary(e);return}const t=e.getByName("Private");if(!Array.isArray(t)||2!==t.length){e.removeByName("Private");return}const a=t[0],r=t[1];if(0===a||r>=this.bytes.length){this.emptyPrivateDictionary(e);return}const i=r+a,n=this.bytes.subarray(r,i),s=this.parseDict(n),o=this.createDict(CFFPrivateDict,s,e.strings);e.privateDict=o;0===o.getByName("ExpansionFactor")&&o.setByName("ExpansionFactor",.06);if(!o.getByName("Subrs"))return;const c=o.getByName("Subrs"),l=r+c;if(0===c||l>=this.bytes.length){this.emptyPrivateDictionary(e);return}const h=this.parseIndex(l);o.subrsIndex=h.obj}parseCharsets(e,t,a,r){if(0===e)return new CFFCharset(!0,Kr.ISO_ADOBE,Ur);if(1===e)return new CFFCharset(!0,Kr.EXPERT,Xr);if(2===e)return new CFFCharset(!0,Kr.EXPERT_SUBSET,qr);const i=this.bytes,n=e,s=i[e++],o=[r?0:".notdef"];let c,l,h;t-=1;switch(s){case 0:for(h=0;h=65535){warn("Not enough space in charstrings to duplicate first glyph.");return}const e=this.charStrings.get(0);this.charStrings.add(e);this.isCIDFont&&this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0])}hasGlyphId(e){if(e<0||e>=this.charStrings.count)return!1;return this.charStrings.get(e).length>0}}class CFFHeader{constructor(e,t,a,r){this.major=e;this.minor=t;this.hdrSize=a;this.offSize=r}}class CFFStrings{constructor(){this.strings=[]}get(e){return e>=0&&e<=390?Hr[e]:e-Wr<=this.strings.length?this.strings[e-Wr]:Hr[0]}getSID(e){let t=Hr.indexOf(e);if(-1!==t)return t;t=this.strings.indexOf(e);return-1!==t?t+Wr:-1}add(e){this.strings.push(e)}get count(){return this.strings.length}}class CFFIndex{constructor(){this.objects=[];this.length=0}add(e){this.length+=e.length;this.objects.push(e)}set(e,t){this.length+=t.length-this.objects[e].length;this.objects[e]=t}get(e){return this.objects[e]}get count(){return this.objects.length}}class CFFDict{constructor(e,t){this.keyToNameMap=e.keyToNameMap;this.nameToKeyMap=e.nameToKeyMap;this.defaults=e.defaults;this.types=e.types;this.opcodes=e.opcodes;this.order=e.order;this.strings=t;this.values=Object.create(null)}setByKey(e,t){if(!(e in this.keyToNameMap))return!1;if(0===t.length)return!0;for(const a of t)if(isNaN(a)){warn(`Invalid CFFDict value: "${t}" for key "${e}".`);return!0}const a=this.types[e];"num"!==a&&"sid"!==a&&"offset"!==a||(t=t[0]);this.values[e]=t;return!0}setByName(e,t){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name "${e}"`);this.values[this.nameToKeyMap[e]]=t}hasName(e){return this.nameToKeyMap[e]in this.values}getByName(e){if(!(e in this.nameToKeyMap))throw new FormatError(`Invalid dictionary name ${e}"`);const t=this.nameToKeyMap[e];return t in this.values?this.values[t]:this.defaults[t]}removeByName(e){delete this.values[this.nameToKeyMap[e]]}static createTables(e){const t={keyToNameMap:{},nameToKeyMap:{},defaults:{},types:{},opcodes:{},order:[]};for(const a of e){const e=Array.isArray(a[0])?(a[0][0]<<8)+a[0][1]:a[0];t.keyToNameMap[e]=a[1];t.nameToKeyMap[a[1]]=e;t.types[e]=a[2];t.defaults[e]=a[3];t.opcodes[e]=Array.isArray(a[0])?a[0]:[a[0]];t.order.push(e)}return t}}const Gr=[[[12,30],"ROS",["sid","sid","num"],null],[[12,20],"SyntheticBase","num",null],[0,"version","sid",null],[1,"Notice","sid",null],[[12,0],"Copyright","sid",null],[2,"FullName","sid",null],[3,"FamilyName","sid",null],[4,"Weight","sid",null],[[12,1],"isFixedPitch","num",0],[[12,2],"ItalicAngle","num",0],[[12,3],"UnderlinePosition","num",-100],[[12,4],"UnderlineThickness","num",50],[[12,5],"PaintType","num",0],[[12,6],"CharstringType","num",2],[[12,7],"FontMatrix",["num","num","num","num","num","num"],[.001,0,0,.001,0,0]],[13,"UniqueID","num",null],[5,"FontBBox",["num","num","num","num"],[0,0,0,0]],[[12,8],"StrokeWidth","num",0],[14,"XUID","array",null],[15,"charset","offset",0],[16,"Encoding","offset",0],[17,"CharStrings","offset",0],[18,"Private",["offset","offset"],null],[[12,21],"PostScript","sid",null],[[12,22],"BaseFontName","sid",null],[[12,23],"BaseFontBlend","delta",null],[[12,31],"CIDFontVersion","num",0],[[12,32],"CIDFontRevision","num",0],[[12,33],"CIDFontType","num",0],[[12,34],"CIDCount","num",8720],[[12,35],"UIDBase","num",null],[[12,37],"FDSelect","offset",null],[[12,36],"FDArray","offset",null],[[12,38],"FontName","sid",null]];class CFFTopDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Gr))}constructor(e){super(CFFTopDict.tables,e);this.privateDict=null}}const Vr=[[6,"BlueValues","delta",null],[7,"OtherBlues","delta",null],[8,"FamilyBlues","delta",null],[9,"FamilyOtherBlues","delta",null],[[12,9],"BlueScale","num",.039625],[[12,10],"BlueShift","num",7],[[12,11],"BlueFuzz","num",1],[10,"StdHW","num",null],[11,"StdVW","num",null],[[12,12],"StemSnapH","delta",null],[[12,13],"StemSnapV","delta",null],[[12,14],"ForceBold","num",0],[[12,17],"LanguageGroup","num",0],[[12,18],"ExpansionFactor","num",.06],[[12,19],"initialRandomSeed","num",0],[20,"defaultWidthX","num",0],[21,"nominalWidthX","num",0],[19,"Subrs","offset",null]];class CFFPrivateDict extends CFFDict{static get tables(){return shadow(this,"tables",this.createTables(Vr))}constructor(e){super(CFFPrivateDict.tables,e);this.subrsIndex=null}}const Kr={ISO_ADOBE:0,EXPERT:1,EXPERT_SUBSET:2};class CFFCharset{constructor(e,t,a,r){this.predefined=e;this.format=t;this.charset=a;this.raw=r}}class CFFEncoding{constructor(e,t,a,r){this.predefined=e;this.format=t;this.encoding=a;this.raw=r}}class CFFFDSelect{constructor(e,t){this.format=e;this.fdSelect=t}getFDIndex(e){return e<0||e>=this.fdSelect.length?-1:this.fdSelect[e]}}class CFFOffsetTracker{constructor(){this.offsets=Object.create(null)}isTracking(e){return e in this.offsets}track(e,t){if(e in this.offsets)throw new FormatError(`Already tracking location of ${e}`);this.offsets[e]=t}offset(e){for(const t in this.offsets)this.offsets[t]+=e}setEntryLocation(e,t,a){if(!(e in this.offsets))throw new FormatError(`Not tracking location of ${e}`);const r=a.data,i=this.offsets[e];for(let e=0,a=t.length;e>24&255;r[s]=l>>16&255;r[o]=l>>8&255;r[c]=255&l}}}class CFFCompiler{constructor(e){this.cff=e}compile(){const e=this.cff,t={data:[],length:0,add(e){try{this.data.push(...e)}catch{this.data=this.data.concat(e)}this.length=this.data.length}},a=this.compileHeader(e.header);t.add(a);const r=this.compileNameIndex(e.names);t.add(r);if(e.isCIDFont&&e.topDict.hasName("FontMatrix")){const t=e.topDict.getByName("FontMatrix");e.topDict.removeByName("FontMatrix");for(const a of e.fdArray){let e=t.slice(0);a.hasName("FontMatrix")&&(e=Util.transform(e,a.getByName("FontMatrix")));a.setByName("FontMatrix",e)}}const i=e.topDict.getByName("XUID");i?.length>16&&e.topDict.removeByName("XUID");e.topDict.setByName("charset",0);let n=this.compileTopDicts([e.topDict],t.length,e.isCIDFont);t.add(n.output);const s=n.trackers[0],o=this.compileStringIndex(e.strings.strings);t.add(o);const c=this.compileIndex(e.globalSubrIndex);t.add(c);if(e.encoding&&e.topDict.hasName("Encoding"))if(e.encoding.predefined)s.setEntryLocation("Encoding",[e.encoding.format],t);else{const a=this.compileEncoding(e.encoding);s.setEntryLocation("Encoding",[t.length],t);t.add(a)}const l=this.compileCharset(e.charset,e.charStrings.count,e.strings,e.isCIDFont);s.setEntryLocation("charset",[t.length],t);t.add(l);const h=this.compileCharStrings(e.charStrings);s.setEntryLocation("CharStrings",[t.length],t);t.add(h);if(e.isCIDFont){s.setEntryLocation("FDSelect",[t.length],t);const a=this.compileFDSelect(e.fdSelect);t.add(a);n=this.compileTopDicts(e.fdArray,t.length,!0);s.setEntryLocation("FDArray",[t.length],t);t.add(n.output);const r=n.trackers;this.compilePrivateDicts(e.fdArray,r,t)}this.compilePrivateDicts([e.topDict],[s],t);t.add([0]);return t.data}encodeNumber(e){return Number.isInteger(e)?this.encodeInteger(e):this.encodeFloat(e)}static get EncodeFloatRegExp(){return shadow(this,"EncodeFloatRegExp",/\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/)}encodeFloat(e){let t=e.toString();const a=CFFCompiler.EncodeFloatRegExp.exec(t);if(a){const r=parseFloat("1e"+((a[2]?+a[2]:0)+a[1].length));t=(Math.round(e*r)/r).toString()}let r,i,n="";for(r=0,i=t.length;r=-107&&e<=107?[e+139]:e>=108&&e<=1131?[247+((e-=108)>>8),255&e]:e>=-1131&&e<=-108?[251+((e=-e-108)>>8),255&e]:e>=-32768&&e<=32767?[28,e>>8&255,255&e]:[29,e>>24&255,e>>16&255,e>>8&255,255&e];return t}compileHeader(e){return[e.major,e.minor,4,e.offSize]}compileNameIndex(e){const t=new CFFIndex;for(const a of e){const e=Math.min(a.length,127);let r=new Array(e);for(let t=0;t"~"||"["===e||"]"===e||"("===e||")"===e||"{"===e||"}"===e||"<"===e||">"===e||"/"===e||"%"===e)&&(e="_");r[t]=e}r=r.join("");""===r&&(r="Bad_Font_Name");t.add(stringToBytes(r))}return this.compileIndex(t)}compileTopDicts(e,t,a){const r=[];let i=new CFFIndex;for(const n of e){if(a){n.removeByName("CIDFontVersion");n.removeByName("CIDFontRevision");n.removeByName("CIDFontType");n.removeByName("CIDCount");n.removeByName("UIDBase")}const e=new CFFOffsetTracker,s=this.compileDict(n,e);r.push(e);i.add(s);e.offset(t)}i=this.compileIndex(i,r);return{trackers:r,output:i}}compilePrivateDicts(e,t,a){for(let r=0,i=e.length;r>8&255,255&e])}else{i=new Uint8Array(1+2*n);i[0]=0;let t=0;const r=e.charset.length;let s=!1;for(let n=1;n>8&255;i[n+1]=255&o}}return this.compileTypedArray(i)}compileEncoding(e){return this.compileTypedArray(e.raw)}compileFDSelect(e){const t=e.format;let a,r;switch(t){case 0:a=new Uint8Array(1+e.fdSelect.length);a[0]=t;for(r=0;r>8&255,255&i,n];for(r=1;r>8&255,255&r,t);n=t}}const o=(s.length-3)/3;s[1]=o>>8&255;s[2]=255&o;s.push(r>>8&255,255&r);a=new Uint8Array(s)}return this.compileTypedArray(a)}compileTypedArray(e){return Array.from(e)}compileIndex(e,t=[]){const a=e.objects,r=a.length;if(0===r)return[0,0];const i=[r>>8&255,255&r];let n,s,o=1;for(n=0;n>8&255,255&c):3===s?i.push(c>>16&255,c>>8&255,255&c):i.push(c>>>24&255,c>>16&255,c>>8&255,255&c);a[n]&&(c+=a[n].length)}for(n=0;n=this.firstChar&&e<=this.lastChar?e:-1}amend(e){unreachable("Should not call amend()")}}class CFFFont{constructor(e,t){this.properties=t;const a=new CFFParser(e,t,Rr);this.cff=a.parse();this.cff.duplicateFirstGlyph();const r=new CFFCompiler(this.cff);this.seacs=this.cff.seacs;try{this.data=r.compile()}catch{warn("Failed to compile font "+t.loadedName);this.data=e}this._createBuiltInEncoding()}get numGlyphs(){return this.cff.charStrings.count}getCharset(){return this.cff.charset.charset}getGlyphMapping(){const e=this.cff,t=this.properties,{cidToGidMap:a,cMap:r}=t,i=e.charset.charset;let n,s;if(t.composite){let t,o;if(a?.length>0){t=Object.create(null);for(let e=0,r=a.length;e=0){const r=a[t];r&&(i[e]=r)}}i.length>0&&(this.properties.builtInEncoding=i)}}function getFloat214(e,t){return readInt16(e,t)/16384}function getSubroutineBias(e){const t=e.length;let a=32768;t<1240?a=107:t<33900&&(a=1131);return a}function parseCmap(e,t,a){const r=1===readUint16(e,t+2)?readUint32(e,t+8):readUint32(e,t+16),i=readUint16(e,t+r);let n,s,o;if(4===i){readUint16(e,t+r+2);const a=readUint16(e,t+r+6)>>1;s=t+r+14;n=[];for(o=0;o>1;a0;)h.push({flags:n})}for(a=0;a>1;y=!0;break;case 4:s+=i.pop();moveTo(n,s);y=!0;break;case 5:for(;i.length>0;){n+=i.shift();s+=i.shift();lineTo(n,s)}break;case 6:for(;i.length>0;){n+=i.shift();lineTo(n,s);if(0===i.length)break;s+=i.shift();lineTo(n,s)}break;case 7:for(;i.length>0;){s+=i.shift();lineTo(n,s);if(0===i.length)break;n+=i.shift();lineTo(n,s)}break;case 8:for(;i.length>0;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 10:m=i.pop();b=null;if(a.isCFFCIDFont){const e=a.fdSelect.getFDIndex(r);if(e>=0&&eMath.abs(s-t)?n+=i.shift():s+=i.shift();bezierCurveTo(l,u,h,d,n,s);break;default:throw new FormatError(`unknown operator: 12 ${w}`)}break;case 14:if(i.length>=4){const e=i.pop(),r=i.pop();s=i.pop();n=i.pop();t.save();t.translate(n,s);let o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Ar[e]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId);t.restore();o=lookupCmap(a.cmap,String.fromCharCode(a.glyphNameMap[Ar[r]]));compileCharString(a.glyphs[o.glyphId],t,a,o.glyphId)}return;case 19:case 20:o+=i.length>>1;c+=o+7>>3;y=!0;break;case 21:s+=i.pop();n+=i.pop();moveTo(n,s);y=!0;break;case 22:n+=i.pop();moveTo(n,s);y=!0;break;case 24:for(;i.length>2;){l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}n+=i.shift();s+=i.shift();lineTo(n,s);break;case 25:for(;i.length>6;){n+=i.shift();s+=i.shift();lineTo(n,s)}l=n+i.shift();u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+i.shift();bezierCurveTo(l,u,h,d,n,s);break;case 26:i.length%2&&(n+=i.shift());for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h;s=d+i.shift();bezierCurveTo(l,u,h,d,n,s)}break;case 27:i.length%2&&(s+=i.shift());for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d;bezierCurveTo(l,u,h,d,n,s)}break;case 28:i.push(readInt16(e,c));c+=2;break;case 29:m=i.pop()+a.gsubrsBias;b=a.gsubrs[m];b&&parse(b);break;case 30:for(;i.length>0;){l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;case 31:for(;i.length>0;){l=n+i.shift();u=s;h=l+i.shift();d=u+i.shift();s=d+i.shift();n=h+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s);if(0===i.length)break;l=n;u=s+i.shift();h=l+i.shift();d=u+i.shift();n=h+i.shift();s=d+(1===i.length?i.shift():0);bezierCurveTo(l,u,h,d,n,s)}break;default:if(w<32)throw new FormatError(`unknown operator: ${w}`);if(w<247)i.push(w-139);else if(w<251)i.push(256*(w-247)+e[c++]+108);else if(w<255)i.push(256*-(w-251)-e[c++]-108);else{i.push((e[c]<<24|e[c+1]<<16|e[c+2]<<8|e[c+3])/65536);c+=4}}y&&(i.length=0)}}(e)}class Commands{cmds=[];transformStack=[];currentTransform=[1,0,0,1,0,0];add(e,t){if(t){const{currentTransform:a}=this;for(let e=0,r=t.length;e=0&&e2*readUint16(e,t)}const n=[];let s=i(t,0);for(let a=r;ae.getSize()+3&-4)))}write(){const e=this.getSize(),t=new DataView(new ArrayBuffer(e)),a=e>131070,r=a?4:2,i=new DataView(new ArrayBuffer((this.glyphs.length+1)*r));a?i.setUint32(0,0):i.setUint16(0,0);let n=0,s=0;for(const e of this.glyphs){n+=e.write(n,t);n=n+3&-4;s+=r;a?i.setUint32(s,n):i.setUint16(s,n>>1)}return{isLocationLong:a,loca:new Uint8Array(i.buffer),glyf:new Uint8Array(t.buffer)}}scale(e){for(let t=0,a=this.glyphs.length;te.getSize())));return this.header.getSize()+e}write(e,t){if(!this.header)return 0;const a=e;e+=this.header.write(e,t);if(this.simple)e+=this.simple.write(e,t);else for(const a of this.composites)e+=a.write(e,t);return e-a}scale(e){if(!this.header)return;const t=(this.header.xMin+this.header.xMax)/2;this.header.scale(t,e);if(this.simple)this.simple.scale(t,e);else for(const a of this.composites)a.scale(t,e)}}class GlyphHeader{constructor({numberOfContours:e,xMin:t,yMin:a,xMax:r,yMax:i}){this.numberOfContours=e;this.xMin=t;this.yMin=a;this.xMax=r;this.yMax=i}static parse(e,t){return[10,new GlyphHeader({numberOfContours:t.getInt16(e),xMin:t.getInt16(e+2),yMin:t.getInt16(e+4),xMax:t.getInt16(e+6),yMax:t.getInt16(e+8)})]}getSize(){return 10}write(e,t){t.setInt16(e,this.numberOfContours);t.setInt16(e+2,this.xMin);t.setInt16(e+4,this.yMin);t.setInt16(e+6,this.xMax);t.setInt16(e+8,this.yMax);return 10}scale(e,t){this.xMin=Math.round(e+(this.xMin-e)*t);this.xMax=Math.round(e+(this.xMax-e)*t)}}class Contour{constructor({flags:e,xCoordinates:t,yCoordinates:a}){this.xCoordinates=t;this.yCoordinates=a;this.flags=e}}class SimpleGlyph{constructor({contours:e,instructions:t}){this.contours=e;this.instructions=t}static parse(e,t,a){const r=[];for(let i=0;i255?e+=2:o>0&&(e+=1);t=n;o=Math.abs(s-a);o>255?e+=2:o>0&&(e+=1);a=s}}return e}write(e,t){const a=e,r=[],i=[],n=[];let s=0,o=0;for(const a of this.contours){for(let e=0,t=a.xCoordinates.length;e=0?18:2;r.push(e)}else r.push(l)}s=c;const h=a.yCoordinates[e];l=h-o;if(0===l){t|=32;i.push(0)}else{const e=Math.abs(l);if(e<=255){t|=l>=0?36:4;i.push(e)}else i.push(l)}o=h;n.push(t)}t.setUint16(e,r.length-1);e+=2}t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}for(const a of n)t.setUint8(e++,a);for(let a=0,i=r.length;a=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(e+=2):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(e+=2);return e}write(e,t){const a=e;2&this.flags?this.argument1>=-128&&this.argument1<=127&&this.argument2>=-128&&this.argument2<=127||(this.flags|=1):this.argument1>=0&&this.argument1<=255&&this.argument2>=0&&this.argument2<=255||(this.flags|=1);t.setUint16(e,this.flags);t.setUint16(e+2,this.glyphIndex);e+=4;if(1&this.flags){if(2&this.flags){t.setInt16(e,this.argument1);t.setInt16(e+2,this.argument2)}else{t.setUint16(e,this.argument1);t.setUint16(e+2,this.argument2)}e+=4}else{t.setUint8(e,this.argument1);t.setUint8(e+1,this.argument2);e+=2}if(256&this.flags){t.setUint16(e,this.instructions.length);e+=2;if(this.instructions.length){new Uint8Array(t.buffer,0,t.buffer.byteLength).set(this.instructions,e);e+=this.instructions.length}}return e-a}scale(e,t){}}function writeInt16(e,t,a){e[t]=a>>8&255;e[t+1]=255&a}function writeInt32(e,t,a){e[t]=a>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}function writeData(e,t,a){if(a instanceof Uint8Array)e.set(a,t);else if("string"==typeof a)for(let r=0,i=a.length;ra;){a<<=1;r++}const i=a*t;return{range:i,entry:r,rangeShift:t*e-i}}toArray(){let e=this.sfnt;const t=this.tables,a=Object.keys(t);a.sort();const r=a.length;let i,n,s,o,c,l=12+16*r;const h=[l];for(i=0;i>>0;h.push(l)}const u=new Uint8Array(l);for(i=0;i>>0}writeInt32(u,l+4,e);writeInt32(u,l+8,h[i]);writeInt32(u,l+12,t[c].length);l+=16}return u}addTable(e,t){if(e in this.tables)throw new Error("Table "+e+" already exists");this.tables[e]=t}}const si=[4],oi=[5],ci=[6],li=[7],hi=[8],ui=[12,35],di=[14],fi=[21],gi=[22],pi=[30],mi=[31];class Type1CharString{constructor(){this.width=0;this.lsb=0;this.flexing=!1;this.output=[];this.stack=[]}convert(e,t,a){const r=e.length;let i,n,s,o=!1;for(let c=0;cr)return!0;const i=r-e;for(let e=i;e>8&255,255&t);else{t=65536*t|0;this.output.push(255,t>>24&255,t>>16&255,t>>8&255,255&t)}}this.output.push(...t);a?this.stack.splice(i,e):this.stack.length=0;return!1}}function isHexDigit(e){return e>=48&&e<=57||e>=65&&e<=70||e>=97&&e<=102}function decrypt(e,t,a){if(a>=e.length)return new Uint8Array(0);let r,i,n=0|t;for(r=0;r>8;n=52845*(t+n)+22719&65535}return o}function isSpecial(e){return 47===e||91===e||93===e||123===e||125===e||40===e||41===e}class Type1Parser{constructor(e,t,a){if(t){const t=e.getBytes(),a=!((isHexDigit(t[0])||isWhiteSpace(t[0]))&&isHexDigit(t[1])&&isHexDigit(t[2])&&isHexDigit(t[3])&&isHexDigit(t[4])&&isHexDigit(t[5])&&isHexDigit(t[6])&&isHexDigit(t[7]));e=new Stream(a?decrypt(t,55665,4):function decryptAscii(e,t,a){let r=0|t;const i=e.length,n=new Uint8Array(i>>>1);let s,o;for(s=0,o=0;s>8;r=52845*(e+r)+22719&65535}}return n.slice(a,o)}(t,55665,4))}this.seacAnalysisEnabled=!!a;this.stream=e;this.nextChar()}readNumberArray(){this.getToken();const e=[];for(;;){const t=this.getToken();if(null===t||"]"===t||"}"===t)break;e.push(parseFloat(t||0))}return e}readNumber(){const e=this.getToken();return parseFloat(e||0)}readInt(){const e=this.getToken();return 0|parseInt(e||0,10)}readBoolean(){return"true"===this.getToken()?1:0}nextChar(){return this.currentChar=this.stream.getByte()}prevChar(){this.stream.skip(-2);return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(-1===t)return null;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}if(isSpecial(t)){this.nextChar();return String.fromCharCode(t)}let a="";do{a+=String.fromCharCode(t);t=this.nextChar()}while(t>=0&&!isWhiteSpace(t)&&!isSpecial(t));return a}readCharStrings(e,t){return-1===t?e:decrypt(e,4330,t)}extractFontProgram(e){const t=this.stream,a=[],r=[],i=Object.create(null);i.lenIV=4;const n={subrs:[],charstrings:[],properties:{privateData:i}};let s,o,c,l;for(;null!==(s=this.getToken());)if("/"===s){s=this.getToken();switch(s){case"CharStrings":this.getToken();this.getToken();this.getToken();this.getToken();for(;;){s=this.getToken();if(null===s||"end"===s)break;if("/"!==s)continue;const e=this.getToken();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const a=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s?this.getToken():"/"===s&&this.prevChar();r.push({glyph:e,encoded:a})}break;case"Subrs":this.readInt();this.getToken();for(;"dup"===this.getToken();){const e=this.readInt();o=this.readInt();this.getToken();c=o>0?t.getBytes(o):new Uint8Array(0);l=n.properties.privateData.lenIV;const r=this.readCharStrings(c,l);this.nextChar();s=this.getToken();"noaccess"===s&&this.getToken();a[e]=r}break;case"BlueValues":case"OtherBlues":case"FamilyBlues":case"FamilyOtherBlues":const e=this.readNumberArray();e.length>0&&e.length,0;break;case"StemSnapH":case"StemSnapV":n.properties.privateData[s]=this.readNumberArray();break;case"StdHW":case"StdVW":n.properties.privateData[s]=this.readNumberArray()[0];break;case"BlueShift":case"lenIV":case"BlueFuzz":case"BlueScale":case"LanguageGroup":n.properties.privateData[s]=this.readNumber();break;case"ExpansionFactor":n.properties.privateData[s]=this.readNumber()||.06;break;case"ForceBold":n.properties.privateData[s]=this.readBoolean()}}for(const{encoded:t,glyph:i}of r){const r=new Type1CharString,s=r.convert(t,a,this.seacAnalysisEnabled);let o=r.output;s&&(o=[14]);const c={glyphName:i,charstring:o,width:r.width,lsb:r.lsb,seac:r.seac};".notdef"===i?n.charstrings.unshift(c):n.charstrings.push(c);if(e.builtInEncoding){const t=e.builtInEncoding.indexOf(i);t>-1&&void 0===e.widths[t]&&t>=e.firstChar&&t<=e.lastChar&&(e.widths[t]=r.width)}}return n}extractFontHeader(e){let t;for(;null!==(t=this.getToken());)if("/"===t){t=this.getToken();switch(t){case"FontMatrix":const a=this.readNumberArray();e.fontMatrix=a;break;case"Encoding":const r=this.getToken();let i;if(/^\d+$/.test(r)){i=[];const e=0|parseInt(r,10);this.getToken();for(let a=0;a=i){s+=a;for(;s=0&&(r[e]=i)}}return type1FontGlyphMapping(e,r,a)}hasGlyphId(e){if(e<0||e>=this.numGlyphs)return!1;if(0===e)return!0;return this.charstrings[e-1].charstring.length>0}getSeacs(e){const t=[];for(let a=0,r=e.length;a0;e--)t[e]-=t[e-1];f.setByName(e,t)}n.topDict.privateDict=f;const p=new CFFIndex;for(h=0,u=r.length;h0&&e.toUnicode.amend(t)}class fonts_Glyph{constructor(e,t,a,r,i,n,s,o,c){this.originalCharCode=e;this.fontChar=t;this.unicode=a;this.accent=r;this.width=i;this.vmetric=n;this.operatorListId=s;this.isSpace=o;this.isInFont=c}get category(){return shadow(this,"category",function getCharUnicodeCategory(e){const t=Dr.get(e);if(t)return t;const a=e.match(Mr),r={isWhitespace:!!a?.[1],isZeroWidthDiacritic:!!a?.[2],isInvisibleFormatMark:!!a?.[3]};Dr.set(e,r);return r}(this.unicode),!0)}}function int16(e,t){return(e<<8)+t}function writeSignedInt16(e,t,a){e[t+1]=a;e[t]=a>>>8}function signedInt16(e,t){const a=(e<<8)+t;return 32768&a?a-65536:a}function string16(e){return String.fromCharCode(e>>8&255,255&e)}function safeString16(e){e>32767?e=32767:e<-32768&&(e=-32768);return String.fromCharCode(e>>8&255,255&e)}function isTrueTypeCollectionFile(e){return"ttcf"===bytesToString(e.peekBytes(4))}function getFontFileType(e,{type:t,subtype:a,composite:r}){let i,n;if(function isTrueTypeFile(e){const t=e.peekBytes(4);return 65536===readUint32(t,0)||"true"===bytesToString(t)}(e)||isTrueTypeCollectionFile(e))i=r?"CIDFontType2":"TrueType";else if(function isOpenTypeFile(e){return"OTTO"===bytesToString(e.peekBytes(4))}(e))i=r?"CIDFontType2":"OpenType";else if(function isType1File(e){const t=e.peekBytes(2);return 37===t[0]&&33===t[1]||128===t[0]&&1===t[1]}(e))i=r?"CIDFontType0":"MMType1"===t?"MMType1":"Type1";else if(function isCFFFile(e){const t=e.peekBytes(4);return t[0]>=1&&t[3]>=1&&t[3]<=4}(e))if(r){i="CIDFontType0";n="CIDFontType0C"}else{i="MMType1"===t?"MMType1":"Type1";n="Type1C"}else{warn("getFontFileType: Unable to detect correct font file Type/Subtype.");i=t;n=a}return[i,n]}function applyStandardFontGlyphMap(e,t){for(const a in t)e[+a]=t[a]}function buildToFontChar(e,t,a){const r=[];let i;for(let a=0,n=e.length;ah){c++;if(c>=bi.length){warn("Ran out of space in font private use area.");break}l=bi[c][0];h=bi[c][1]}const p=l++;0===g&&(g=a);let m=r.get(f);if("string"==typeof m)if(1===m.length)m=m.codePointAt(0);else{if(!u){u=new Map;for(let e=64256;e<=64335;e++){const t=String.fromCharCode(e).normalize("NFKD");t.length>1&&u.set(t,e)}}m=u.get(m)||m.codePointAt(0)}if(m&&!(d=m,bi[0][0]<=d&&d<=bi[0][1]||bi[1][0]<=d&&d<=bi[1][1])&&!o.has(g)){n.set(m,g);o.add(g)}i[p]=g;s[f]=p}var d;return{toFontChar:s,charCodeToGlyphId:i,toUnicodeExtraMap:n,nextAvailableFontCharCode:l}}function createCmapTable(e,t,a){const r=function getRanges(e,t,a){const r=[];for(const t in e)e[t]>=a||r.push({fontCharCode:0|t,glyphId:e[t]});if(t)for(const[e,i]of t)i>=a||r.push({fontCharCode:e,glyphId:i});0===r.length&&r.push({fontCharCode:0,glyphId:0});r.sort(((e,t)=>e.fontCharCode-t.fontCharCode));const i=[],n=r.length;for(let e=0;e65535?2:1;let n,s,o,c,l="\0\0"+string16(i)+"\0\0"+string32(4+8*i);for(n=r.length-1;n>=0&&!(r[n][0]<=65535);--n);const h=n+1;r[n][0]<65535&&65535===r[n][1]&&(r[n][1]=65534);const u=r[n][1]<65535?1:0,d=h+u,f=OpenTypeFileBuilder.getSearchParams(d,2);let g,p,m,b,y="",w="",S="",x="",k="",C=0;for(n=0,s=h;n0){w+="รฟรฟ";y+="รฟรฟ";S+="\0";x+="\0\0"}const v="\0\0"+string16(2*d)+string16(f.range)+string16(f.entry)+string16(f.rangeShift)+w+"\0\0"+y+S+x+k;let F="",O="";if(i>1){l+="\0\0\n"+string32(4+8*i+4+v.length);F="";for(n=0,s=r.length;ne||!o)&&(o=e);c 123 are reserved for internal usage");s|=1<65535&&(c=65535)}else{o=0;c=255}const h=e.bbox||[0,0,0,0],u=a.unitsPerEm||(e.fontMatrix?1/Math.max(...e.fontMatrix.slice(0,4).map(Math.abs)):1e3),d=e.ascentScaled?1:u/yi,f=a.ascent||Math.round(d*(e.ascent||h[3]));let g=a.descent||Math.round(d*(e.descent||h[1]));g>0&&e.descent>0&&h[1]<0&&(g=-g);const p=a.yMax||f,m=-a.yMin||-g;return"\0$รด\0\0\0ยŠยป\0\0\0ยŒยŠยป\0\0รŸ\x001\0\0\0\0"+String.fromCharCode(e.fixedPitch?9:0)+"\0\0\0\0\0\0"+string32(r)+string32(i)+string32(n)+string32(s)+"*21*"+string16(e.italicAngle?1:0)+string16(o||e.firstChar)+string16(c||e.lastChar)+string16(f)+string16(g)+"\0d"+string16(p)+string16(m)+"\0\0\0\0\0\0\0\0"+string16(e.xHeight)+string16(e.capHeight)+string16(0)+string16(o||e.firstChar)+"\0"}function createPostTable(e){return"\0\0\0"+string32(Math.floor(65536*e.italicAngle))+"\0\0\0\0"+string32(e.fixedPitch?1:0)+"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}function createPostscriptName(e){return e.replaceAll(/[^\x21-\x7E]|[[\](){}<>/%]/g,"").slice(0,63)}function createNameTable(e,t){t||(t=[[],[]]);const a=[t[0][0]||"Original licence",t[0][1]||e,t[0][2]||"Unknown",t[0][3]||"uniqueID",t[0][4]||e,t[0][5]||"Version 0.11",t[0][6]||createPostscriptName(e),t[0][7]||"Unknown",t[0][8]||"Unknown",t[0][9]||"Unknown"],r=[];let i,n,s,o,c;for(i=0,n=a.length;i0;if((s||o)&&"CIDFontType2"===a&&this.cidEncoding.startsWith("Identity-")){const a=e.cidToGidMap,r=[];applyStandardFontGlyphMap(r,ti());/Arial-?Black/i.test(t)?applyStandardFontGlyphMap(r,ai()):/Calibri/i.test(t)&&applyStandardFontGlyphMap(r,ri());if(a){for(const e in r){const t=r[e];void 0!==a[t]&&(r[+e]=a[t])}a.length!==this.toUnicode.length&&e.hasIncludedToUnicodeMap&&this.toUnicode instanceof IdentityToUnicodeMap&&this.toUnicode.forEach((function(e,t){const i=r[e];void 0===a[i]&&(r[+e]=t)}))}this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(e,t){r[+e]=t}));this.toFontChar=r;this.toUnicode=new ToUnicodeMap(r)}else if(/Symbol/i.test(r))this.toFontChar=buildToFontChar(Cr,Fr(),this.differences);else if(/Dingbats/i.test(r))this.toFontChar=buildToFontChar(vr,Ir(),this.differences);else if(s||o){const e=buildToFontChar(this.defaultEncoding,Fr(),this.differences);"CIDFontType2"!==a||this.cidEncoding.startsWith("Identity-")||this.toUnicode instanceof IdentityToUnicodeMap||this.toUnicode.forEach((function(t,a){e[+t]=a}));this.toFontChar=e}else{const e=Fr(),a=[];this.toUnicode.forEach(((t,r)=>{if(!this.composite){const a=getUnicodeForGlyph(this.differences[t]||this.defaultEncoding[t],e);-1!==a&&(r=a)}a[+t]=r}));this.composite&&this.toUnicode instanceof IdentityToUnicodeMap&&/Tahoma|Verdana/i.test(t)&&applyStandardFontGlyphMap(a,ti());this.toFontChar=a}amendFallbackToUnicode(e);this.loadedName=r.split("-",1)[0]}checkAndRepair(e,t,a){const r=["OS/2","cmap","head","hhea","hmtx","maxp","name","post","loca","glyf","fpgm","prep","cvt ","CFF "];function readTables(e,t){const a=Object.create(null);a["OS/2"]=null;a.cmap=null;a.head=null;a.hhea=null;a.hmtx=null;a.maxp=null;a.name=null;a.post=null;for(let i=0;i>>0,r=e.getInt32()>>>0,i=e.getInt32()>>>0,n=e.pos;e.pos=e.start||0;e.skip(r);const s=e.getBytes(i);e.pos=n;if("head"===t){s[8]=s[9]=s[10]=s[11]=0;s[17]|=32}return{tag:t,checksum:a,length:i,offset:r,data:s}}function readOpenTypeHeader(e){return{version:e.getString(4),numTables:e.getUint16(),searchRange:e.getUint16(),entrySelector:e.getUint16(),rangeShift:e.getUint16()}}function sanitizeGlyph(e,t,a,r,i,n){const s={length:0,sizeOfInstructions:0};if(t<0||t>=e.length||a>e.length||a-t<=12)return s;const o=e.subarray(t,a),c=signedInt16(o[2],o[3]),l=signedInt16(o[4],o[5]),h=signedInt16(o[6],o[7]),u=signedInt16(o[8],o[9]);if(c>h){writeSignedInt16(o,2,h);writeSignedInt16(o,6,c)}if(l>u){writeSignedInt16(o,4,u);writeSignedInt16(o,8,l)}const d=signedInt16(o[0],o[1]);if(d<0){if(d<-1)return s;r.set(o,i);s.length=o.length;return s}let f,g=10,p=0;for(f=0;fo.length)return s;if(!n&&b>0){r.set(o.subarray(0,m),i);r.set([0,0],i+m);r.set(o.subarray(y,S),i+m+2);S-=b;o.length-S>3&&(S=S+3&-4);s.length=S;return s}if(o.length-S>3){S=S+3&-4;r.set(o.subarray(0,S),i);s.length=S;return s}r.set(o,i);s.length=o.length;return s}function readNameTable(e){const a=(t.start||0)+e.offset;t.pos=a;const r=[[],[]],i=[],n=e.length,s=a+n;if(0!==t.getUint16()||n<6)return[r,i];const o=t.getUint16(),c=t.getUint16();let l,h;for(l=0;ls)continue;t.pos=n;const o=e.name;if(e.encoding){let a="";for(let r=0,i=e.length;r0&&(l+=e-1)}}else{if(m||y){warn("TT: nested FDEFs not allowed");p=!0}m=!0;u=l;s=d.pop();t.functionsDefined[s]={data:c,i:l}}else if(!m&&!y){s=d.at(-1);if(isNaN(s))info("TT: CALL empty stack (or invalid entry).");else{t.functionsUsed[s]=!0;if(s in t.functionsStackDeltas){const e=d.length+t.functionsStackDeltas[s];if(e<0){warn("TT: CALL invalid functions stack delta.");t.hintsValid=!1;return}d.length=e}else if(s in t.functionsDefined&&!g.includes(s)){f.push({data:c,i:l,stackTop:d.length-1});g.push(s);o=t.functionsDefined[s];if(!o){warn("TT: CALL non-existent function");t.hintsValid=!1;return}c=o.data;l=o.i}}}if(!m&&!y){let t=0;e<=142?t=i[e]:e>=192&&e<=223?t=-1:e>=224&&(t=-2);if(e>=113&&e<=117){r=d.pop();isNaN(r)||(t=2*-r)}for(;t<0&&d.length>0;){d.pop();t++}for(;t>0;){d.push(NaN);t--}}}t.tooComplexToFollowFunctions=p;const w=[c];l>c.length&&w.push(new Uint8Array(l-c.length));if(u>h){warn("TT: complementing a missing function tail");w.push(new Uint8Array([34,45]))}!function foldTTTable(e,t){if(t.length>1){let a,r,i=0;for(a=0,r=t.length;a>>0,n=[];for(let t=0;t>>0);const s={ttcTag:t,majorVersion:a,minorVersion:r,numFonts:i,offsetTable:n};switch(a){case 1:return s;case 2:s.dsigTag=e.getInt32()>>>0;s.dsigLength=e.getInt32()>>>0;s.dsigOffset=e.getInt32()>>>0;return s}throw new FormatError(`Invalid TrueType Collection majorVersion: ${a}.`)}(e),i=t.split("+");let n;for(let s=0;s0||!(a.cMap instanceof IdentityCMap));if("OTTO"===n.version&&!t||!s.head||!s.hhea||!s.maxp||!s.post){c=new Stream(s["CFF "].data);o=new CFFFont(c,a);return this.convert(e,o,a)}delete s.glyf;delete s.loca;delete s.fpgm;delete s.prep;delete s["cvt "];this.isOpenType=!0}if(!s.maxp)throw new FormatError('Required "maxp" table is not found');t.pos=(t.start||0)+s.maxp.offset;let h=t.getInt32();const u=t.getUint16();if(65536!==h&&20480!==h){if(6===s.maxp.length)h=20480;else{if(!(s.maxp.length>=32))throw new FormatError('"maxp" table has a wrong version number');h=65536}!function writeUint32(e,t,a){e[t+3]=255&a;e[t+2]=a>>>8;e[t+1]=a>>>16;e[t]=a>>>24}(s.maxp.data,0,h)}if(a.scaleFactors?.length===u&&l){const{scaleFactors:e}=a,t=int16(s.head.data[50],s.head.data[51]),r=new GlyfTable({glyfTable:s.glyf.data,isGlyphLocationsLong:t,locaTable:s.loca.data,numGlyphs:u});r.scale(e);const{glyf:i,loca:n,isLocationLong:o}=r.write();s.glyf.data=i;s.loca.data=n;if(o!==!!t){s.head.data[50]=0;s.head.data[51]=o?1:0}const c=s.hmtx.data;for(let t=0;t>8&255;c[a+1]=255&r;writeSignedInt16(c,a+2,Math.round(e[t]*signedInt16(c[a+2],c[a+3])))}}let d=u+1,f=!0;if(d>65535){f=!1;d=u;warn("Not enough space in glyfs to duplicate first glyph.")}let g=0,p=0;if(h>=65536&&s.maxp.length>=32){t.pos+=8;if(t.getUint16()>2){s.maxp.data[14]=0;s.maxp.data[15]=2}t.pos+=4;g=t.getUint16();t.pos+=4;p=t.getUint16()}s.maxp.data[4]=d>>8;s.maxp.data[5]=255&d;const m=function sanitizeTTPrograms(e,t,a,r){const i={functionsDefined:[],functionsUsed:[],functionsStackDeltas:[],tooComplexToFollowFunctions:!1,hintsValid:!0};e&&sanitizeTTProgram(e,i);t&&sanitizeTTProgram(t,i);e&&function checkInvalidFunctions(e,t){if(!e.tooComplexToFollowFunctions)if(e.functionsDefined.length>t){warn("TT: more functions defined than expected");e.hintsValid=!1}else for(let a=0,r=e.functionsUsed.length;at){warn("TT: invalid function id: "+a);e.hintsValid=!1;return}if(e.functionsUsed[a]&&!e.functionsDefined[a]){warn("TT: undefined function: "+a);e.hintsValid=!1;return}}}(i,r);if(a&&1&a.length){const e=new Uint8Array(a.length+1);e.set(a.data);a.data=e}return i.hintsValid}(s.fpgm,s.prep,s["cvt "],g);if(!m){delete s.fpgm;delete s.prep;delete s["cvt "]}!function sanitizeMetrics(e,t,a,r,i,n){if(!t){a&&(a.data=null);return}e.pos=(e.start||0)+t.offset;e.pos+=4;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;e.pos+=2;const s=e.getUint16();e.pos+=8;e.pos+=2;let o=e.getUint16();if(0!==s){if(!(2&int16(r.data[44],r.data[45]))){t.data[22]=0;t.data[23]=0}}if(o>i){info(`The numOfMetrics (${o}) should not be greater than the numGlyphs (${i}).`);o=i;t.data[34]=(65280&o)>>8;t.data[35]=255&o}const c=i-o-(a.length-4*o>>1);if(c>0){const e=new Uint8Array(a.length+2*c);e.set(a.data);if(n){e[a.length]=a.data[2];e[a.length+1]=a.data[3]}a.data=e}}(t,s.hhea,s.hmtx,s.head,d,f);if(!s.head)throw new FormatError('Required "head" table is not found');!function sanitizeHead(e,t,a){const r=e.data,i=function int32(e,t,a,r){return(e<<24)+(t<<16)+(a<<8)+r}(r[0],r[1],r[2],r[3]);if(i>>16!=1){info("Attempting to fix invalid version in head table: "+i);r[0]=0;r[1]=1;r[2]=0;r[3]=0}const n=int16(r[50],r[51]);if(n<0||n>1){info("Attempting to fix invalid indexToLocFormat in head table: "+n);const e=t+1;if(a===e<<1){r[50]=0;r[51]=0}else{if(a!==e<<2)throw new FormatError("Could not fix indexToLocFormat: "+n);r[50]=0;r[51]=1}}}(s.head,u,l?s.loca.length:0);let b=Object.create(null);if(l){const e=int16(s.head.data[50],s.head.data[51]),t=function sanitizeGlyphLocations(e,t,a,r,i,n,s){let o,c,l;if(r){o=4;c=function fontItemDecodeLong(e,t){return e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3]};l=function fontItemEncodeLong(e,t,a){e[t]=a>>>24&255;e[t+1]=a>>16&255;e[t+2]=a>>8&255;e[t+3]=255&a}}else{o=2;c=function fontItemDecode(e,t){return e[t]<<9|e[t+1]<<1};l=function fontItemEncode(e,t,a){e[t]=a>>9&255;e[t+1]=a>>1&255}}const h=n?a+1:a,u=o*(1+h),d=new Uint8Array(u);d.set(e.data.subarray(0,u));e.data=d;const f=t.data,g=f.length,p=new Uint8Array(g);let m,b;const y=[];for(m=0,b=0;mg&&(e=g);y.push({index:m,offset:e,endOffset:0})}y.sort(((e,t)=>e.offset-t.offset));for(m=0;me.index-t.index));for(m=0;ms&&(s=e.sizeOfInstructions);x+=t;l(d,b,x)}if(0===x){const e=new Uint8Array([0,1,0,0,0,0,0,0,0,0,0,0,0,0,49,0]);for(m=0,b=o;ma+x)t.data=p.subarray(0,a+x);else{t.data=new Uint8Array(a+x);t.data.set(p.subarray(0,x))}t.data.set(p.subarray(0,a),x);l(e.data,d.length-o,x+a)}else t.data=p.subarray(0,x);return{missingGlyphs:S,maxSizeOfInstructions:s}}(s.loca,s.glyf,u,e,m,f,p);b=t.missingGlyphs;if(h>=65536&&s.maxp.length>=32){s.maxp.data[26]=t.maxSizeOfInstructions>>8;s.maxp.data[27]=255&t.maxSizeOfInstructions}}if(!s.hhea)throw new FormatError('Required "hhea" table is not found');if(0===s.hhea.data[10]&&0===s.hhea.data[11]){s.hhea.data[10]=255;s.hhea.data[11]=255}const y={unitsPerEm:int16(s.head.data[18],s.head.data[19]),yMax:signedInt16(s.head.data[42],s.head.data[43]),yMin:signedInt16(s.head.data[38],s.head.data[39]),ascent:signedInt16(s.hhea.data[4],s.hhea.data[5]),descent:signedInt16(s.hhea.data[6],s.hhea.data[7]),lineGap:signedInt16(s.hhea.data[8],s.hhea.data[9])};this.ascent=y.ascent/y.unitsPerEm;this.descent=y.descent/y.unitsPerEm;this.lineGap=y.lineGap/y.unitsPerEm;if(this.cssFontInfo?.lineHeight){this.lineHeight=this.cssFontInfo.metrics.lineHeight;this.lineGap=this.cssFontInfo.metrics.lineGap}else this.lineHeight=this.ascent-this.descent+this.lineGap;s.post&&function readPostScriptTable(e,a,r){const i=(t.start||0)+e.offset;t.pos=i;const n=i+e.length,s=t.getInt32();t.skip(28);let o,c,l=!0;switch(s){case 65536:o=jr;break;case 131072:const e=t.getUint16();if(e!==r){l=!1;break}const i=[];for(c=0;c=32768){l=!1;break}i.push(e)}if(!l)break;const h=[],u=[];for(;t.pos65535)throw new FormatError("Max size of CID is 65,535");let i=-1;t?i=r:void 0!==e[r]&&(i=e[r]);i>=0&&i>>0;let h=!1;if(o?.platformId!==i||o?.encodingId!==n){if(0!==i||0!==n&&1!==n&&3!==n)if(1===i&&0===n)h=!0;else if(3!==i||1!==n||!r&&o){if(a&&3===i&&0===n){h=!0;let a=!0;if(e>3;e.push(r);a=Math.max(r,a)}const r=[];for(let e=0;e<=a;e++)r.push({firstCode:t.getUint16(),entryCount:t.getUint16(),idDelta:signedInt16(t.getByte(),t.getByte()),idRangePos:t.pos+t.getUint16()});for(let a=0;a<256;a++)if(0===e[a]){t.pos=r[0].idRangePos+2*a;f=t.getUint16();u.push({charCode:a,glyphId:f})}else{const i=r[e[a]];for(d=0;d>1;t.skip(6);const a=[];let r;for(r=0;r>1)-(e-r);i.offsetIndex=s;o=Math.max(o,s+i.end-i.start+1)}else i.offsetIndex=-1}const c=[];for(d=0;d>>0;for(d=0;d>>0,a=t.getInt32()>>>0;let r=t.getInt32()>>>0;for(let t=e;t<=a;t++)u.push({charCode:t,glyphId:r++})}}}u.sort(((e,t)=>e.charCode-t.charCode));const g=[],p=new Set;for(const e of u){const{charCode:t}=e;if(!p.has(t)){p.add(t);g.push(e)}}return{platformId:o.platformId,encodingId:o.encodingId,mappings:g,hasShortCmap:h}}(s.cmap,t,this.isSymbolicFont,a.hasEncoding),r=e.platformId,i=e.encodingId,n=e.mappings;let o=[],c=!1;!a.hasEncoding||"MacRomanEncoding"!==a.baseEncodingName&&"WinAnsiEncoding"!==a.baseEncodingName||(o=getEncoding(a.baseEncodingName));if(a.hasEncoding&&!this.isSymbolicFont&&(3===r&&1===i||1===r&&0===i)){const e=Fr();for(let t=0;t<256;t++){let s;s=void 0!==this.differences[t]?this.differences[t]:o.length&&""!==o[t]?o[t]:Ar[t];if(!s)continue;const c=recoverGlyphName(s,e);let l;3===r&&1===i?l=e[c]:1===r&&0===i&&(l=xr.indexOf(c));if(void 0===l){if(!a.glyphNames&&a.hasIncludedToUnicodeMap&&!(this.toUnicode instanceof IdentityToUnicodeMap)){const e=this.toUnicode.get(t);e&&(l=e.codePointAt(0))}if(void 0===l)continue}for(const e of n)if(e.charCode===l){w[t]=e.glyphId;break}}}else if(0===r){for(const e of n)w[e.charCode]=e.glyphId;c=!0}else if(3===r&&0===i)for(const e of n){let t=e.charCode;t>=61440&&t<=61695&&(t&=255);w[t]=e.glyphId}else for(const e of n)w[e.charCode]=e.glyphId;if(a.glyphNames&&(o.length||this.differences.length))for(let e=0;e<256;++e){if(!c&&void 0!==w[e])continue;const t=this.differences[e]||o[e];if(!t)continue;const r=a.glyphNames.indexOf(t);r>0&&hasGlyph(r)&&(w[e]=r)}}0===w.length&&(w[0]=0);let S=d-1;f||(S=0);if(!a.cssFontInfo){const e=adjustMapping(w,hasGlyph,S,this.toUnicode);this.toFontChar=e.toFontChar;s.cmap={tag:"cmap",data:createCmapTable(e.charCodeToGlyphId,e.toUnicodeExtraMap,d)};s["OS/2"]&&function validateOS2Table(e,t){t.pos=(t.start||0)+e.offset;const a=t.getUint16();t.skip(60);const r=t.getUint16();if(a<4&&768&r)return!1;if(t.getUint16()>t.getUint16())return!1;t.skip(6);if(0===t.getUint16())return!1;e.data[8]=e.data[9]=0;return!0}(s["OS/2"],t)||(s["OS/2"]={tag:"OS/2",data:createOS2Table(a,e.charCodeToGlyphId,y)})}if(!l)try{c=new Stream(s["CFF "].data);o=new CFFParser(c,a,Rr).parse();o.duplicateFirstGlyph();const e=new CFFCompiler(o);s["CFF "].data=e.compile()}catch{warn("Failed to compile font "+a.loadedName)}if(s.name){const[t,r]=readNameTable(s.name);s.name.data=createNameTable(e,t);this.psName=t[0][6]||null;a.composite||function adjustTrueTypeToUnicode(e,t,a){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(e.hasEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;if(!t)return;if(0===a.length)return;if(e.defaultEncoding===kr)return;for(const e of a)if(!isWinNameRecord(e))return;const r=kr,i=[],n=Fr();for(const e in r){const t=r[e];if(""===t)continue;const a=n[t];void 0!==a&&(i[e]=String.fromCharCode(a))}i.length>0&&e.toUnicode.amend(i)}(a,this.isSymbolicFont,r)}else s.name={tag:"name",data:createNameTable(this.name)};const x=new OpenTypeFileBuilder(n.version);for(const e in s)x.addTable(e,s[e].data);return x.toArray()}convert(e,a,r){r.fixedPitch=!1;r.builtInEncoding&&function adjustType1ToUnicode(e,t){if(e.isInternalFont)return;if(e.hasIncludedToUnicodeMap)return;if(t===e.defaultEncoding)return;if(e.toUnicode instanceof IdentityToUnicodeMap)return;const a=[],r=Fr();for(const i in t){if(e.hasEncoding&&(e.baseEncodingName||void 0!==e.differences[i]))continue;const n=getUnicodeForGlyph(t[i],r);-1!==n&&(a[i]=String.fromCharCode(n))}a.length>0&&e.toUnicode.amend(a)}(r,r.builtInEncoding);let i=1;a instanceof CFFFont&&(i=a.numGlyphs-1);const n=a.getGlyphMapping(r);let s=null,o=n,c=null;if(!r.cssFontInfo){s=adjustMapping(n,a.hasGlyphId.bind(a),i,this.toUnicode);this.toFontChar=s.toFontChar;o=s.charCodeToGlyphId;c=s.toUnicodeExtraMap}const l=a.numGlyphs;function getCharCodes(e,t){let a=null;for(const r in e)t===e[r]&&(a||=[]).push(0|r);return a}function createCharCode(e,t){for(const a in e)if(t===e[a])return 0|a;s.charCodeToGlyphId[s.nextAvailableFontCharCode]=t;return s.nextAvailableFontCharCode++}const h=a.seacs;if(s&&h?.length){const e=r.fontMatrix||t,i=a.getCharset(),o=Object.create(null);for(let t in h){t|=0;const a=h[t],r=Ar[a[2]],c=Ar[a[3]],l=i.indexOf(r),u=i.indexOf(c);if(l<0||u<0)continue;const d={x:a[0]*e[0]+a[1]*e[2]+e[4],y:a[0]*e[1]+a[1]*e[3]+e[5]},f=getCharCodes(n,t);if(f)for(const e of f){const t=s.charCodeToGlyphId,a=createCharCode(t,l),r=createCharCode(t,u);o[e]={baseFontCharCode:a,accentFontCharCode:r,accentOffset:d}}}r.seacMap=o}const u=r.fontMatrix?1/Math.max(...r.fontMatrix.slice(0,4).map(Math.abs)):1e3,d=new OpenTypeFileBuilder("OTTO");d.addTable("CFF ",a.data);d.addTable("OS/2",createOS2Table(r,o));d.addTable("cmap",createCmapTable(o,c,l));d.addTable("head","\0\0\0\0\0\0\0\0\0\0_<รต\0\0"+safeString16(u)+"\0\0\0\0ยž\v~'\0\0\0\0ยž\v~'\0\0"+safeString16(r.descent)+"รฟ"+safeString16(r.ascent)+string16(r.italicAngle?2:0)+"\0\0\0\0\0\0\0");d.addTable("hhea","\0\0\0"+safeString16(r.ascent)+safeString16(r.descent)+"\0\0รฟรฟ\0\0\0\0\0\0"+safeString16(r.capHeight)+safeString16(Math.tan(r.italicAngle)*r.xHeight)+"\0\0\0\0\0\0\0\0\0\0\0\0"+string16(l));d.addTable("hmtx",function fontFieldsHmtx(){const e=a.charstrings,t=a.cff?a.cff.widths:null;let r="\0\0\0\0";for(let a=1,i=l;a=65520&&e<=65535?0:e>=62976&&e<=63743?Or()[e]||e:173===e?45:e}(a)}this.isType3Font&&(i=a);let h=null;if(this.seacMap?.[e]){l=!0;const t=this.seacMap[e];a=t.baseFontCharCode;h={fontChar:String.fromCodePoint(t.accentFontCharCode),offset:t.accentOffset}}let u="";"number"==typeof a&&(a<=1114111?u=String.fromCodePoint(a):warn(`charToGlyph - invalid fontCharCode: ${a}`));if(this.missingFile&&this.vertical&&1===u.length){const e=_r()[u.charCodeAt(0)];e&&(u=c=String.fromCharCode(e))}n=new fonts_Glyph(e,u,c,h,r,o,i,t,l);return this._glyphCache[e]=n}charsToGlyphs(e){let t=this._charsCache[e];if(t)return t;t=[];if(this.cMap){const a=Object.create(null),r=e.length;let i=0;for(;it.length%2==1,r=this.toUnicode instanceof IdentityToUnicodeMap?e=>this.toUnicode.charCodeOf(e):e=>this.toUnicode.charCodeOf(String.fromCodePoint(e));for(let i=0,n=e.length;i55295&&(n<57344||n>65533)&&i++;if(this.toUnicode){const e=r(n);if(-1!==e){if(hasCurrentBufErrors()){t.push(a.join(""));a.length=0}for(let t=(this.cMap?this.cMap.getCharCodeLength(e):1)-1;t>=0;t--)a.push(String.fromCharCode(e>>8*t&255));continue}}if(!hasCurrentBufErrors()){t.push(a.join(""));a.length=0}a.push(String.fromCodePoint(n))}t.push(a.join(""));return t}}class ErrorFont{constructor(e){this.error=e;this.loadedName="g_font_error";this.missingFile=!0}charsToGlyphs(){return[]}encodeString(e){return[e]}exportData(){return{error:this.error}}}const xi=2,Ai=3,ki=4,Ci=5,vi=6,Fi=7;class Pattern{constructor(){unreachable("Cannot initialize Pattern.")}static parseShading(e,t,a,r,i,n){const s=e instanceof BaseStream?e.dict:e,o=s.get("ShadingType");try{switch(o){case xi:case Ai:return new RadialAxialShading(s,t,a,r,i,n);case ki:case Ci:case vi:case Fi:return new MeshShading(e,t,a,r,i,n);default:throw new FormatError("Unsupported ShadingType: "+o)}}catch(e){if(e instanceof MissingDataException)throw e;warn(e);return new DummyShading}}}class BaseShading{static SMALL_NUMBER=1e-6;getIR(){unreachable("Abstract method `getIR` called.")}}class RadialAxialShading extends BaseShading{constructor(e,t,a,r,i,n){super();this.shadingType=e.get("ShadingType");let s=0;this.shadingType===xi?s=4:this.shadingType===Ai&&(s=6);this.coordsArr=e.getArray("Coords");if(!isNumberArray(this.coordsArr,s))throw new FormatError("RadialAxialShading: Invalid /Coords array.");const o=ColorSpaceUtils.parse({cs:e.getRaw("CS")||e.getRaw("ColorSpace"),xref:t,resources:a,pdfFunctionFactory:r,globalColorSpaceCache:i,localColorSpaceCache:n});this.bbox=lookupNormalRect(e.getArray("BBox"),null);let c=0,l=1;const h=e.getArray("Domain");isNumberArray(h,2)&&([c,l]=h);let u=!1,d=!1;const f=e.getArray("Extend");(function isBooleanArray(e,t){return Array.isArray(e)&&(null===t||e.length===t)&&e.every((e=>"boolean"==typeof e))})(f,2)&&([u,d]=f);if(!(this.shadingType!==Ai||u&&d)){const[e,t,a,r,i,n]=this.coordsArr,s=Math.hypot(e-r,t-i);a<=n+s&&n<=a+s&&warn("Unsupported radial gradient.")}this.extendStart=u;this.extendEnd=d;const g=e.getRaw("Function"),p=r.create(g,!0),m=(l-c)/840,b=this.colorStops=[];if(c>=l||m<=0){info("Bad shading domain.");return}const y=new Float32Array(o.numComps),w=new Float32Array(1);let S=0;w[0]=c;p(w,0,y,0);const x=new Uint8ClampedArray(3);o.getRgb(y,0,x);let[k,C,v]=x;b.push([0,Util.makeHexColor(k,C,v)]);let F=1;w[0]=c+m;p(w,0,y,0);o.getRgb(y,0,x);let[O,T,M]=x,D=O-k+1,R=T-C+1,N=M-v+1,E=O-k-1,L=T-C-1,j=M-v-1;for(let e=2;e<840;e++){w[0]=c+e*m;p(w,0,y,0);o.getRgb(y,0,x);const[t,a,r]=x,i=e-S;D=Math.min(D,(t-k+1)/i);R=Math.min(R,(a-C+1)/i);N=Math.min(N,(r-v+1)/i);E=Math.max(E,(t-k-1)/i);L=Math.max(L,(a-C-1)/i);j=Math.max(j,(r-v-1)/i);if(!(E<=D&&L<=R&&j<=N)){const e=Util.makeHexColor(O,T,M);b.push([F/840,e]);D=t-O+1;R=a-T+1;N=r-M+1;E=t-O-1;L=a-T-1;j=r-M-1;S=F;k=O;C=T;v=M}F=e;O=t;T=a;M=r}b.push([1,Util.makeHexColor(O,T,M)]);let _="transparent";e.has("Background")&&(_=o.getRgbHex(e.get("Background"),0));if(!u){b.unshift([0,_]);b[1][0]+=BaseShading.SMALL_NUMBER}if(!d){b.at(-1)[0]-=BaseShading.SMALL_NUMBER;b.push([1,_])}this.colorStops=b}getIR(){const{coordsArr:e,shadingType:t}=this;let a,r,i,n,s;if(t===xi){r=[e[0],e[1]];i=[e[2],e[3]];n=null;s=null;a="axial"}else if(t===Ai){r=[e[0],e[1]];i=[e[3],e[4]];n=e[2];s=e[5];a="radial"}else unreachable(`getPattern type unknown: ${t}`);return["RadialAxial",a,this.bbox,this.colorStops,r,i,n,s]}}class MeshStreamReader{constructor(e,t){this.stream=e;this.context=t;this.buffer=0;this.bufferLength=0;const a=t.numComps;this.tmpCompsBuf=new Float32Array(a);const r=t.colorSpace.numComps;this.tmpCsCompsBuf=t.colorFn?new Float32Array(r):this.tmpCompsBuf}get hasData(){if(this.stream.end)return this.stream.pos0)return!0;const e=this.stream.getByte();if(e<0)return!1;this.buffer=e;this.bufferLength=8;return!0}readBits(e){const{stream:t}=this;let{buffer:a,bufferLength:r}=this;if(32===e){if(0===r)return t.getInt32()>>>0;a=a<<24|t.getByte()<<16|t.getByte()<<8|t.getByte();const e=t.getByte();this.buffer=e&(1<>r)>>>0}if(8===e&&0===r)return t.getByte();for(;r>r}align(){this.buffer=0;this.bufferLength=0}readFlag(){return this.readBits(this.context.bitsPerFlag)}readCoordinate(){const{bitsPerCoordinate:e,decode:t}=this.context,a=this.readBits(e),r=this.readBits(e),i=e<32?1/((1<n?n:e;t=t>s?s:t;a=ae*i[t])):a;let s,o=-2;const c=[];for(const[e,t]of r.map(((e,t)=>[e,t])).sort((([e],[t])=>e-t)))if(-1!==e)if(e===o+1){s.push(n[t]);o+=1}else{o=e;s=[n[t]];c.push(e,s)}return c}(e),a=new Dict(null);a.set("BaseFont",Name.get(e));a.set("Type",Name.get("Font"));a.set("Subtype",Name.get("CIDFontType2"));a.set("Encoding",Name.get("Identity-H"));a.set("CIDToGIDMap",Name.get("Identity"));a.set("W",t);a.set("FirstChar",t[0]);a.set("LastChar",t.at(-2)+t.at(-1).length-1);const r=new Dict(null);a.set("FontDescriptor",r);const i=new Dict(null);i.set("Ordering","Identity");i.set("Registry","Adobe");i.set("Supplement",0);a.set("CIDSystemInfo",i);return a}class PostScriptParser{constructor(e){this.lexer=e;this.operators=[];this.token=null;this.prev=null}nextToken(){this.prev=this.token;this.token=this.lexer.getToken()}accept(e){if(this.token.type===e){this.nextToken();return!0}return!1}expect(e){if(this.accept(e))return!0;throw new FormatError(`Unexpected symbol: found ${this.token.type} expected ${e}.`)}parse(){this.nextToken();this.expect(yn.LBRACE);this.parseBlock();this.expect(yn.RBRACE);return this.operators}parseBlock(){for(;;)if(this.accept(yn.NUMBER))this.operators.push(this.prev.value);else if(this.accept(yn.OPERATOR))this.operators.push(this.prev.value);else{if(!this.accept(yn.LBRACE))return;this.parseCondition()}}parseCondition(){const e=this.operators.length;this.operators.push(null,null);this.parseBlock();this.expect(yn.RBRACE);if(this.accept(yn.IF)){this.operators[e]=this.operators.length;this.operators[e+1]="jz"}else{if(!this.accept(yn.LBRACE))throw new FormatError("PS Function: error parsing conditional.");{const t=this.operators.length;this.operators.push(null,null);const a=this.operators.length;this.parseBlock();this.expect(yn.RBRACE);this.expect(yn.IFELSE);this.operators[t]=this.operators.length;this.operators[t+1]="j";this.operators[e]=a;this.operators[e+1]="jz"}}}}const yn={LBRACE:0,RBRACE:1,NUMBER:2,OPERATOR:3,IF:4,IFELSE:5};class PostScriptToken{static get opCache(){return shadow(this,"opCache",Object.create(null))}constructor(e,t){this.type=e;this.value=t}static getOperator(e){return PostScriptToken.opCache[e]||=new PostScriptToken(yn.OPERATOR,e)}static get LBRACE(){return shadow(this,"LBRACE",new PostScriptToken(yn.LBRACE,"{"))}static get RBRACE(){return shadow(this,"RBRACE",new PostScriptToken(yn.RBRACE,"}"))}static get IF(){return shadow(this,"IF",new PostScriptToken(yn.IF,"IF"))}static get IFELSE(){return shadow(this,"IFELSE",new PostScriptToken(yn.IFELSE,"IFELSE"))}}class PostScriptLexer{constructor(e){this.stream=e;this.nextChar();this.strBuf=[]}nextChar(){return this.currentChar=this.stream.getByte()}getToken(){let e=!1,t=this.currentChar;for(;;){if(t<0)return wa;if(e)10!==t&&13!==t||(e=!1);else if(37===t)e=!0;else if(!isWhiteSpace(t))break;t=this.nextChar()}switch(0|t){case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:case 43:case 45:case 46:return new PostScriptToken(yn.NUMBER,this.getNumber());case 123:this.nextChar();return PostScriptToken.LBRACE;case 125:this.nextChar();return PostScriptToken.RBRACE}const a=this.strBuf;a.length=0;a[0]=String.fromCharCode(t);for(;(t=this.nextChar())>=0&&(t>=65&&t<=90||t>=97&&t<=122);)a.push(String.fromCharCode(t));const r=a.join("");switch(r.toLowerCase()){case"if":return PostScriptToken.IF;case"ifelse":return PostScriptToken.IFELSE;default:return PostScriptToken.getOperator(r)}}getNumber(){let e=this.currentChar;const t=this.strBuf;t.length=0;t[0]=String.fromCharCode(e);for(;(e=this.nextChar())>=0&&(e>=48&&e<=57||45===e||46===e);)t.push(String.fromCharCode(e));const a=parseFloat(t.join(""));if(isNaN(a))throw new FormatError(`Invalid floating point number: ${a}`);return a}}class BaseLocalCache{constructor(e){this._onlyRefs=!0===e?.onlyRefs;if(!this._onlyRefs){this._nameRefMap=new Map;this._imageMap=new Map}this._imageCache=new RefSetCache}getByName(e){this._onlyRefs&&unreachable("Should not call `getByName` method.");const t=this._nameRefMap.get(e);return t?this.getByRef(t):this._imageMap.get(e)||null}getByRef(e){return this._imageCache.get(e)||null}set(e,t,a){unreachable("Abstract method `set` called.")}}class LocalImageCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalImageCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalColorSpaceCache extends BaseLocalCache{set(e=null,t=null,a){if("string"!=typeof e&&!t)throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');if(t){if(this._imageCache.has(t))return;null!==e&&this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalFunctionCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalFunctionCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class LocalGStateCache extends BaseLocalCache{set(e,t=null,a){if("string"!=typeof e)throw new Error('LocalGStateCache.set - expected "name" argument.');if(t){if(this._imageCache.has(t))return;this._nameRefMap.set(e,t);this._imageCache.put(t,a)}else this._imageMap.has(e)||this._imageMap.set(e,a)}}class LocalTilingPatternCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('LocalTilingPatternCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class RegionalImageCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('RegionalImageCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}}class GlobalColorSpaceCache extends BaseLocalCache{constructor(e){super({onlyRefs:!0})}set(e=null,t,a){if(!t)throw new Error('GlobalColorSpaceCache.set - expected "ref" argument.');this._imageCache.has(t)||this._imageCache.put(t,a)}clear(){this._imageCache.clear()}}class GlobalImageCache{static NUM_PAGES_THRESHOLD=2;static MIN_IMAGES_TO_CACHE=10;static MAX_BYTE_SIZE=5e7;#H=new RefSet;constructor(){this._refCache=new RefSetCache;this._imageCache=new RefSetCache}get#W(){let e=0;for(const t of this._imageCache)e+=t.byteSize;return e}get#z(){return!(this._imageCache.size+e)):null}class PDFFunction{static getSampleArray(e,t,a,r){let i,n,s=1;for(i=0,n=e.length;i>c)*h;l&=(1<0&&(d=n[u-1]);let f=a[1];u>1,c=r.length>>1,l=new PostScriptEvaluator(s),h=Object.create(null);let u=8192;const d=new Float32Array(c);return function constructPostScriptFn(e,t,a,r){let n,s,f="";const g=d;for(n=0;ne&&(s=e)}m[n]=s}if(u>0){u--;h[f]=m}a.set(m,r)}}}function isPDFFunction(e){let t;if(e instanceof Dict)t=e;else{if(!(e instanceof BaseStream))return!1;t=e.dict}return t.has("FunctionType")}class PostScriptStack{static MAX_STACK_SIZE=100;constructor(e){this.stack=e?Array.from(e):[]}push(e){if(this.stack.length>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");this.stack.push(e)}pop(){if(this.stack.length<=0)throw new Error("PostScript function stack underflow.");return this.stack.pop()}copy(e){if(this.stack.length+e>=PostScriptStack.MAX_STACK_SIZE)throw new Error("PostScript function stack overflow.");const t=this.stack;for(let a=t.length-e,r=e-1;r>=0;r--,a++)t.push(t[a])}index(e){this.push(this.stack[this.stack.length-e-1])}roll(e,t){const a=this.stack,r=a.length-e,i=a.length-1,n=r+(t-Math.floor(t/e)*e);for(let e=r,t=i;e0?t.push(s<>o);break;case"ceiling":s=t.pop();t.push(Math.ceil(s));break;case"copy":s=t.pop();t.copy(s);break;case"cos":s=t.pop();t.push(Math.cos(s%360/180*Math.PI));break;case"cvi":s=0|t.pop();t.push(s);break;case"cvr":break;case"div":o=t.pop();s=t.pop();t.push(s/o);break;case"dup":t.copy(1);break;case"eq":o=t.pop();s=t.pop();t.push(s===o);break;case"exch":t.roll(2,1);break;case"exp":o=t.pop();s=t.pop();t.push(s**o);break;case"false":t.push(!1);break;case"floor":s=t.pop();t.push(Math.floor(s));break;case"ge":o=t.pop();s=t.pop();t.push(s>=o);break;case"gt":o=t.pop();s=t.pop();t.push(s>o);break;case"idiv":o=t.pop();s=t.pop();t.push(s/o|0);break;case"index":s=t.pop();t.index(s);break;case"le":o=t.pop();s=t.pop();t.push(s<=o);break;case"ln":s=t.pop();t.push(Math.log(s));break;case"log":s=t.pop();t.push(Math.log10(s));break;case"lt":o=t.pop();s=t.pop();t.push(s=t?new AstLiteral(t):e.max<=t?e:new AstMin(e,t)}class PostScriptCompiler{compile(e,t,a){const r=[],i=[],n=t.length>>1,s=a.length>>1;let o,c,l,h,u,d,f,g,p=0;for(let e=0;et.min){o.unshift("Math.max(",n,", ");o.push(")")}if(s4){r=!0;t=0}else{r=!1;t=1}const c=[];for(n=0;n=0&&"ET"===An[e];--e)An[e]="EN";for(let e=n+1;e0&&(t=An[n-1]);let a=u;e+1g&&isOdd(g)&&(m=g)}for(g=p;g>=m;--g){let e=-1;for(n=0,s=c.length;n=0){reverseValues(xn,e,n);e=-1}}else e<0&&(e=n);e>=0&&reverseValues(xn,e,c.length)}for(n=0,s=xn.length;n"!==e||(xn[n]="")}return createBidiText(xn.join(""),r)}const kn={style:"normal",weight:"normal"},Cn={style:"normal",weight:"bold"},vn={style:"italic",weight:"normal"},Fn={style:"italic",weight:"bold"},In=new Map([["Times-Roman",{local:["Times New Roman","Times-Roman","Times","Liberation Serif","Nimbus Roman","Nimbus Roman L","Tinos","Thorndale","TeX Gyre Termes","FreeSerif","Linux Libertine O","Libertinus Serif","DejaVu Serif","Bitstream Vera Serif","Ubuntu"],style:kn,ultimate:"serif"}],["Times-Bold",{alias:"Times-Roman",style:Cn,ultimate:"serif"}],["Times-Italic",{alias:"Times-Roman",style:vn,ultimate:"serif"}],["Times-BoldItalic",{alias:"Times-Roman",style:Fn,ultimate:"serif"}],["Helvetica",{local:["Helvetica","Helvetica Neue","Arial","Arial Nova","Liberation Sans","Arimo","Nimbus Sans","Nimbus Sans L","A030","TeX Gyre Heros","FreeSans","DejaVu Sans","Albany","Bitstream Vera Sans","Arial Unicode MS","Microsoft Sans Serif","Apple Symbols","Cantarell"],path:"LiberationSans-Regular.ttf",style:kn,ultimate:"sans-serif"}],["Helvetica-Bold",{alias:"Helvetica",path:"LiberationSans-Bold.ttf",style:Cn,ultimate:"sans-serif"}],["Helvetica-Oblique",{alias:"Helvetica",path:"LiberationSans-Italic.ttf",style:vn,ultimate:"sans-serif"}],["Helvetica-BoldOblique",{alias:"Helvetica",path:"LiberationSans-BoldItalic.ttf",style:Fn,ultimate:"sans-serif"}],["Courier",{local:["Courier","Courier New","Liberation Mono","Nimbus Mono","Nimbus Mono L","Cousine","Cumberland","TeX Gyre Cursor","FreeMono","Linux Libertine Mono O","Libertinus Mono"],style:kn,ultimate:"monospace"}],["Courier-Bold",{alias:"Courier",style:Cn,ultimate:"monospace"}],["Courier-Oblique",{alias:"Courier",style:vn,ultimate:"monospace"}],["Courier-BoldOblique",{alias:"Courier",style:Fn,ultimate:"monospace"}],["ArialBlack",{local:["Arial Black"],style:{style:"normal",weight:"900"},fallback:"Helvetica-Bold"}],["ArialBlack-Bold",{alias:"ArialBlack"}],["ArialBlack-Italic",{alias:"ArialBlack",style:{style:"italic",weight:"900"},fallback:"Helvetica-BoldOblique"}],["ArialBlack-BoldItalic",{alias:"ArialBlack-Italic"}],["ArialNarrow",{local:["Arial Narrow","Liberation Sans Narrow","Helvetica Condensed","Nimbus Sans Narrow","TeX Gyre Heros Cn"],style:kn,fallback:"Helvetica"}],["ArialNarrow-Bold",{alias:"ArialNarrow",style:Cn,fallback:"Helvetica-Bold"}],["ArialNarrow-Italic",{alias:"ArialNarrow",style:vn,fallback:"Helvetica-Oblique"}],["ArialNarrow-BoldItalic",{alias:"ArialNarrow",style:Fn,fallback:"Helvetica-BoldOblique"}],["Calibri",{local:["Calibri","Carlito"],style:kn,fallback:"Helvetica"}],["Calibri-Bold",{alias:"Calibri",style:Cn,fallback:"Helvetica-Bold"}],["Calibri-Italic",{alias:"Calibri",style:vn,fallback:"Helvetica-Oblique"}],["Calibri-BoldItalic",{alias:"Calibri",style:Fn,fallback:"Helvetica-BoldOblique"}],["Wingdings",{local:["Wingdings","URW Dingbats"],style:kn}],["Wingdings-Regular",{alias:"Wingdings"}],["Wingdings-Bold",{alias:"Wingdings"}]]),On=new Map([["Arial-Black","ArialBlack"]]);function getFamilyName(e){const t=new Set(["thin","extralight","ultralight","demilight","semilight","light","book","regular","normal","medium","demibold","semibold","bold","extrabold","ultrabold","black","heavy","extrablack","ultrablack","roman","italic","oblique","ultracondensed","extracondensed","condensed","semicondensed","normal","semiexpanded","expanded","extraexpanded","ultraexpanded","bolditalic"]);return e.split(/[- ,+]+/g).filter((e=>!t.has(e.toLowerCase()))).join(" ")}function generateFont({alias:e,local:t,path:a,fallback:r,style:i,ultimate:n},s,o,c=!0,l=!0,h=""){const u={style:null,ultimate:null};if(t){const e=h?` ${h}`:"";for(const a of t)s.push(`local(${a}${e})`)}if(e){const t=In.get(e),n=h||function getStyleToAppend(e){switch(e){case Cn:return"Bold";case vn:return"Italic";case Fn:return"Bold Italic";default:if("bold"===e?.weight)return"Bold";if("italic"===e?.style)return"Italic"}return""}(i);Object.assign(u,generateFont(t,s,o,c&&!r,l&&!a,n))}i&&(u.style=i);n&&(u.ultimate=n);if(c&&r){const e=In.get(r),{ultimate:t}=generateFont(e,s,o,c,l&&!a,h);u.ultimate||=t}l&&a&&o&&s.push(`url(${o}${a})`);return u}function getFontSubstitution(e,t,a,r,i,n){if(r.startsWith("InvalidPDFjsFont_"))return null;"TrueType"!==n&&"Type1"!==n||!/^[A-Z]{6}\+/.test(r)||(r=r.slice(7));const s=r=normalizeFontName(r);let o=e.get(s);if(o)return o;let c=In.get(r);if(!c)for(const[e,t]of On)if(r.startsWith(e)){r=`${t}${r.substring(e.length)}`;c=In.get(r);break}let l=!1;if(!c){c=In.get(i);l=!0}const h=`${t.getDocId()}_s${t.createFontId()}`;if(!c){if(!validateFontName(r)){warn(`Cannot substitute the font because of its name: ${r}`);e.set(s,null);return null}const t=/bold/gi.test(r),a=/oblique|italic/gi.test(r),i=t&&a&&Fn||t&&Cn||a&&vn||kn;o={css:`"${getFamilyName(r)}",${h}`,guessFallback:!0,loadedName:h,baseFontName:r,src:`local(${r})`,style:i};e.set(s,o);return o}const u=[];l&&validateFontName(r)&&u.push(`local(${r})`);const{style:d,ultimate:f}=generateFont(c,u,a),g=null===f,p=g?"":`,${f}`;o={css:`"${getFamilyName(r)}",${h}${p}`,guessFallback:g,loadedName:h,baseFontName:r,src:u.join(","),style:d};e.set(s,o);return o}const Tn=3285377520,Mn=4294901760,Dn=65535;class MurmurHash3_64{constructor(e){this.h1=e?4294967295&e:Tn;this.h2=e?4294967295&e:Tn}update(e){let t,a;if("string"==typeof e){t=new Uint8Array(2*e.length);a=0;for(let r=0,i=e.length;r>>8;t[a++]=255&i}}}else{if(!ArrayBuffer.isView(e))throw new Error("Invalid data format, must be a string or TypedArray.");t=e.slice();a=t.byteLength}const r=a>>2,i=a-4*r,n=new Uint32Array(t.buffer,0,r);let s=0,o=0,c=this.h1,l=this.h2;const h=3432918353,u=461845907,d=11601,f=13715;for(let e=0;e>>17;s=s*u&Mn|s*f&Dn;c^=s;c=c<<13|c>>>19;c=5*c+3864292196}else{o=n[e];o=o*h&Mn|o*d&Dn;o=o<<15|o>>>17;o=o*u&Mn|o*f&Dn;l^=o;l=l<<13|l>>>19;l=5*l+3864292196}s=0;switch(i){case 3:s^=t[4*r+2]<<16;case 2:s^=t[4*r+1]<<8;case 1:s^=t[4*r];s=s*h&Mn|s*d&Dn;s=s<<15|s>>>17;s=s*u&Mn|s*f&Dn;1&r?c^=s:l^=s}this.h1=c;this.h2=l}hexdigest(){let e=this.h1,t=this.h2;e^=t>>>1;e=3981806797*e&Mn|36045*e&Dn;t=4283543511*t&Mn|(2950163797*(t<<16|e>>>16)&Mn)>>>16;e^=t>>>1;e=444984403*e&Mn|60499*e&Dn;t=3301882366*t&Mn|(3120437893*(t<<16|e>>>16)&Mn)>>>16;e^=t>>>1;return(e>>>0).toString(16).padStart(8,"0")+(t>>>0).toString(16).padStart(8,"0")}}function resizeImageMask(e,t,a,r,i,n){const s=i*n;let o;o=t<=8?new Uint8Array(s):t<=16?new Uint16Array(s):new Uint32Array(s);const c=a/i,l=r/n;let h,u,d,f,g=0;const p=new Uint16Array(i),m=a;for(h=0;h0&&Number.isInteger(a.height)&&a.height>0&&(a.width!==f||a.height!==g)){warn("PDFImage - using the Width/Height of the image data, rather than the image dictionary.");f=a.width;g=a.height}else{const e="number"==typeof f&&f>0,t="number"==typeof g&&g>0;if(!e||!t){if(!a.fallbackDims)throw new FormatError(`Invalid image width: ${f} or height: ${g}`);warn("PDFImage - using the Width/Height of the parent image, for SMask/Mask data.");e||(f=a.fallbackDims.width);t||(g=a.fallbackDims.height)}}this.width=f;this.height=g;this.interpolate=h.get("I","Interpolate");this.imageMask=h.get("IM","ImageMask")||!1;this.matte=h.get("Matte")||!1;let p=a.bitsPerComponent;if(!p){p=h.get("BPC","BitsPerComponent");if(!p){if(!this.imageMask)throw new FormatError(`Bits per component missing in image: ${this.imageMask}`);p=1}}this.bpc=p;if(!this.imageMask){let i=h.getRaw("CS")||h.getRaw("ColorSpace");const n=!!i;if(n)this.jpxDecoderOptions?.smaskInData&&(i=Name.get("DeviceRGBA"));else if(this.jpxDecoderOptions)i=Name.get("DeviceRGBA");else switch(a.numComps){case 1:i=Name.get("DeviceGray");break;case 3:i=Name.get("DeviceRGB");break;case 4:i=Name.get("DeviceCMYK");break;default:throw new Error(`Images with ${a.numComps} color components not supported.`)}this.colorSpace=ColorSpaceUtils.parse({cs:i,xref:e,resources:r?t:null,pdfFunctionFactory:o,globalColorSpaceCache:c,localColorSpaceCache:l});this.numComps=this.colorSpace.numComps;if(this.jpxDecoderOptions){this.jpxDecoderOptions.numComponents=n?this.numComps:0;this.jpxDecoderOptions.isIndexedColormap="Indexed"===this.colorSpace.name}}this.decode=h.getArray("D","Decode");this.needsDecode=!1;if(this.decode&&(this.colorSpace&&!this.colorSpace.isDefaultDecode(this.decode,p)||s&&!ColorSpace.isDefaultDecode(this.decode,1))){this.needsDecode=!0;const e=(1<0,c=(r+7>>3)*i,l=e.getBytes(c),h=1===r&&1===i&&o===(0===l.length||!!(128&l[0]));if(h)return{isSingleOpaquePixel:h};if(t){if(ImageResizer.needsToBeResized(r,i)){const e=new Uint8ClampedArray(r*i*4);convertBlackAndWhiteToRGBA({src:l,dest:e,width:r,height:i,nonBlackColor:0,inverseDecode:o});return ImageResizer.createImage({kind:v,data:e,width:r,height:i,interpolate:n})}const e=new OffscreenCanvas(r,i),t=e.getContext("2d"),a=t.createImageData(r,i);convertBlackAndWhiteToRGBA({src:l,dest:a.data,width:r,height:i,nonBlackColor:0,inverseDecode:o});t.putImageData(a,0,0);return{data:null,width:r,height:i,interpolate:n,bitmap:e.transferToImageBitmap()}}const u=l.byteLength;let d;if(e instanceof DecodeStream&&(!o||c===u))d=l;else if(o){d=new Uint8Array(c);d.set(l);d.fill(255,u)}else d=new Uint8Array(l);if(o)for(let e=0;e>7&1;s[d+1]=u>>6&1;s[d+2]=u>>5&1;s[d+3]=u>>4&1;s[d+4]=u>>3&1;s[d+5]=u>>2&1;s[d+6]=u>>1&1;s[d+7]=1&u;d+=8}if(d>=1}}}}else{let a=0;u=0;for(d=0,h=n;d>r;i<0?i=0:i>l&&(i=l);s[d]=i;u&=(1<s[r+1]){t=255;break}}o[h]=t}}}if(o)for(h=0,d=3,u=t*r;h>3,h=t&&ImageResizer.needsToBeResized(a,r);if(!this.smask&&!this.mask&&"DeviceRGBA"===this.colorSpace.name){i.kind=v;const e=i.data=await this.getImageBytes(o*s*4,{});return t?h?ImageResizer.createImage(i,!1):this.createBitmap(v,a,r,e):i}if(!e){let e;"DeviceGray"===this.colorSpace.name&&1===c?e=k:"DeviceRGB"!==this.colorSpace.name||8!==c||this.needsDecode||(e=C);if(e&&!this.smask&&!this.mask&&a===s&&r===o){const n=await this.#$(s,o);if(n)return n;const c=await this.getImageBytes(o*l,{});if(t)return h?ImageResizer.createImage({data:c,kind:e,width:a,height:r,interpolate:this.interpolate},this.needsDecode):this.createBitmap(e,s,o,c);i.kind=e;i.data=c;if(this.needsDecode){assert(e===k,"PDFImage.createImageData: The image must be grayscale.");const t=i.data;for(let e=0,a=t.length;e>3,s=await this.getImageBytes(r*n,{internal:!0}),o=this.getComponents(s);let c,l;if(1===i){l=a*r;if(this.needsDecode)for(c=0;c0&&r[0].count++}class TimeSlotManager{static TIME_SLOT_DURATION_MS=20;static CHECK_TIME_EVERY=100;constructor(){this.reset()}check(){if(++this.checkedo){const e="Image exceeded maximum allowed size and was removed.";if(!c)throw new Error(e);warn(e);return}let g;h.has("OC")&&(g=await this.parseMarkedContentProps(h.get("OC"),e));let p,m,b;if(h.get("IM","ImageMask")||!1){p=await PDFImage.createMask({image:t,isOffscreenCanvasSupported:l&&!this.parsingType3Font});if(p.isSingleOpaquePixel){m=ta;b=[];r.addImageOps(m,b,g);if(i){const e={fn:m,args:b,optionalContent:g};n.set(i,u,e);u&&this._regionalImageCache.set(null,u,e)}return}if(this.parsingType3Font){b=function compileType3Glyph({data:e,width:t,height:a}){if(t>1e3||a>1e3)return null;const r=new Uint8Array([0,2,4,0,1,0,5,4,8,10,0,8,0,2,1,0]),i=t+1,n=new Uint8Array(i*(a+1));let s,o,c;const l=t+7&-8,h=new Uint8Array(l*a);let u=0;for(const t of e){let e=128;for(;e>0;){h[u++]=t&e?0:255;e>>=1}}let d=0;u=0;if(0!==h[u]){n[0]=1;++d}for(o=1;o>2)+(h[u+1]?4:0)+(h[u-l+1]?8:0);if(r[e]){n[c+o]=r[e];++d}u++}if(h[u-l]!==h[u]){n[c+o]=h[u]?2:4;++d}if(d>1e3)return null}u=l*(a-1);c=s*i;if(0!==h[u]){n[c]=8;++d}for(o=1;o1e3)return null;const f=new Int32Array([0,i,-1,0,-i,0,0,0,1]),g=[],{a:p,b:m,c:b,d:y,e:w,f:S}=(new DOMMatrix).scaleSelf(1/t,-1/a).translateSelf(0,-a);for(s=0;d&&s<=a;s++){let e=s*i;const a=e+t;for(;e>4;n[e]&=l>>2|l<<2}r=e%i;o=e/i|0;g.push(oa,p*r+b*o+w,m*r+y*o+S);n[e]||--d}while(c!==e);--s}return[na,[new Float32Array(g)],new Float32Array([0,0,t,a])]}(p);if(b){r.addImageOps(aa,b,g);return}warn("Cannot compile Type3 glyph.");r.addImageOps(Vt,[p],g);return}const e=`mask_${this.idFactory.createObjId()}`;r.addDependency(e);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;this._sendImgData(e,p);m=Vt;b=[{data:e,width:p.width,height:p.height,interpolate:p.interpolate,count:1}];r.addImageOps(m,b,g);if(i){const t={objId:e,fn:m,args:b,optionalContent:g};n.set(i,u,t);u&&this._regionalImageCache.set(null,u,t)}return}const y=h.has("SMask")||h.has("Mask");if(a&&d+f<200&&!y){try{const i=new PDFImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s});p=await i.createImageData(!0,!1);r.addImageOps(Yt,[p],g)}catch(e){const t=`Unable to decode inline image: "${e}".`;if(!c)throw new Error(t);warn(t)}return}let w=`img_${this.idFactory.createObjId()}`,S=!1,x=null;if(this.parsingType3Font)w=`${this.idFactory.getDocId()}_type3_${w}`;else if(i&&u){S=this.globalImageCache.shouldCache(u,this.pageIndex);if(S){assert(!a,"Cannot cache an inline image globally.");w=`${this.idFactory.getDocId()}_${w}`}}r.addDependency(w);m=Jt;b=[w,d,f];r.addImageOps(m,b,g,y);if(S){x={objId:w,fn:m,args:b,optionalContent:g,hasMask:y,byteSize:0};if(this.globalImageCache.hasDecodeFailed(u)){this.globalImageCache.setData(u,x);this._sendImgData(w,null,S);return}if(d*f>25e4||y){const e=await this.handler.sendWithPromise("commonobj",[w,"CopyLocalImage",{imageRef:u}]);if(e){this.globalImageCache.setData(u,x);this.globalImageCache.addByteSize(u,e);return}}}PDFImage.buildImage({xref:this.xref,res:e,image:t,isInline:a,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:s}).then((async e=>{p=await e.createImageData(!1,l);p.dataLen=p.bitmap?p.width*p.height*4:p.data.length;p.ref=u;S&&this.globalImageCache.addByteSize(u,p.dataLen);return this._sendImgData(w,p,S)})).catch((e=>{warn(`Unable to decode image "${w}": "${e}".`);u&&this.globalImageCache.addDecodeFailed(u);return this._sendImgData(w,null,S)}));if(i){const e={objId:w,fn:m,args:b,optionalContent:g,hasMask:y};n.set(i,u,e);if(u){this._regionalImageCache.set(null,u,e);if(S){assert(x,"The global cache-data must be available.");this.globalImageCache.setData(u,x)}}}}handleSMask(e,t,a,r,i,n,s){const o=e.get("G"),c={subtype:e.get("S").name,backdrop:e.get("BC")},l=e.get("TR");if(isPDFFunction(l)){const e=this._pdfFunctionFactory.create(l),t=new Uint8Array(256),a=new Float32Array(1);for(let r=0;r<256;r++){a[0]=r/255;e(a,0,a,0);t[r]=255*a[0]|0}c.transferMap=t}return this.buildFormXObject(t,o,c,a,r,i.state.clone({newPath:!0}),n,s)}handleTransferFunction(e){let t;if(Array.isArray(e))t=e;else{if(!isPDFFunction(e))return null;t=[e]}const a=[];let r=0,i=0;for(const e of t){const t=this.xref.fetchIfRef(e);r++;if(isName(t,"Identity")){a.push(null);continue}if(!isPDFFunction(t))return null;const n=this._pdfFunctionFactory.create(t),s=new Uint8Array(256),o=new Float32Array(1);for(let e=0;e<256;e++){o[0]=e/255;n(o,0,o,0);s[e]=255*o[0]|0}a.push(s);i++}return 1!==r&&4!==r||0===i?null:a}handleTilingType(e,t,a,r,i,n,s,o){const c=new OperatorList,l=Dict.merge({xref:this.xref,dictArray:[i.get("Resources"),a]});return this.getOperatorList({stream:r,task:s,resources:l,operatorList:c}).then((function(){const a=c.getIR(),r=getTilingPatternIR(a,i,t);n.addDependencies(c.dependencies);n.addOp(e,r);i.objId&&o.set(null,i.objId,{operatorListIR:a,dict:i})})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`handleTilingType - ignoring pattern: "${e}".`)}}))}async handleSetFont(e,t,a,r,i,n,s=null,o=null){const c=t?.[0]instanceof Name?t[0].name:null,l=await this.loadFont(c,a,e,i,s,o);l.font.isType3Font&&r.addDependencies(l.type3Dependencies);n.font=l.font;l.send(this.handler);return l.loadedName}handleText(e,t){const a=t.font,r=a.charsToGlyphs(e);if(a.data){(!!(t.textRenderingMode&x)||"Pattern"===t.fillColorSpace.name||a.disableFontFace)&&PartialEvaluator.buildFontPaths(a,r,this.handler,this.options)}return r}ensureStateFont(e){if(e.font)return;const t=new FormatError("Missing setFont (Tf) operator before text rendering operator.");if(!this.options.ignoreErrors)throw t;warn(`ensureStateFont: "${t}".`)}async setGState({resources:e,gState:t,operatorList:a,cacheKey:r,task:i,stateManager:n,localGStateCache:s,localColorSpaceCache:o,seenRefs:c}){const l=t.objId;let h=!0;const u=[];let d=Promise.resolve();for(const[r,s]of t)switch(r){case"Type":break;case"LW":if("number"!=typeof s){warn(`Invalid LW (line width): ${s}`);break}u.push([r,Math.abs(s)]);break;case"LC":case"LJ":case"ML":case"D":case"RI":case"FL":case"CA":case"ca":u.push([r,s]);break;case"Font":h=!1;d=d.then((()=>this.handleSetFont(e,null,s[0],a,i,n.state).then((function(e){a.addDependency(e);u.push([r,[e,s[1]]])}))));break;case"BM":u.push([r,normalizeBlendMode(s)]);break;case"SMask":if(isName(s,"None")){u.push([r,!1]);break}if(s instanceof Dict){h=!1;d=d.then((()=>this.handleSMask(s,e,a,i,n,o,c)));u.push([r,!0])}else warn("Unsupported SMask type");break;case"TR":const t=this.handleTransferFunction(s);u.push([r,t]);break;case"OP":case"op":case"OPM":case"BG":case"BG2":case"UCR":case"UCR2":case"TR2":case"HT":case"SM":case"SA":case"AIS":case"TK":info("graphic state operator "+r);break;default:info("Unknown graphic state operator "+r)}await d;u.length>0&&a.addOp(De,[u]);h&&s.set(r,l,u)}loadFont(e,t,a,r,i=null,n=null){const errorFont=async()=>new TranslatedFont({loadedName:"g_font_error",font:new ErrorFont(`Font "${e}" is not available.`),dict:t});let s;if(t)t instanceof Ref&&(s=t);else{const t=a.get("Font");t&&(s=t.getRaw(e))}if(s){if(this.type3FontRefs?.has(s))return errorFont();if(this.fontCache.has(s))return this.fontCache.get(s);try{t=this.xref.fetchIfRef(s)}catch(e){warn(`loadFont - lookup failed: "${e}".`)}}if(!(t instanceof Dict)){if(!this.options.ignoreErrors&&!this.parsingType3Font){warn(`Font "${e}" is not available.`);return errorFont()}warn(`Font "${e}" is not available -- attempting to fallback to a default font.`);t=i||PartialEvaluator.fallbackFontDict}if(t.cacheKey&&this.fontCache.has(t.cacheKey))return this.fontCache.get(t.cacheKey);const{promise:o,resolve:c}=Promise.withResolvers();let l;try{l=this.preEvaluateFont(t);l.cssFontInfo=n}catch(e){warn(`loadFont - preEvaluateFont failed: "${e}".`);return errorFont()}const{descriptor:h,hash:u}=l,d=s instanceof Ref;let f;if(u&&h instanceof Dict){const e=h.fontAliases||=Object.create(null);if(e[u]){const t=e[u].aliasRef;if(d&&t&&this.fontCache.has(t)){this.fontCache.putAlias(s,t);return this.fontCache.get(s)}}else e[u]={fontID:this.idFactory.createFontId()};d&&(e[u].aliasRef=s);f=e[u].fontID}else f=this.idFactory.createFontId();assert(f?.startsWith("f"),'The "fontID" must be (correctly) defined.');if(d)this.fontCache.put(s,o);else{t.cacheKey=`cacheKey_${f}`;this.fontCache.put(t.cacheKey,o)}t.loadedName=`${this.idFactory.getDocId()}_${f}`;this.translateFont(l).then((async e=>{const i=new TranslatedFont({loadedName:t.loadedName,font:e,dict:t});if(e.isType3Font)try{await i.loadType3Data(this,a,r)}catch(e){throw new Error(`Type3 font load error: ${e}`)}c(i)})).catch((e=>{warn(`loadFont - translateFont failed: "${e}".`);c(new TranslatedFont({loadedName:t.loadedName,font:new ErrorFont(e?.message),dict:t}))}));return o}buildPath(e,t,a){const{pathMinMax:r,pathBuffer:i}=a;switch(0|e){case Xe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1],s=t[2],o=t[3],c=e+s,l=n+o;0===s||0===o?i.push(sa,e,n,oa,c,l,la):i.push(sa,e,n,oa,c,n,oa,c,l,oa,e,l,la);Util.rectBoundingBox(e,n,c,l,r);break}case Ee:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(sa,e,n);Util.pointBoundingBox(e,n,r);break}case Pe:{const e=a.currentPointX=t[0],n=a.currentPointY=t[1];i.push(oa,e,n);Util.pointBoundingBox(e,n,r);break}case Le:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l,h,u]=t;a.currentPointX=h;a.currentPointY=u;i.push(ca,s,o,c,l,h,u);Util.bezierBoundingBox(e,n,s,o,c,l,h,u,r);break}case je:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,e,n,s,o,c,l);Util.bezierBoundingBox(e,n,e,n,s,o,c,l,r);break}case _e:{const e=a.currentPointX,n=a.currentPointY,[s,o,c,l]=t;a.currentPointX=c;a.currentPointY=l;i.push(ca,s,o,c,l,c,l);Util.bezierBoundingBox(e,n,s,o,c,l,c,l,r);break}case Ue:i.push(la)}}_getColorSpace(e,t,a){return ColorSpaceUtils.parse({cs:e,xref:this.xref,resources:t,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:a,asyncIfNotCached:!0})}async _handleColorSpace(e){try{return await e}catch(e){if(e instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`_handleColorSpace - ignoring ColorSpace: "${e}".`);return null}throw e}}parseShading({shading:e,resources:t,localColorSpaceCache:a,localShadingPatternCache:r}){let i,n=r.get(e);if(n)return n;try{i=Pattern.parseShading(e,this.xref,t,this._pdfFunctionFactory,this.globalColorSpaceCache,a).getIR()}catch(t){if(t instanceof AbortException)return null;if(this.options.ignoreErrors){warn(`parseShading - ignoring shading: "${t}".`);r.set(e,null);return null}throw t}n=`pattern_${this.idFactory.createObjId()}`;this.parsingType3Font&&(n=`${this.idFactory.getDocId()}_type3_${n}`);r.set(e,n);this.parsingType3Font?this.handler.send("commonobj",[n,"Pattern",i]):this.handler.send("obj",[n,this.pageIndex,"Pattern",i]);return n}handleColorN(e,t,a,r,i,n,s,o,c,l){const h=a.pop();if(h instanceof Name){const u=i.getRaw(h.name),d=u instanceof Ref&&c.getByRef(u);if(d)try{const i=r.base?r.base.getRgbHex(a,0):null,n=getTilingPatternIR(d.operatorListIR,d.dict,i);e.addOp(t,n);return}catch{}const f=this.xref.fetchIfRef(u);if(f){const i=f instanceof BaseStream?f.dict:f,h=i.get("PatternType");if(h===Rn){const o=r.base?r.base.getRgbHex(a,0):null;return this.handleTilingType(t,o,n,f,i,e,s,c)}if(h===Nn){const a=i.get("Shading"),r=this.parseShading({shading:a,resources:n,localColorSpaceCache:o,localShadingPatternCache:l});if(r){const a=lookupMatrix(i.getArray("Matrix"),null);e.addOp(t,["Shading",r,a])}return}throw new FormatError(`Unknown PatternType: ${h}`)}}throw new FormatError(`Unknown PatternName: ${h}`)}_parseVisibilityExpression(e,t,a){if(++t>10){warn("Visibility expression is too deeply nested");return}const r=e.length,i=this.xref.fetchIfRef(e[0]);if(!(r<2)&&i instanceof Name){switch(i.name){case"And":case"Or":case"Not":a.push(i.name);break;default:warn(`Invalid operator ${i.name} in visibility expression`);return}for(let i=1;i0)return{type:"OCMD",expression:t}}const t=a.get("OCGs");if(Array.isArray(t)||t instanceof Dict){const e=[];if(Array.isArray(t))for(const a of t)e.push(a.toString());else e.push(t.objId);return{type:r,ids:e,policy:a.get("P")instanceof Name?a.get("P").name:null,expression:null}}if(t instanceof Ref)return{type:r,id:t.toString()}}return null}getOperatorList({stream:e,task:t,resources:a,operatorList:r,initialState:i=null,fallbackFontDict:n=null,prevRefs:s=null}){const o=e.dict?.objId,c=new RefSet(s);if(o){if(s?.has(o))throw new Error(`getOperatorList - ignoring circular reference: ${o}`);c.put(o)}a||=Dict.empty;i||=new EvalState;if(!r)throw new Error('getOperatorList: missing "operatorList" parameter');const l=this,h=this.xref,u=new LocalImageCache,d=new LocalColorSpaceCache,f=new LocalGStateCache,g=new LocalTilingPatternCache,p=new Map,m=a.get("XObject")||Dict.empty,b=a.get("Pattern")||Dict.empty,y=new StateManager(i),w=new EvaluatorPreprocessor(e,h,y),S=new TimeSlotManager;function closePendingRestoreOPS(e){for(let e=0,t=w.savedStatesDepth;e{y.state.fillColorSpace=e||ColorSpaceUtils.gray})));return}case yt:{const t=l._getColorSpace(e[0],a,d);if(t instanceof ColorSpace){y.state.strokeColorSpace=t;continue}next(l._handleColorSpace(t).then((e=>{y.state.strokeColorSpace=e||ColorSpaceUtils.gray})));return}case At:C=y.state.fillColorSpace;e=[C.getRgbHex(e,0)];i=It;break;case St:C=y.state.strokeColorSpace;e=[C.getRgbHex(e,0)];i=Ft;break;case vt:y.state.fillColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=It;break;case Ct:y.state.strokeColorSpace=ColorSpaceUtils.gray;e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=Ft;break;case Tt:y.state.fillColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];i=It;break;case Ot:y.state.strokeColorSpace=ColorSpaceUtils.cmyk;e=[ColorSpaceUtils.cmyk.getRgbHex(e,0)];i=Ft;break;case It:y.state.fillColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case Ft:y.state.strokeColorSpace=ColorSpaceUtils.rgb;e=[ColorSpaceUtils.rgb.getRgbHex(e,0)];break;case kt:C=y.state.patternFillColorSpace;if(!C){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=It;break}e=[];i=ia;break}if("Pattern"===C.name){next(l.handleColorN(r,kt,e,C,b,a,t,d,g,p));return}e=[C.getRgbHex(e,0)];i=It;break;case xt:C=y.state.patternStrokeColorSpace;if(!C){if(isNumberArray(e,null)){e=[ColorSpaceUtils.gray.getRgbHex(e,0)];i=Ft;break}e=[];i=ra;break}if("Pattern"===C.name){next(l.handleColorN(r,xt,e,C,b,a,t,d,g,p));return}e=[C.getRgbHex(e,0)];i=Ft;break;case Mt:let O;try{const t=a.get("Shading");if(!t)throw new FormatError("No shading resource found");O=t.get(e[0].name);if(!O)throw new FormatError("No shading object found")}catch(e){if(e instanceof AbortException)continue;if(l.options.ignoreErrors){warn(`getOperatorList - ignoring Shading: "${e}".`);continue}throw e}const T=l.parseShading({shading:O,resources:a,localColorSpaceCache:d,localShadingPatternCache:p});if(!T)continue;e=[T];i=Mt;break;case De:F=e[0]instanceof Name;v=e[0].name;if(F){const t=f.getByName(v);if(t){t.length>0&&r.addOp(De,[t]);e=null;continue}}next(new Promise((function(e,i){if(!F)throw new FormatError("GState must be referred to by name.");const n=a.get("ExtGState");if(!(n instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const s=n.get(v);if(!(s instanceof Dict))throw new FormatError("GState should be a dictionary.");l.setGState({resources:a,gState:s,operatorList:r,cacheKey:v,task:t,stateManager:y,localGStateCache:f,localColorSpaceCache:d,seenRefs:c}).then(e,i)})).catch((function(e){if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring ExtGState: "${e}".`)}})));return;case Ce:{const[t]=e;if("number"!=typeof t){warn(`Invalid setLineWidth: ${t}`);continue}e[0]=Math.abs(t);break}case Ee:case Pe:case Le:case je:case _e:case Ue:case Xe:l.buildPath(i,e,y.state);continue;case qe:case He:case We:case ze:case $e:case Ge:case Ve:case Ke:case Je:{const{state:{pathBuffer:e,pathMinMax:t}}=y;i!==He&&i!==Ve&&i!==Ke||e.push(la);if(0===e.length)r.addOp(aa,[i,[null],null]);else{r.addOp(aa,[i,[new Float32Array(e)],t.slice()]);e.length=0;t.set([1/0,1/0,-1/0,-1/0],0)}continue}case ht:r.addOp(i,[new Float32Array(e)]);continue;case Et:case Pt:case Ut:case Xt:continue;case jt:if(!(e[0]instanceof Name)){warn(`Expected name for beginMarkedContentProps arg0=${e[0]}`);r.addOp(jt,["OC",null]);continue}if("OC"===e[0].name){next(l.parseMarkedContentProps(e[1],a).then((e=>{r.addOp(jt,["OC",e])})).catch((e=>{if(!(e instanceof AbortException)){if(!l.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring beginMarkedContentProps: "${e}".`);r.addOp(jt,["OC",null])}})));return}e=[e[0].name,e[1]instanceof Dict?e[1].get("MCID"):null];break;default:if(null!==e){for(x=0,k=e.length;x{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getOperatorList - ignoring errors during "${t.name}" task: "${e}".`);closePendingRestoreOPS()}}))}getTextContent({stream:e,task:a,resources:r,stateManager:i=null,includeMarkedContent:n=!1,sink:s,seenStyles:o=new Set,viewBox:c,lang:l=null,markedContentData:h=null,disableNormalization:u=!1,keepWhiteSpace:d=!1,prevRefs:f=null}){const g=e.dict?.objId,p=new RefSet(f);if(g){if(f?.has(g))throw new Error(`getTextContent - ignoring circular reference: ${g}`);p.put(g)}r||=Dict.empty;i||=new StateManager(new TextState);n&&(h||={level:0});const m={items:[],styles:Object.create(null),lang:l},b={initialized:!1,str:[],totalWidth:0,totalHeight:0,width:0,height:0,vertical:!1,prevTransform:null,textAdvanceScale:0,spaceInFlowMin:0,spaceInFlowMax:0,trackingSpaceMin:1/0,negativeSpaceMax:-1/0,notASpace:-1/0,transform:null,fontName:null,hasEOL:!1},y=[" "," "];let w=0;function saveLastChar(e){const t=(w+1)%2,a=" "!==y[w]&&" "===y[t];y[w]=e;w=t;return!d&&a}function shouldAddWhitepsace(){return!d&&" "!==y[w]&&" "===y[(w+1)%2]}function resetLastChars(){y[0]=y[1]=" ";w=0}const S=this,x=this.xref,k=[];let C=null;const v=new LocalImageCache,F=new LocalGStateCache,O=new EvaluatorPreprocessor(e,x,i);let T;function pushWhitespace({width:e=0,height:t=0,transform:a=b.prevTransform,fontName:r=b.fontName}){m.items.push({str:" ",dir:"ltr",width:e,height:t,transform:a,fontName:r,hasEOL:!1})}function getCurrentTextTransform(){const e=T.font,a=[T.fontSize*T.textHScale,0,0,T.fontSize,0,T.textRise];if(e.isType3Font&&(T.fontSize<=1||e.isCharBBox)&&!isArrayEqual(T.fontMatrix,t)){const t=e.bbox[3]-e.bbox[1];t>0&&(a[3]*=t*T.fontMatrix[3])}return Util.transform(T.ctm,Util.transform(T.textMatrix,a))}function ensureTextContentItem(){if(b.initialized)return b;const{font:e,loadedName:t}=T;if(!o.has(t)){o.add(t);m.styles[t]={fontFamily:e.fallbackName,ascent:e.ascent,descent:e.descent,vertical:e.vertical};if(S.options.fontExtraProperties&&e.systemFontInfo){const a=m.styles[t];a.fontSubstitution=e.systemFontInfo.css;a.fontSubstitutionLoadedName=e.systemFontInfo.loadedName}}b.fontName=t;const a=b.transform=getCurrentTextTransform();if(e.vertical){b.width=b.totalWidth=Math.hypot(a[0],a[1]);b.height=b.totalHeight=0;b.vertical=!0}else{b.width=b.totalWidth=0;b.height=b.totalHeight=Math.hypot(a[2],a[3]);b.vertical=!1}const r=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),i=Math.hypot(T.ctm[0],T.ctm[1]);b.textAdvanceScale=i*r;const{fontSize:n}=T;b.trackingSpaceMin=.102*n;b.notASpace=.03*n;b.negativeSpaceMax=-.2*n;b.spaceInFlowMin=.102*n;b.spaceInFlowMax=.6*n;b.hasEOL=!1;b.initialized=!0;return b}function updateAdvanceScale(){if(!b.initialized)return;const e=Math.hypot(T.textLineMatrix[0],T.textLineMatrix[1]),t=Math.hypot(T.ctm[0],T.ctm[1])*e;if(t!==b.textAdvanceScale){if(b.vertical){b.totalHeight+=b.height*b.textAdvanceScale;b.height=0}else{b.totalWidth+=b.width*b.textAdvanceScale;b.width=0}b.textAdvanceScale=t}}function runBidiTransform(e){let t=e.str.join("");u||(t=function normalizeUnicode(e){if(!ma){ma=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;ba=new Map([["๏ฌ…","ลฟt"]])}return e.replaceAll(ma,((e,t,a)=>t?t.normalize("NFKC"):ba.get(a)))}(t));const a=bidi(t,-1,e.vertical);return{str:a.str,dir:a.dir,width:Math.abs(e.totalWidth),height:Math.abs(e.totalHeight),transform:e.transform,fontName:e.fontName,hasEOL:e.hasEOL}}async function handleSetFont(e,i){const n=await S.loadFont(e,i,r,a);T.loadedName=n.loadedName;T.font=n.font;T.fontMatrix=n.font.fontMatrix||t}function applyInverseRotation(e,t,a){const r=Math.hypot(a[0],a[1]);return[(a[0]*e+a[1]*t)/r,(a[2]*e+a[3]*t)/r]}function compareWithLastPosition(e){const t=getCurrentTextTransform();let a=t[4],r=t[5];if(T.font?.vertical){if(ac[2]||r+ec[3])return!1}else if(a+ec[2]||rc[3])return!1;if(!T.font||!b.prevTransform)return!0;let i=b.prevTransform[4],n=b.prevTransform[5];if(i===a&&n===r)return!0;let s=-1;t[0]&&0===t[1]&&0===t[2]?s=t[0]>0?0:180:t[1]&&0===t[0]&&0===t[3]&&(s=t[1]>0?90:270);switch(s){case 0:break;case 90:[a,r]=[r,a];[i,n]=[n,i];break;case 180:[a,r,i,n]=[-a,-r,-i,-n];break;case 270:[a,r]=[-r,-a];[i,n]=[-n,-i];break;default:[a,r]=applyInverseRotation(a,r,t);[i,n]=applyInverseRotation(i,n,b.prevTransform)}if(T.font.vertical){const e=(n-r)/b.textAdvanceScale,t=a-i,s=Math.sign(b.height);if(e.5*b.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>b.width){appendEOL();return!0}e<=s*b.notASpace&&resetLastChars();if(e<=s*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else b.height+=e;else if(!addFakeSpaces(e,b.prevTransform,s))if(0===b.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else b.height+=e;Math.abs(t)>.25*b.width&&flushTextContentItem();return!0}const o=(a-i)/b.textAdvanceScale,l=r-n,h=Math.sign(b.width);if(o.5*b.height){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(l)>b.height){appendEOL();return!0}o<=h*b.notASpace&&resetLastChars();if(o<=h*b.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({width:Math.abs(o)})}else b.width+=o;else if(!addFakeSpaces(o,b.prevTransform,h))if(0===b.str.length){resetLastChars();pushWhitespace({width:Math.abs(o)})}else b.width+=o;Math.abs(l)>.25*b.height&&flushTextContentItem();return!0}function buildTextContentItem({chars:e,extraSpacing:t}){const a=T.font;if(!e){const e=T.charSpacing+t;e&&(a.vertical?T.translateTextMatrix(0,-e):T.translateTextMatrix(e*T.textHScale,0));d&&compareWithLastPosition(0);return}const r=a.charsToGlyphs(e),i=T.fontMatrix[0]*T.fontSize;for(let e=0,n=r.length;e0){const e=k.join("");k.length=0;buildTextContentItem({chars:e,extraSpacing:0})}break;case dt:if(!i.state.font){S.ensureStateFont(i.state);continue}buildTextContentItem({chars:w[0],extraSpacing:0});break;case gt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.carriageReturn();buildTextContentItem({chars:w[0],extraSpacing:0});break;case pt:if(!i.state.font){S.ensureStateFont(i.state);continue}T.wordSpacing=w[0];T.charSpacing=w[1];T.carriageReturn();buildTextContentItem({chars:w[2],extraSpacing:0});break;case Nt:flushTextContentItem();C??=r.get("XObject")||Dict.empty;y=w[0]instanceof Name;b=w[0].name;if(y&&v.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("XObject must be referred to by name.");let f=C.getRaw(b);if(f instanceof Ref){if(v.getByRef(f)){e();return}if(S.globalImageCache.getData(f,S.pageIndex)){e();return}f=x.fetch(f)}if(!(f instanceof BaseStream))throw new FormatError("XObject should be a stream");const{dict:g}=f,m=g.get("Subtype");if(!(m instanceof Name))throw new FormatError("XObject should have a Name subtype");if("Form"!==m.name){v.set(b,g.objId,!0);e();return}const w=i.state.clone(),k=new StateManager(w),F=lookupMatrix(g.getArray("Matrix"),null);F&&k.transform(F);const O=g.get("Resources");enqueueChunk();const T={enqueueInvoked:!1,enqueue(e,t){this.enqueueInvoked=!0;s.enqueue(e,t)},get desiredSize(){return s.desiredSize},get ready(){return s.ready}};S.getTextContent({stream:f,task:a,resources:O instanceof Dict?O:r,stateManager:k,includeMarkedContent:n,sink:T,seenStyles:o,viewBox:c,lang:l,markedContentData:h,disableNormalization:u,keepWhiteSpace:d,prevRefs:p}).then((function(){T.enqueueInvoked||v.set(b,g.objId,!0);e()}),t)})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring XObject: "${e}".`)}})));return;case De:y=w[0]instanceof Name;b=w[0].name;if(y&&F.getByName(b))break;next(new Promise((function(e,t){if(!y)throw new FormatError("GState must be referred to by name.");const a=r.get("ExtGState");if(!(a instanceof Dict))throw new FormatError("ExtGState should be a dictionary.");const i=a.get(b);if(!(i instanceof Dict))throw new FormatError("GState should be a dictionary.");const n=i.get("Font");if(n){flushTextContentItem();T.fontName=null;T.fontSize=n[1];handleSetFont(null,n[0]).then(e,t)}else{F.set(b,i.objId,!0);e()}})).catch((function(e){if(!(e instanceof AbortException)){if(!S.options.ignoreErrors)throw e;warn(`getTextContent - ignoring ExtGState: "${e}".`)}})));return;case Lt:flushTextContentItem();if(n){h.level++;m.items.push({type:"beginMarkedContent",tag:w[0]instanceof Name?w[0].name:null})}break;case jt:flushTextContentItem();if(n){h.level++;let e=null;w[1]instanceof Dict&&(e=w[1].get("MCID"));m.items.push({type:"beginMarkedContentProps",id:Number.isInteger(e)?`${S.idFactory.getPageObjId()}_mc${e}`:null,tag:w[0]instanceof Name?w[0].name:null})}break;case _t:flushTextContentItem();if(n){if(0===h.level)break;h.level--;m.items.push({type:"endMarkedContent"})}break;case Re:!e||e.font===T.font&&e.fontSize===T.fontSize&&e.fontName===T.fontName||flushTextContentItem()}if(m.items.length>=s.desiredSize){g=!0;break}}if(g)next(En);else{flushTextContentItem();enqueueChunk();e()}})).catch((e=>{if(!(e instanceof AbortException)){if(!this.options.ignoreErrors)throw e;warn(`getTextContent - ignoring errors during "${a.name}" task: "${e}".`);flushTextContentItem();enqueueChunk()}}))}async extractDataStructures(e,t){const a=this.xref;let r;const i=this.readToUnicode(t.toUnicode);if(t.composite){const a=e.get("CIDSystemInfo");a instanceof Dict&&(t.cidSystemInfo={registry:stringToPDFString(a.get("Registry")),ordering:stringToPDFString(a.get("Ordering")),supplement:a.get("Supplement")});try{const t=e.get("CIDToGIDMap");t instanceof BaseStream&&(r=t.getBytes())}catch(e){if(!this.options.ignoreErrors)throw e;warn(`extractDataStructures - ignoring CIDToGIDMap data: "${e}".`)}}const n=[];let s,o=null;if(e.has("Encoding")){s=e.get("Encoding");if(s instanceof Dict){o=s.get("BaseEncoding");o=o instanceof Name?o.name:null;if(s.has("Differences")){const e=s.get("Differences");let t=0;for(const r of e){const e=a.fetchIfRef(r);if("number"==typeof e)t=e;else{if(!(e instanceof Name))throw new FormatError(`Invalid entry in 'Differences' array: ${e}`);n[t++]=e.name}}}}else if(s instanceof Name)o=s.name;else{const e="Encoding is not a Name nor a Dict";if(!this.options.ignoreErrors)throw new FormatError(e);warn(e)}"MacRomanEncoding"!==o&&"MacExpertEncoding"!==o&&"WinAnsiEncoding"!==o&&(o=null)}const c=!t.file||t.isInternalFont,l=ei()[t.name];o&&c&&l&&(o=null);if(o)t.defaultEncoding=getEncoding(o);else{const e=!!(t.flags&Pr),a=!!(t.flags&Lr);s=Ar;"TrueType"!==t.type||a||(s=kr);if(e||l){s=xr;c&&(/Symbol/i.test(t.name)?s=Cr:/Dingbats/i.test(t.name)?s=vr:/Wingdings/i.test(t.name)&&(s=kr))}t.defaultEncoding=s}t.differences=n;t.baseEncodingName=o;t.hasEncoding=!!o||n.length>0;t.dict=e;t.toUnicode=await i;const h=await this.buildToUnicode(t);t.toUnicode=h;r&&(t.cidToGidMap=this.readCidToGidMap(r,h));return t}_simpleFontToUnicode(e,t=!1){assert(!e.composite,"Must be a simple font.");const a=[],r=e.defaultEncoding.slice(),i=e.baseEncodingName,n=e.differences;for(const e in n){const t=n[e];".notdef"!==t&&(r[e]=t)}const s=Fr();for(const n in r){let o=r[n];if(""===o)continue;let c=s[o];if(void 0!==c){a[n]=String.fromCharCode(c);continue}let l=0;switch(o[0]){case"G":3===o.length&&(l=parseInt(o.substring(1),16));break;case"g":5===o.length&&(l=parseInt(o.substring(1),16));break;case"C":case"c":if(o.length>=3&&o.length<=4){const a=o.substring(1);if(t){l=parseInt(a,16);break}l=+a;if(Number.isNaN(l)&&Number.isInteger(parseInt(a,16)))return this._simpleFontToUnicode(e,!0)}break;case"u":c=getUnicodeForGlyph(o,s);-1!==c&&(l=c);break;default:switch(o){case"f_h":case"f_t":case"T_h":a[n]=o.replaceAll("_","");continue}}if(l>0&&l<=1114111&&Number.isInteger(l)){if(i&&l===+n){const e=getEncoding(i);if(e&&(o=e[n])){a[n]=String.fromCharCode(s[o]);continue}}a[n]=String.fromCodePoint(l)}}return a}async buildToUnicode(e){e.hasIncludedToUnicodeMap=e.toUnicode?.length>0;if(e.hasIncludedToUnicodeMap){!e.composite&&e.hasEncoding&&(e.fallbackToUnicode=this._simpleFontToUnicode(e));return e.toUnicode}if(!e.composite)return new ToUnicodeMap(this._simpleFontToUnicode(e));if(e.composite&&(e.cMap.builtInCMap&&!(e.cMap instanceof IdentityCMap)||"Adobe"===e.cidSystemInfo?.registry&&("GB1"===e.cidSystemInfo.ordering||"CNS1"===e.cidSystemInfo.ordering||"Japan1"===e.cidSystemInfo.ordering||"Korea1"===e.cidSystemInfo.ordering))){const{registry:t,ordering:a}=e.cidSystemInfo,r=Name.get(`${t}-${a}-UCS2`),i=await CMapFactory.create({encoding:r,fetchBuiltInCMap:this._fetchBuiltInCMapBound,useCMap:null}),n=[],s=[];e.cMap.forEach((function(e,t){if(t>65535)throw new FormatError("Max size of CID is 65,535");const a=i.lookup(t);if(a){s.length=0;for(let e=0,t=a.length;e>1;(0!==i||t.has(n))&&(a[n]=i)}return a}extractWidths(e,t,a){const r=this.xref;let i=[],n=0;const s=[];let o;if(a.composite){const t=e.get("DW");n="number"==typeof t?Math.ceil(t):1e3;const c=e.get("W");if(Array.isArray(c))for(let e=0,t=c.length;e{const t=c.get(e),r=new OperatorList;return n.getOperatorList({stream:t,task:a,resources:l,operatorList:r}).then((()=>{switch(r.fnArray[0]){case bt:this.#K(r,b);break;case mt:b||this.#J(r)}h[e]=r.getIR();for(const e of r.dependencies)i.add(e)})).catch((function(t){warn(`Type3 font resource "${e}" is not available.`);const a=new OperatorList;h[e]=a.getIR()}))}));this.#V=o.then((()=>{r.charProcOperatorList=h;if(this._bbox){r.isCharBBox=!0;r.bbox=this._bbox}}));return this.#V}#K(e,t=NaN){const a=Util.normalizeRect(e.argsArray[0].slice(2)),r=a[2]-a[0],i=a[3]-a[1],n=Math.hypot(r,i);if(0===r||0===i){e.fnArray.splice(0,1);e.argsArray.splice(0,1)}else if(0===t||Math.round(n/t)>=10){this._bbox??=[1/0,1/0,-1/0,-1/0];Util.rectBoundingBox(...a,this._bbox)}let s=0,o=e.length;for(;s=Ee&&n<=Je;if(i.variableArgs)o>s&&info(`Command ${r}: expected [0, ${s}] args, but received ${o} args.`);else{if(o!==s){const e=this.nonProcessedArgs;for(;o>s;){e.push(t.shift());o--}for(;oEvaluatorPreprocessor.MAX_INVALID_PATH_OPS)throw new FormatError(`Invalid ${e}`);warn(`Skipping ${e}`);null!==t&&(t.length=0);continue}}this.preprocessCommand(n,t);e.fn=n;e.args=t;return!0}if(a===wa)return!1;if(null!==a){null===t&&(t=[]);t.push(a);if(t.length>33)throw new FormatError("Too many arguments")}}}preprocessCommand(e,t){switch(0|e){case Be:this.stateManager.save();break;case Re:this.stateManager.restore();break;case Ne:this.stateManager.transform(t)}}}class DefaultAppearanceEvaluator extends EvaluatorPreprocessor{constructor(e){super(new StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:a,args:r}=e;switch(0|a){case nt:const[e,a]=r;e instanceof Name&&(t.fontName=e.name);"number"==typeof a&&a>0&&(t.fontSize=a);break;case It:ColorSpaceUtils.rgb.getRgbItem(r,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(r,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(r,0,t.fontColor,0)}}}catch(e){warn(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()}class AppearanceStreamEvaluator extends EvaluatorPreprocessor{constructor(e,t,a,r){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=a;this.globalColorSpaceCache=r;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:ColorSpaceUtils.gray},a=!1;const r=[];try{for(;;){e.args.length=0;if(a||!this.read(e))break;const{fn:i,args:n}=e;switch(0|i){case Be:r.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case Re:t=r.pop()||t;break;case ht:t.scaleFactor*=Math.hypot(n[0],n[1]);break;case nt:const[e,i]=n;e instanceof Name&&(t.fontName=e.name);"number"==typeof i&&i>0&&(t.fontSize=i*t.scaleFactor);break;case wt:t.fillColorSpace=ColorSpaceUtils.parse({cs:n[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,globalColorSpaceCache:this.globalColorSpaceCache,localColorSpaceCache:this._localColorSpaceCache});break;case At:t.fillColorSpace.getRgbItem(n,0,t.fontColor,0);break;case It:ColorSpaceUtils.rgb.getRgbItem(n,0,t.fontColor,0);break;case vt:ColorSpaceUtils.gray.getRgbItem(n,0,t.fontColor,0);break;case Tt:ColorSpaceUtils.cmyk.getRgbItem(n,0,t.fontColor,0);break;case dt:case ft:case gt:case pt:a=!0}}}catch(e){warn(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return shadow(this,"_localColorSpaceCache",new LocalColorSpaceCache)}get _pdfFunctionFactory(){return shadow(this,"_pdfFunctionFactory",new PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported}))}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){return`${numberToString(e[0]/255)} ${t?"g":"G"}`}return Array.from(e,(e=>numberToString(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const a=new OffscreenCanvas(1,1);this.ctxMeasure=a.getContext("2d",{willReadFrequently:!0});FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get fontDescriptorRef(){if(!FakeUnicodeFont._fontDescriptorRef){const e=new Dict(this.xref);e.set("Type",Name.get("FontDescriptor"));e.set("FontName",this.fontName);e.set("FontFamily","MyriadPro Regular");e.set("FontBBox",[0,0,0,0]);e.set("FontStretch",Name.get("Normal"));e.set("FontWeight",400);e.set("ItalicAngle",0);FakeUnicodeFont._fontDescriptorRef=this.xref.getNewPersistentRef(e)}return FakeUnicodeFont._fontDescriptorRef}get descendantFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("CIDFontType0"));e.set("CIDToGIDMap",Name.get("Identity"));e.set("FirstChar",this.firstChar);e.set("LastChar",this.lastChar);e.set("FontDescriptor",this.fontDescriptorRef);e.set("DW",1e3);const t=[],a=[...this.widths.entries()].sort();let r=null,i=null;for(const[e,n]of a)if(r)if(e===r+i.length)i.push(n);else{t.push(r,i);r=e;i=[n]}else{r=e;i=[n]}r&&t.push(r,i);e.set("W",t);const n=new Dict(this.xref);n.set("Ordering","Identity");n.set("Registry","Adobe");n.set("Supplement",0);e.set("CIDSystemInfo",n);return this.xref.getNewPersistentRef(e)}get baseFontRef(){const e=new Dict(this.xref);e.set("BaseFont",this.fontName);e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type0"));e.set("Encoding",Name.get("Identity-H"));e.set("DescendantFonts",[this.descendantFontRef]);e.set("ToUnicode",Name.get("Identity-H"));return this.xref.getNewPersistentRef(e)}get resources(){const e=new Dict(this.xref),t=new Dict(this.xref);t.set(this.fontName.name,this.baseFontRef);e.set("Font",t);return e}_createContext(){this.widths=new Map;this.ctxMeasure.font=`1000px ${this.fontFamily}`;return this.ctxMeasure}createFontResources(e){const t=this._createContext();for(const a of e.split(/\r\n?|\n/))for(const e of a.split("")){const a=e.charCodeAt(0);if(this.widths.has(a))continue;const r=t.measureText(e),i=Math.ceil(r.width);this.widths.set(a,i);this.firstChar=Math.min(a,this.firstChar);this.lastChar=Math.max(a,this.lastChar)}return this.resources}static getFirstPositionInfo(e,t,i){const[n,s,o,c]=e;let l=o-n,h=c-s;t%180!=0&&([l,h]=[h,l]);const u=a*i;return{coords:[0,h+r*i-u],bbox:[0,0,l,h],matrix:0!==t?getRotationMatrix(t,h,u):void 0}}createAppearance(e,t,i,n,s,o){const c=this._createContext(),l=[];let h=-1/0;for(const t of e.split(/\r\n?|\n/)){l.push(t);const e=c.measureText(t).width;h=Math.max(h,e);for(const e of codePointIter(t)){const t=String.fromCodePoint(e);let a=this.widths.get(e);if(void 0===a){const r=c.measureText(t);a=Math.ceil(r.width);this.widths.set(e,a);this.firstChar=Math.min(e,this.firstChar);this.lastChar=Math.max(e,this.lastChar)}}}h*=n/1e3;const[u,d,f,g]=t;let p=f-u,m=g-d;i%180!=0&&([p,m]=[m,p]);let b=1;h>p&&(b=p/h);let y=1;const w=a*n,S=r*n,x=w*l.length;x>m&&(y=m/x);const k=n*Math.min(b,y),C=["q",`0 0 ${numberToString(p)} ${numberToString(m)} re W n`,"BT",`1 0 0 1 0 ${numberToString(m+S)} Tm 0 Tc ${getPdfColor(s,!0)}`,`/${this.fontName.name} ${numberToString(k)} Tf`],{resources:v}=this;if(1!==(o="number"==typeof o&&o>=0&&o<=1?o:1)){C.push("/R0 gs");const e=new Dict(this.xref),t=new Dict(this.xref);t.set("ca",o);t.set("CA",o);t.set("Type",Name.get("ExtGState"));e.set("R0",t);v.set("ExtGState",e)}const F=numberToString(w);for(const e of l)C.push(`0 -${F} Td <${stringToUTF16HexString(e)}> Tj`);C.push("ET","Q");const O=C.join("\n"),T=new Dict(this.xref);T.set("Subtype",Name.get("Form"));T.set("Type",Name.get("XObject"));T.set("BBox",[0,0,p,m]);T.set("Length",O.length);T.set("Resources",v);if(i){const e=getRotationMatrix(i,p,m);T.set("Matrix",e)}const M=new StringStream(O);M.dict=T;return M}}class NameOrNumberTree{constructor(e,t,a){this.root=e;this.xref=t;this._type=a}getAll(){const e=new Map;if(!this.root)return e;const t=this.xref,a=new RefSet;a.put(this.root);const r=[this.root];for(;r.length>0;){const i=t.fetchIfRef(r.shift());if(!(i instanceof Dict))continue;if(i.has("Kids")){const e=i.get("Kids");if(!Array.isArray(e))continue;for(const t of e){if(a.has(t))throw new FormatError(`Duplicate entry in "${this._type}" tree.`);r.push(t);a.put(t)}continue}const n=i.get(this._type);if(Array.isArray(n))for(let a=0,r=n.length;a10){warn(`Search depth limit reached for "${this._type}" tree.`);return null}const i=a.get("Kids");if(!Array.isArray(i))return null;let n=0,s=i.length-1;for(;n<=s;){const r=n+s>>1,o=t.fetchIfRef(i[r]),c=o.get("Limits");if(et.fetchIfRef(c[1]))){a=o;break}n=r+1}}if(n>s)return null}const i=a.get(this._type);if(Array.isArray(i)){let a=0,r=i.length-2;for(;a<=r;){const n=a+r>>1,s=n+(1&n),o=t.fetchIfRef(i[s]);if(eo))return i[s+1];a=s+2}}}return null}get(e){return this.xref.fetchIfRef(this.getRaw(e))}}class NameTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Names")}}class NumberTree extends NameOrNumberTree{constructor(e,t){super(e,t,"Nums")}}function clearGlobalCaches(){!function clearPatternCaches(){Ii=Object.create(null)}();!function clearPrimitiveCaches(){Sa=Object.create(null);xa=Object.create(null);Aa=Object.create(null)}();!function clearUnicodeCaches(){Dr.clear()}();JpxImage.cleanup()}function pickPlatformItem(e){return e instanceof Dict?e.has("UF")?e.get("UF"):e.has("F")?e.get("F"):e.has("Unix")?e.get("Unix"):e.has("Mac")?e.get("Mac"):e.has("DOS")?e.get("DOS"):null:null}class FileSpec{#Y=!1;constructor(e,t,a=!1){if(e instanceof Dict){this.xref=t;this.root=e;e.has("FS")&&(this.fs=e.get("FS"));e.has("RF")&&warn("Related file specifications are not supported");a||(e.has("EF")?this.#Y=!0:warn("Non-embedded file specifications are not supported"))}}get filename(){let e="";const t=pickPlatformItem(this.root);t&&"string"==typeof t&&(e=stringToPDFString(t,!0).replaceAll("\\\\","\\").replaceAll("\\/","/").replaceAll("\\","/"));return shadow(this,"filename",e||"unnamed")}get content(){if(!this.#Y)return null;this._contentRef||=pickPlatformItem(this.root?.get("EF"));let e=null;if(this._contentRef){const t=this.xref.fetchIfRef(this._contentRef);t instanceof BaseStream?e=t.getBytes():warn("Embedded file specification points to non-existing/invalid content")}else warn("Embedded file specification does not have any content");return e}get description(){let e="";const t=this.root?.get("Desc");t&&"string"==typeof t&&(e=stringToPDFString(t));return shadow(this,"description",e)}get serializable(){return{rawFilename:this.filename,filename:(e=this.filename,e.substring(e.lastIndexOf("/")+1)),content:this.content,description:this.description};var e}}const Pn=0,Ln=-2,jn=-3,_n=-4,Un=-5,Xn=-6,qn=-9;function isWhitespace(e,t){const a=e[t];return" "===a||"\n"===a||"\r"===a||"\t"===a}class XMLParserBase{_resolveEntities(e){return e.replaceAll(/&([^;]+);/g,((e,t)=>{if("#x"===t.substring(0,2))return String.fromCodePoint(parseInt(t.substring(2),16));if("#"===t.substring(0,1))return String.fromCodePoint(parseInt(t.substring(1),10));switch(t){case"lt":return"<";case"gt":return">";case"amp":return"&";case"quot":return'"';case"apos":return"'"}return this.onResolveEntity(t)}))}_parseContent(e,t){const a=[];let r=t;function skipWs(){for(;r"!==e[r]&&"/"!==e[r];)++r;const i=e.substring(t,r);skipWs();for(;r"!==e[r]&&"/"!==e[r]&&"?"!==e[r];){skipWs();let t="",i="";for(;r"!==e[a]&&"?"!==e[a]&&"/"!==e[a];)++a;const r=e.substring(t,a);!function skipWs(){for(;a"!==e[a+1]);)++a;return{name:r,value:e.substring(i,a),parsed:a-t}}parseXml(e){let t=0;for(;t",a);if(t<0){this.onError(qn);return}this.onEndElement(e.substring(a,t));a=t+1;break;case"?":++a;const r=this._parseProcessingInstruction(e,a);if("?>"!==e.substring(a+r.parsed,a+r.parsed+2)){this.onError(jn);return}this.onPi(r.name,r.value);a+=r.parsed+2;break;case"!":if("--"===e.substring(a+1,a+3)){t=e.indexOf("--\x3e",a+3);if(t<0){this.onError(Un);return}this.onComment(e.substring(a+3,t));a=t+3}else if("[CDATA["===e.substring(a+1,a+8)){t=e.indexOf("]]>",a+8);if(t<0){this.onError(Ln);return}this.onCdata(e.substring(a+8,t));a=t+3}else{if("DOCTYPE"!==e.substring(a+1,a+8)){this.onError(Xn);return}{const r=e.indexOf("[",a+8);let i=!1;t=e.indexOf(">",a+8);if(t<0){this.onError(_n);return}if(r>0&&t>r){t=e.indexOf("]>",a+8);if(t<0){this.onError(_n);return}i=!0}const n=e.substring(a+8,t+(i?1:0));this.onDoctype(n);a=t+(i?2:1)}}break;default:const i=this._parseContent(e,a);if(null===i){this.onError(Xn);return}let n=!1;if("/>"===e.substring(a+i.parsed,a+i.parsed+2))n=!0;else if(">"!==e.substring(a+i.parsed,a+i.parsed+1)){this.onError(qn);return}this.onBeginElement(i.name,i.attributes,n);a+=i.parsed+(n?2:1)}}else{for(;ae.textContent)).join(""):this.nodeValue||""}get children(){return this.childNodes||[]}hasChildNodes(){return this.childNodes?.length>0}searchNode(e,t){if(t>=e.length)return this;const a=e[t];if(a.name.startsWith("#")&&t0){r.push([i,0]);i=i.childNodes[0]}else{if(0===r.length)return null;for(;0!==r.length;){const[e,t]=r.pop(),a=t+1;if(a");for(const t of this.childNodes)t.dump(e);e.push(``)}else this.nodeValue?e.push(`>${encodeToXmlString(this.nodeValue)}`):e.push("/>")}else e.push(encodeToXmlString(this.nodeValue))}}class SimpleXMLParser extends XMLParserBase{constructor({hasAttributes:e=!1,lowerCaseName:t=!1}){super();this._currentFragment=null;this._stack=null;this._errorCode=Pn;this._hasAttributes=e;this._lowerCaseName=t}parseFromString(e){this._currentFragment=[];this._stack=[];this._errorCode=Pn;this.parseXml(e);if(this._errorCode!==Pn)return;const[t]=this._currentFragment;return t?{documentElement:t}:void 0}onText(e){if(function isWhitespaceString(e){for(let t=0,a=e.length;t\\376\\377([^<]+)/g,(function(e,t){const a=t.replaceAll(/\\([0-3])([0-7])([0-7])/g,(function(e,t,a,r){return String.fromCharCode(64*t+8*a+1*r)})).replaceAll(/&(amp|apos|gt|lt|quot);/g,(function(e,t){switch(t){case"amp":return"&";case"apos":return"'";case"gt":return">";case"lt":return"<";case"quot":return'"'}throw new Error(`_repair: ${t} isn't defined.`)})),r=[">"];for(let e=0,t=a.length;e=32&&t<127&&60!==t&&62!==t&&38!==t?r.push(String.fromCharCode(t)):r.push("&#x"+(65536+t).toString(16).substring(1)+";")}return r.join("")}))}_getSequence(e){const t=e.nodeName;return"rdf:bag"!==t&&"rdf:seq"!==t&&"rdf:alt"!==t?null:e.childNodes.filter((e=>"rdf:li"===e.nodeName))}_parseArray(e){if(!e.hasChildNodes())return;const[t]=e.childNodes,a=this._getSequence(t)||[];this._metadataMap.set(e.nodeName,a.map((e=>e.textContent.trim())))}_parse(e){let t=e.documentElement;if("rdf:rdf"!==t.nodeName){t=t.firstChild;for(;t&&"rdf:rdf"!==t.nodeName;)t=t.nextSibling}if(t&&"rdf:rdf"===t.nodeName&&t.hasChildNodes())for(const e of t.childNodes)if("rdf:description"===e.nodeName)for(const t of e.childNodes){const e=t.nodeName;switch(e){case"#text":continue;case"dc:creator":case"dc:subject":this._parseArray(t);continue}this._metadataMap.set(e,t.textContent.trim())}}get serializable(){return{parsedData:this._metadataMap,rawData:this._data}}}const Hn=1,Wn=2,zn=3,$n=4,Gn=5;class StructTreeRoot{constructor(e,t,a){this.xref=e;this.dict=t;this.ref=a instanceof Ref?a:null;this.roleMap=new Map;this.structParentIds=null}init(){this.readRoleMap()}#Z(e,t,a){if(!(e instanceof Ref)||t<0)return;this.structParentIds||=new RefSetCache;let r=this.structParentIds.get(e);if(!r){r=[];this.structParentIds.put(e,r)}r.push([t,a])}addAnnotationIdToPage(e,t){this.#Z(e,t,$n)}readRoleMap(){const e=this.dict.get("RoleMap");if(e instanceof Dict)for(const[t,a]of e)a instanceof Name&&this.roleMap.set(t,a.name)}static async canCreateStructureTree({catalogRef:e,pdfManager:t,newAnnotationsByPage:a}){if(!(e instanceof Ref)){warn("Cannot save the struct tree: no catalog reference.");return!1}let r=0,i=!0;for(const[e,n]of a){const{ref:a}=await t.getPage(e);if(!(a instanceof Ref)){warn(`Cannot save the struct tree: page ${e} has no ref.`);i=!0;break}for(const e of n)if(e.accessibilityData?.type){e.parentTreeId=r++;i=!1}}if(i){for(const e of a.values())for(const t of e)delete t.parentTreeId;return!1}return!0}static async createStructureTree({newAnnotationsByPage:e,xref:t,catalogRef:a,pdfManager:r,changes:i}){const n=await r.ensureCatalog("cloneDict"),s=new RefSetCache;s.put(a,n);const o=t.getNewTemporaryRef();n.set("StructTreeRoot",o);const c=new Dict(t);c.set("Type",Name.get("StructTreeRoot"));const l=t.getNewTemporaryRef();c.set("ParentTree",l);const h=[];c.set("K",h);s.put(o,c);const u=new Dict(t),d=[];u.set("Nums",d);const f=await this.#Q({newAnnotationsByPage:e,structTreeRootRef:o,structTreeRoot:null,kids:h,nums:d,xref:t,pdfManager:r,changes:i,cache:s});c.set("ParentTreeNextKey",f);s.put(l,u);for(const[e,t]of s.items())i.put(e,{data:t})}async canUpdateStructTree({pdfManager:e,newAnnotationsByPage:t}){if(!this.ref){warn("Cannot update the struct tree: no root reference.");return!1}let a=this.dict.get("ParentTreeNextKey");if(!Number.isInteger(a)||a<0){warn("Cannot update the struct tree: invalid next key.");return!1}const r=this.dict.get("ParentTree");if(!(r instanceof Dict)){warn("Cannot update the struct tree: ParentTree isn't a dict.");return!1}const i=r.get("Nums");if(!Array.isArray(i)){warn("Cannot update the struct tree: nums isn't an array.");return!1}const n=new NumberTree(r,this.xref);for(const a of t.keys()){const{pageDict:t}=await e.getPage(a);if(!t.has("StructParents"))continue;const r=t.get("StructParents");if(!Number.isInteger(r)||!Array.isArray(n.get(r))){warn(`Cannot save the struct tree: page ${a} has a wrong id.`);return!1}}let s=!0;for(const[r,i]of t){const{pageDict:t}=await e.getPage(r);StructTreeRoot.#ee({elements:i,xref:this.xref,pageDict:t,numberTree:n});for(const e of i)if(e.accessibilityData?.type){e.accessibilityData.structParent>=0||(e.parentTreeId=a++);s=!1}}if(s){for(const e of t.values())for(const t of e){delete t.parentTreeId;delete t.structTreeParent}return!1}return!0}async updateStructureTree({newAnnotationsByPage:e,pdfManager:t,changes:a}){const{ref:r,xref:i}=this,n=this.dict.clone(),s=new RefSetCache;s.put(r,n);let o,c=n.getRaw("ParentTree");if(c instanceof Ref)o=i.fetch(c);else{o=c;c=i.getNewTemporaryRef();n.set("ParentTree",c)}o=o.clone();s.put(c,o);let l=o.getRaw("Nums"),h=null;if(l instanceof Ref){h=l;l=i.fetch(h)}l=l.slice();h||o.set("Nums",l);const u=await StructTreeRoot.#Q({newAnnotationsByPage:e,structTreeRootRef:r,structTreeRoot:this,kids:null,nums:l,xref:i,pdfManager:t,changes:a,cache:s});if(-1!==u){n.set("ParentTreeNextKey",u);h&&s.put(h,l);for(const[e,t]of s.items())a.put(e,{data:t})}}static async#Q({newAnnotationsByPage:e,structTreeRootRef:t,structTreeRoot:a,kids:r,nums:i,xref:n,pdfManager:s,changes:o,cache:c}){const l=Name.get("OBJR");let h,u=-1;for(const[d,f]of e){const e=await s.getPage(d),{ref:g}=e,p=g instanceof Ref;for(const{accessibilityData:s,ref:m,parentTreeId:b,structTreeParent:y}of f){if(!s?.type)continue;const{structParent:f}=s;if(a&&Number.isInteger(f)&&f>=0){let t=(h||=new Map).get(d);if(void 0===t){t=new StructTreePage(a,e.pageDict).collectObjects(g);h.set(d,t)}const r=t?.get(f);if(r){const e=n.fetch(r).clone();StructTreeRoot.#te(e,s);o.put(r,{data:e});continue}}u=Math.max(u,b);const w=n.getNewTemporaryRef(),S=new Dict(n);StructTreeRoot.#te(S,s);await this.#ae({structTreeParent:y,tagDict:S,newTagRef:w,structTreeRootRef:t,fallbackKids:r,xref:n,cache:c});const x=new Dict(n);S.set("K",x);x.set("Type",l);p&&x.set("Pg",g);x.set("Obj",m);c.put(w,S);i.push(b,w)}}return u+1}static#te(e,{type:t,title:a,lang:r,alt:i,expanded:n,actualText:s}){e.set("S",Name.get(t));a&&e.set("T",stringToAsciiOrUTF16BE(a));r&&e.set("Lang",stringToAsciiOrUTF16BE(r));i&&e.set("Alt",stringToAsciiOrUTF16BE(i));n&&e.set("E",stringToAsciiOrUTF16BE(n));s&&e.set("ActualText",stringToAsciiOrUTF16BE(s))}static#ee({elements:e,xref:t,pageDict:a,numberTree:r}){const i=new Map;for(const t of e)if(t.structTreeParentId){const e=parseInt(t.structTreeParentId.split("_mc")[1],10);let a=i.get(e);if(!a){a=[];i.set(e,a)}a.push(t)}const n=a.get("StructParents");if(!Number.isInteger(n))return;const s=r.get(n),updateElement=(e,a,r)=>{const n=i.get(e);if(n){const e=a.getRaw("P"),i=t.fetchIfRef(e);if(e instanceof Ref&&i instanceof Dict){const e={ref:r,dict:a};for(const t of n)t.structTreeParent=e}return!0}return!1};for(const e of s){if(!(e instanceof Ref))continue;const a=t.fetch(e),r=a.get("K");if(Number.isInteger(r))updateElement(r,a,e);else if(Array.isArray(r))for(let i of r){i=t.fetchIfRef(i);if(Number.isInteger(i)&&updateElement(i,a,e))break;if(!(i instanceof Dict))continue;if(!isName(i.get("Type"),"MCR"))break;const r=i.get("MCID");if(Number.isInteger(r)&&updateElement(r,a,e))break}}}static async#ae({structTreeParent:e,tagDict:t,newTagRef:a,structTreeRootRef:r,fallbackKids:i,xref:n,cache:s}){let o,c=null;if(e){({ref:c}=e);o=e.dict.getRaw("P")||r}else o=r;t.set("P",o);const l=n.fetchIfRef(o);if(!l){i.push(a);return}let h=s.get(o);if(!h){h=l.clone();s.put(o,h)}const u=h.getRaw("K");let d=u instanceof Ref?s.get(u):null;if(!d){d=n.fetchIfRef(u);d=Array.isArray(d)?d.slice():[u];const e=n.getNewTemporaryRef();h.set("K",e);s.put(e,d)}const f=d.indexOf(c);d.splice(f>=0?f+1:d.length,0,a)}}class StructElementNode{constructor(e,t){this.tree=e;this.xref=e.xref;this.dict=t;this.kids=[];this.parseKids()}get role(){const e=this.dict.get("S"),t=e instanceof Name?e.name:"",{root:a}=this.tree;return a.roleMap.get(t)??t}parseKids(){let e=null;const t=this.dict.getRaw("Pg");t instanceof Ref&&(e=t.toString());const a=this.dict.get("K");if(Array.isArray(a))for(const t of a){const a=this.parseKid(e,this.xref.fetchIfRef(t));a&&this.kids.push(a)}else{const t=this.parseKid(e,a);t&&this.kids.push(t)}}parseKid(e,t){if(Number.isInteger(t))return this.tree.pageDict.objId!==e?null:new StructElement({type:Hn,mcid:t,pageObjId:e});if(!(t instanceof Dict))return null;const a=t.getRaw("Pg");a instanceof Ref&&(e=a.toString());const r=t.get("Type")instanceof Name?t.get("Type").name:null;if("MCR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Stm");return new StructElement({type:Wn,refObjId:a instanceof Ref?a.toString():null,pageObjId:e,mcid:t.get("MCID")})}if("OBJR"===r){if(this.tree.pageDict.objId!==e)return null;const a=t.getRaw("Obj");return new StructElement({type:zn,refObjId:a instanceof Ref?a.toString():null,pageObjId:e})}return new StructElement({type:Gn,dict:t})}}class StructElement{constructor({type:e,dict:t=null,mcid:a=null,pageObjId:r=null,refObjId:i=null}){this.type=e;this.dict=t;this.mcid=a;this.pageObjId=r;this.refObjId=i;this.parentNode=null}}class StructTreePage{constructor(e,t){this.root=e;this.xref=e?.xref??null;this.rootDict=e?.dict??null;this.pageDict=t;this.nodes=[]}collectObjects(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return null;const t=this.rootDict.get("ParentTree");if(!t)return null;const a=this.root.structParentIds?.get(e);if(!a)return null;const r=new Map,i=new NumberTree(t,this.xref);for(const[e]of a){const t=i.getRaw(e);t instanceof Ref&&r.set(e,t)}return r}parse(e){if(!(this.root&&this.rootDict&&e instanceof Ref))return;const t=this.rootDict.get("ParentTree");if(!t)return;const a=this.pageDict.get("StructParents"),r=this.root.structParentIds?.get(e);if(!Number.isInteger(a)&&!r)return;const i=new Map,n=new NumberTree(t,this.xref);if(Number.isInteger(a)){const e=n.get(a);if(Array.isArray(e))for(const t of e)t instanceof Ref&&this.addNode(this.xref.fetch(t),i)}if(r)for(const[e,t]of r){const a=n.get(e);if(a){const e=this.addNode(this.xref.fetchIfRef(a),i);1===e?.kids?.length&&e.kids[0].type===zn&&(e.kids[0].type=t)}}}addNode(e,t,a=0){if(a>40){warn("StructTree MAX_DEPTH reached.");return null}if(!(e instanceof Dict))return null;if(t.has(e))return t.get(e);const r=new StructElementNode(this,e);t.set(e,r);const i=e.get("P");if(!(i instanceof Dict)||isName(i.get("Type"),"StructTreeRoot")){this.addTopLevelNode(e,r)||t.delete(e);return r}const n=this.addNode(i,t,a+1);if(!n)return r;let s=!1;for(const t of n.kids)if(t.type===Gn&&t.dict===e){t.parentNode=r;s=!0}s||t.delete(e);return r}addTopLevelNode(e,t){const a=this.rootDict.get("K");if(!a)return!1;if(a instanceof Dict){if(a.objId!==e.objId)return!1;this.nodes[0]=t;return!0}if(!Array.isArray(a))return!0;let r=!1;for(let i=0;i40){warn("StructTree too deep to be fully serialized.");return}const r=Object.create(null);r.role=e.role;r.children=[];t.children.push(r);let i=e.dict.get("Alt");"string"!=typeof i&&(i=e.dict.get("ActualText"));"string"==typeof i&&(r.alt=stringToPDFString(i));const n=e.dict.get("A");if(n instanceof Dict){const e=lookupNormalRect(n.getArray("BBox"),null);if(e)r.bbox=e;else{const e=n.get("Width"),t=n.get("Height");"number"==typeof e&&e>0&&"number"==typeof t&&t>0&&(r.bbox=[0,0,e,t])}}const s=e.dict.get("Lang");"string"==typeof s&&(r.lang=stringToPDFString(s));for(const t of e.kids){const e=t.type===Gn?t.parentNode:null;e?nodeToSerializable(e,r,a+1):t.type===Hn||t.type===Wn?r.children.push({type:"content",id:`p${t.pageObjId}_mc${t.mcid}`}):t.type===zn?r.children.push({type:"object",id:t.refObjId}):t.type===$n&&r.children.push({type:"annotation",id:`pdfjs_internal_id_${t.refObjId}`})}}const e=Object.create(null);e.children=[];e.role="Root";for(const t of this.nodes)t&&nodeToSerializable(t,e);return e}}const Vn=function _isValidExplicitDest(e,t,a){if(!Array.isArray(a)||a.length<2)return!1;const[r,i,...n]=a;if(!e(r)&&!Number.isInteger(r))return!1;if(!t(i))return!1;const s=n.length;let o=!0;switch(i.name){case"XYZ":if(s<2||s>3)return!1;break;case"Fit":case"FitB":return 0===s;case"FitH":case"FitBH":case"FitV":case"FitBV":if(s>1)return!1;break;case"FitR":if(4!==s)return!1;o=!1;break;default:return!1}for(const e of n)if(!("number"==typeof e||o&&null===e))return!1;return!0}.bind(null,(e=>e instanceof Ref),isName);function fetchDest(e){e instanceof Dict&&(e=e.get("D"));return Vn(e)?e:null}function fetchRemoteDest(e){let t=e.get("D");if(t){t instanceof Name&&(t=t.name);if("string"==typeof t)return stringToPDFString(t,!0);if(Vn(t))return JSON.stringify(t)}return null}class Catalog{#re=null;#ie=null;builtInCMapCache=new Map;fontCache=new RefSetCache;globalColorSpaceCache=new GlobalColorSpaceCache;globalImageCache=new GlobalImageCache;nonBlendModesSet=new RefSet;pageDictCache=new RefSetCache;pageIndexCache=new RefSetCache;pageKidsCountCache=new RefSetCache;standardFontDataCache=new Map;systemFontCache=new Map;constructor(e,t){this.pdfManager=e;this.xref=t;this.#ie=t.getCatalogObj();if(!(this.#ie instanceof Dict))throw new FormatError("Catalog object is not a dictionary.");this.toplevelPagesDict}cloneDict(){return this.#ie.clone()}get version(){const e=this.#ie.get("Version");if(e instanceof Name){if(Ca.test(e.name))return shadow(this,"version",e.name);warn(`Invalid PDF catalog version: ${e.name}`)}return shadow(this,"version",null)}get lang(){const e=this.#ie.get("Lang");return shadow(this,"lang",e&&"string"==typeof e?stringToPDFString(e):null)}get needsRendering(){const e=this.#ie.get("NeedsRendering");return shadow(this,"needsRendering","boolean"==typeof e&&e)}get collection(){let e=null;try{const t=this.#ie.get("Collection");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch Collection entry; assuming no collection is present.")}return shadow(this,"collection",e)}get acroForm(){let e=null;try{const t=this.#ie.get("AcroForm");t instanceof Dict&&t.size>0&&(e=t)}catch(e){if(e instanceof MissingDataException)throw e;info("Cannot fetch AcroForm entry; assuming no forms are present.")}return shadow(this,"acroForm",e)}get acroFormRef(){const e=this.#ie.getRaw("AcroForm");return shadow(this,"acroFormRef",e instanceof Ref?e:null)}get metadata(){const e=this.#ie.getRaw("Metadata");if(!(e instanceof Ref))return shadow(this,"metadata",null);let t=null;try{const a=this.xref.fetch(e,!this.xref.encrypt?.encryptMetadata);if(a instanceof BaseStream&&a.dict instanceof Dict){const e=a.dict.get("Type"),r=a.dict.get("Subtype");if(isName(e,"Metadata")&&isName(r,"XML")){const e=stringToUTF8String(a.getString());e&&(t=new MetadataParser(e).serializable)}}}catch(e){if(e instanceof MissingDataException)throw e;info(`Skipping invalid Metadata: "${e}".`)}return shadow(this,"metadata",t)}get markInfo(){let e=null;try{e=this.#ne()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read mark info.")}return shadow(this,"markInfo",e)}#ne(){const e=this.#ie.get("MarkInfo");if(!(e instanceof Dict))return null;const t={Marked:!1,UserProperties:!1,Suspects:!1};for(const a in t){const r=e.get(a);"boolean"==typeof r&&(t[a]=r)}return t}get structTreeRoot(){let e=null;try{e=this.#se()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable read to structTreeRoot info.")}return shadow(this,"structTreeRoot",e)}#se(){const e=this.#ie.getRaw("StructTreeRoot"),t=this.xref.fetchIfRef(e);if(!(t instanceof Dict))return null;const a=new StructTreeRoot(this.xref,t,e);a.init();return a}get toplevelPagesDict(){const e=this.#ie.get("Pages");if(!(e instanceof Dict))throw new FormatError("Invalid top-level pages dictionary.");return shadow(this,"toplevelPagesDict",e)}get documentOutline(){let e=null;try{e=this.#oe()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read document outline.")}return shadow(this,"documentOutline",e)}#oe(){let e=this.#ie.get("Outlines");if(!(e instanceof Dict))return null;e=e.getRaw("First");if(!(e instanceof Ref))return null;const t={items:[]},a=[{obj:e,parent:t}],r=new RefSet;r.put(e);const i=this.xref,n=new Uint8ClampedArray(3);for(;a.length>0;){const t=a.shift(),s=i.fetchIfRef(t.obj);if(null===s)continue;s.has("Title")||warn("Invalid outline item encountered.");const o={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:s,resultObj:o,docBaseUrl:this.baseUrl,docAttachments:this.attachments});const c=s.get("Title"),l=s.get("F")||0,h=s.getArray("C"),u=s.get("Count");let d=n;!isNumberArray(h,3)||0===h[0]&&0===h[1]&&0===h[2]||(d=ColorSpaceUtils.rgb.getRgb(h,0));const f={action:o.action,attachment:o.attachment,dest:o.dest,url:o.url,unsafeUrl:o.unsafeUrl,newWindow:o.newWindow,setOCGState:o.setOCGState,title:"string"==typeof c?stringToPDFString(c):"",color:d,count:Number.isInteger(u)?u:void 0,bold:!!(2&l),italic:!!(1&l),items:[]};t.parent.items.push(f);e=s.getRaw("First");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:f});r.put(e)}e=s.getRaw("Next");if(e instanceof Ref&&!r.has(e)){a.push({obj:e,parent:t.parent});r.put(e)}}return t.items.length>0?t.items:null}get permissions(){let e=null;try{e=this.#ce()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read permissions.")}return shadow(this,"permissions",e)}#ce(){const e=this.xref.trailer.get("Encrypt");if(!(e instanceof Dict))return null;let t=e.get("P");if("number"!=typeof t)return null;t+=2**32;const a=[];for(const e in w){const r=w[e];t&r&&a.push(r)}return a}get optionalContentConfig(){let e=null;try{const t=this.#ie.get("OCProperties");if(!t)return shadow(this,"optionalContentConfig",null);const a=t.get("D");if(!a)return shadow(this,"optionalContentConfig",null);const r=t.get("OCGs");if(!Array.isArray(r))return shadow(this,"optionalContentConfig",null);const i=new RefSetCache;for(const e of r)e instanceof Ref&&!i.has(e)&&i.put(e,this.#le(e));e=this.#he(a,i)}catch(e){if(e instanceof MissingDataException)throw e;warn(`Unable to read optional content config: ${e}`)}return shadow(this,"optionalContentConfig",e)}#le(e){const t=this.xref.fetch(e),a={id:e.toString(),name:null,intent:null,usage:{print:null,view:null},rbGroups:[]},r=t.get("Name");"string"==typeof r&&(a.name=stringToPDFString(r));let i=t.getArray("Intent");Array.isArray(i)||(i=[i]);i.every((e=>e instanceof Name))&&(a.intent=i.map((e=>e.name)));const n=t.get("Usage");if(!(n instanceof Dict))return a;const s=a.usage,o=n.get("Print");if(o instanceof Dict){const e=o.get("PrintState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.print={printState:e.name}}}const c=n.get("View");if(c instanceof Dict){const e=c.get("ViewState");if(e instanceof Name)switch(e.name){case"ON":case"OFF":s.view={viewState:e.name}}}return a}#he(e,t){function parseOnOff(e){const a=[];if(Array.isArray(e))for(const r of e)r instanceof Ref&&t.has(r)&&a.push(r.toString());return a}function parseOrder(e,a=0){if(!Array.isArray(e))return null;const i=[];for(const n of e){if(n instanceof Ref&&t.has(n)){r.put(n);i.push(n.toString());continue}const e=parseNestedOrder(n,a);e&&i.push(e)}if(a>0)return i;const n=[];for(const[e]of t.items())r.has(e)||n.push(e.toString());n.length&&i.push({name:null,order:n});return i}function parseNestedOrder(e,t){if(++t>i){warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");return null}const r=a.fetchIfRef(e);if(!Array.isArray(r))return null;const n=a.fetchIfRef(r[0]);if("string"!=typeof n)return null;const s=parseOrder(r.slice(1),t);return s?.length?{name:stringToPDFString(n),order:s}:null}const a=this.xref,r=new RefSet,i=10;!function parseRBGroups(e){if(Array.isArray(e))for(const r of e){const e=a.fetchIfRef(r);if(!Array.isArray(e)||!e.length)continue;const i=new Set;for(const a of e)if(a instanceof Ref&&t.has(a)&&!i.has(a.toString())){i.add(a.toString());t.get(a).rbGroups.push(i)}}}(e.get("RBGroups"));return{name:"string"==typeof e.get("Name")?stringToPDFString(e.get("Name")):null,creator:"string"==typeof e.get("Creator")?stringToPDFString(e.get("Creator")):null,baseState:e.get("BaseState")instanceof Name?e.get("BaseState").name:null,on:parseOnOff(e.get("ON")),off:parseOnOff(e.get("OFF")),order:parseOrder(e.get("Order")),groups:[...t]}}setActualNumPages(e=null){this.#re=e}get hasActualNumPages(){return null!==this.#re}get _pagesCount(){const e=this.toplevelPagesDict.get("Count");if(!Number.isInteger(e))throw new FormatError("Page count in top-level pages dictionary is not an integer.");return shadow(this,"_pagesCount",e)}get numPages(){return this.#re??this._pagesCount}get destinations(){const e=this.#ue(),t=Object.create(null);for(const a of e)if(a instanceof NameTree)for(const[e,r]of a.getAll()){const a=fetchDest(r);a&&(t[stringToPDFString(e,!0)]=a)}else if(a instanceof Dict)for(const[e,r]of a){const a=fetchDest(r);a&&(t[stringToPDFString(e,!0)]||=a)}return shadow(this,"destinations",t)}getDestination(e){if(this.hasOwnProperty("destinations"))return this.destinations[e]??null;const t=this.#ue();for(const a of t)if(a instanceof NameTree||a instanceof Dict){const t=fetchDest(a.get(e));if(t)return t}if(t.length){const t=this.destinations[e];if(t)return t}return null}#ue(){const e=this.#ie.get("Names"),t=[];e?.has("Dests")&&t.push(new NameTree(e.getRaw("Dests"),this.xref));this.#ie.has("Dests")&&t.push(this.#ie.get("Dests"));return t}get pageLabels(){let e=null;try{e=this.#de()}catch(e){if(e instanceof MissingDataException)throw e;warn("Unable to read page labels.")}return shadow(this,"pageLabels",e)}#de(){const e=this.#ie.getRaw("PageLabels");if(!e)return null;const t=new Array(this.numPages);let a=null,r="";const i=new NumberTree(e,this.xref).getAll();let n="",s=1;for(let e=0,o=this.numPages;e=1))throw new FormatError("Invalid start in PageLabel dictionary.");s=e}else s=1}switch(a){case"D":n=s;break;case"R":case"r":n=toRomanNumerals(s,"r"===a);break;case"A":case"a":const e=26,t="a"===a?97:65,r=s-1;n=String.fromCharCode(t+r%e).repeat(Math.floor(r/e)+1);break;default:if(a)throw new FormatError(`Invalid style "${a}" in PageLabel dictionary.`);n=""}t[e]=r+n;s++}return t}get pageLayout(){const e=this.#ie.get("PageLayout");let t="";if(e instanceof Name)switch(e.name){case"SinglePage":case"OneColumn":case"TwoColumnLeft":case"TwoColumnRight":case"TwoPageLeft":case"TwoPageRight":t=e.name}return shadow(this,"pageLayout",t)}get pageMode(){const e=this.#ie.get("PageMode");let t="UseNone";if(e instanceof Name)switch(e.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"FullScreen":case"UseOC":case"UseAttachments":t=e.name}return shadow(this,"pageMode",t)}get viewerPreferences(){const e=this.#ie.get("ViewerPreferences");if(!(e instanceof Dict))return shadow(this,"viewerPreferences",null);let t=null;for(const[a,r]of e){let e;switch(a){case"HideToolbar":case"HideMenubar":case"HideWindowUI":case"FitWindow":case"CenterWindow":case"DisplayDocTitle":case"PickTrayByPDFSize":"boolean"==typeof r&&(e=r);break;case"NonFullScreenPageMode":if(r instanceof Name)switch(r.name){case"UseNone":case"UseOutlines":case"UseThumbs":case"UseOC":e=r.name;break;default:e="UseNone"}break;case"Direction":if(r instanceof Name)switch(r.name){case"L2R":case"R2L":e=r.name;break;default:e="L2R"}break;case"ViewArea":case"ViewClip":case"PrintArea":case"PrintClip":if(r instanceof Name)switch(r.name){case"MediaBox":case"CropBox":case"BleedBox":case"TrimBox":case"ArtBox":e=r.name;break;default:e="CropBox"}break;case"PrintScaling":if(r instanceof Name)switch(r.name){case"None":case"AppDefault":e=r.name;break;default:e="AppDefault"}break;case"Duplex":if(r instanceof Name)switch(r.name){case"Simplex":case"DuplexFlipShortEdge":case"DuplexFlipLongEdge":e=r.name;break;default:e="None"}break;case"PrintPageRange":if(Array.isArray(r)&&r.length%2==0){r.every(((e,t,a)=>Number.isInteger(e)&&e>0&&(0===t||e>=a[t-1])&&e<=this.numPages))&&(e=r)}break;case"NumCopies":Number.isInteger(r)&&r>0&&(e=r);break;default:warn(`Ignoring non-standard key in ViewerPreferences: ${a}.`);continue}if(void 0!==e){t??=Object.create(null);t[a]=e}else warn(`Bad value, for key "${a}", in ViewerPreferences: ${r}.`)}return shadow(this,"viewerPreferences",t)}get openAction(){const e=this.#ie.get("OpenAction"),t=Object.create(null);if(e instanceof Dict){const a=new Dict(this.xref);a.set("A",e);const r={url:null,dest:null,action:null};Catalog.parseDestDictionary({destDict:a,resultObj:r});Array.isArray(r.dest)?t.dest=r.dest:r.action&&(t.action=r.action)}else Vn(e)&&(t.dest=e);return shadow(this,"openAction",objectSize(t)>0?t:null)}get attachments(){const e=this.#ie.get("Names");let t=null;if(e instanceof Dict&&e.has("EmbeddedFiles")){const a=new NameTree(e.getRaw("EmbeddedFiles"),this.xref);for(const[e,r]of a.getAll()){const a=new FileSpec(r,this.xref);t??=Object.create(null);t[stringToPDFString(e,!0)]=a.serializable}}return shadow(this,"attachments",t)}get xfaImages(){const e=this.#ie.get("Names");let t=null;if(e instanceof Dict&&e.has("XFAImages")){const a=new NameTree(e.getRaw("XFAImages"),this.xref);for(const[e,r]of a.getAll())if(r instanceof BaseStream){t??=new Map;t.set(stringToPDFString(e,!0),r.getBytes())}}return shadow(this,"xfaImages",t)}#fe(){const e=this.#ie.get("Names");let t=null;function appendIfJavaScriptDict(e,a){if(!(a instanceof Dict))return;if(!isName(a.get("S"),"JavaScript"))return;let r=a.get("JS");if(r instanceof BaseStream)r=r.getString();else if("string"!=typeof r)return;r=stringToPDFString(r,!0).replaceAll("\0","");r&&(t||=new Map).set(e,r)}if(e instanceof Dict&&e.has("JavaScript")){const t=new NameTree(e.getRaw("JavaScript"),this.xref);for(const[e,a]of t.getAll())appendIfJavaScriptDict(stringToPDFString(e,!0),a)}const a=this.#ie.get("OpenAction");a&&appendIfJavaScriptDict("OpenAction",a);return t}get jsActions(){const e=this.#fe();let t=collectActions(this.xref,this.#ie,we);if(e){t||=Object.create(null);for(const[a,r]of e)a in t?t[a].push(r):t[a]=[r]}return shadow(this,"jsActions",t)}async cleanup(e=!1){clearGlobalCaches();this.globalColorSpaceCache.clear();this.globalImageCache.clear(e);this.pageKidsCountCache.clear();this.pageIndexCache.clear();this.pageDictCache.clear();this.nonBlendModesSet.clear();for(const{dict:e}of await Promise.all(this.fontCache))delete e.cacheKey;this.fontCache.clear();this.builtInCMapCache.clear();this.standardFontDataCache.clear();this.systemFontCache.clear()}async getPageDict(e){const t=[this.toplevelPagesDict],a=new RefSet,r=this.#ie.getRaw("Pages");r instanceof Ref&&a.put(r);const i=this.xref,n=this.pageKidsCountCache,s=this.pageIndexCache,o=this.pageDictCache;let c=0;for(;t.length;){const r=t.pop();if(r instanceof Ref){const l=n.get(r);if(l>=0&&c+l<=e){c+=l;continue}if(a.has(r))throw new FormatError("Pages tree contains circular reference.");a.put(r);const h=await(o.get(r)||i.fetchAsync(r));if(h instanceof Dict){let t=h.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!h.has("Kids")){n.has(r)||n.put(r,1);s.has(r)||s.put(r,c);if(c===e)return[h,r];c++;continue}}t.push(h);continue}if(!(r instanceof Dict))throw new FormatError("Page dictionary kid reference points to wrong type of object.");const{objId:l}=r;let h=r.getRaw("Count");h instanceof Ref&&(h=await i.fetchAsync(h));if(Number.isInteger(h)&&h>=0){l&&!n.has(l)&&n.put(l,h);if(c+h<=e){c+=h;continue}}let u=r.getRaw("Kids");u instanceof Ref&&(u=await i.fetchAsync(u));if(!Array.isArray(u)){let t=r.getRaw("Type");t instanceof Ref&&(t=await i.fetchAsync(t));if(isName(t,"Page")||!r.has("Kids")){if(c===e)return[r,null];c++;continue}throw new FormatError("Page dictionary kids object is not an array.")}for(let e=u.length-1;e>=0;e--){const a=u[e];t.push(a);r===this.toplevelPagesDict&&a instanceof Ref&&!o.has(a)&&o.put(a,i.fetchAsync(a))}}throw new Error(`Page index ${e} not found.`)}async getAllPageDicts(e=!1){const{ignoreErrors:t}=this.pdfManager.evaluatorOptions,a=[{currentNode:this.toplevelPagesDict,posInKids:0}],r=new RefSet,i=this.#ie.getRaw("Pages");i instanceof Ref&&r.put(i);const n=new Map,s=this.xref,o=this.pageIndexCache;let c=0;function addPageDict(e,t){t&&!o.has(t)&&o.put(t,c);n.set(c++,[e,t])}function addPageError(a){if(a instanceof XRefEntryException&&!e)throw a;if(e&&t&&0===c){warn(`getAllPageDicts - Skipping invalid first page: "${a}".`);a=Dict.empty}n.set(c++,[a,null])}for(;a.length>0;){const e=a.at(-1),{currentNode:t,posInKids:i}=e;let n=t.getRaw("Kids");if(n instanceof Ref)try{n=await s.fetchAsync(n)}catch(e){addPageError(e);break}if(!Array.isArray(n)){addPageError(new FormatError("Page dictionary kids object is not an array."));break}if(i>=n.length){a.pop();continue}const o=n[i];let c;if(o instanceof Ref){if(r.has(o)){addPageError(new FormatError("Pages tree contains circular reference."));break}r.put(o);try{c=await s.fetchAsync(o)}catch(e){addPageError(e);break}}else c=o;if(!(c instanceof Dict)){addPageError(new FormatError("Page dictionary kid reference points to wrong type of object."));break}let l=c.getRaw("Type");if(l instanceof Ref)try{l=await s.fetchAsync(l)}catch(e){addPageError(e);break}isName(l,"Page")||!c.has("Kids")?addPageDict(c,o instanceof Ref?o:null):a.push({currentNode:c,posInKids:0});e.posInKids++}return n}getPageIndex(e){const t=this.pageIndexCache.get(e);if(void 0!==t)return Promise.resolve(t);const a=this.xref;let r=0;const next=t=>function pagesBeforeRef(t){let r,i=0;return a.fetchAsync(t).then((function(a){if(isRefsEqual(t,e)&&!isDict(a,"Page")&&!(a instanceof Dict&&!a.has("Type")&&a.has("Contents")))throw new FormatError("The reference does not point to a /Page dictionary.");if(!a)return null;if(!(a instanceof Dict))throw new FormatError("Node must be a dictionary.");r=a.getRaw("Parent");return a.getAsync("Parent")})).then((function(e){if(!e)return null;if(!(e instanceof Dict))throw new FormatError("Parent must be a dictionary.");return e.getAsync("Kids")})).then((function(e){if(!e)return null;const n=[];let s=!1;for(const r of e){if(!(r instanceof Ref))throw new FormatError("Kid must be a reference.");if(isRefsEqual(r,t)){s=!0;break}n.push(a.fetchAsync(r).then((function(e){if(!(e instanceof Dict))throw new FormatError("Kid node must be a dictionary.");e.has("Count")?i+=e.get("Count"):i++})))}if(!s)throw new FormatError("Kid reference not found in parent's kids.");return Promise.all(n).then((()=>[i,r]))}))}(t).then((t=>{if(!t){this.pageIndexCache.put(e,r);return r}const[a,i]=t;r+=a;return next(i)}));return next(e)}get baseUrl(){const e=this.#ie.get("URI");if(e instanceof Dict){const t=e.get("Base");if("string"==typeof t){const e=createValidAbsoluteUrl(t,null,{tryConvertEncoding:!0});if(e)return shadow(this,"baseUrl",e.href)}}return shadow(this,"baseUrl",this.pdfManager.docBaseUrl)}static parseDestDictionary({destDict:e,resultObj:t,docBaseUrl:a=null,docAttachments:r=null}){if(!(e instanceof Dict)){warn("parseDestDictionary: `destDict` must be a dictionary.");return}let i,n,s=e.get("A");if(!(s instanceof Dict))if(e.has("Dest"))s=e.get("Dest");else{s=e.get("AA");s instanceof Dict&&(s.has("D")?s=s.get("D"):s.has("U")&&(s=s.get("U")))}if(s instanceof Dict){const e=s.get("S");if(!(e instanceof Name)){warn("parseDestDictionary: Invalid type in Action dictionary.");return}const a=e.name;switch(a){case"ResetForm":const e=s.get("Flags"),o=!(1&("number"==typeof e?e:0)),c=[],l=[];for(const e of s.get("Fields")||[])e instanceof Ref?l.push(e.toString()):"string"==typeof e&&c.push(stringToPDFString(e));t.resetForm={fields:c,refs:l,include:o};break;case"URI":i=s.get("URI");i instanceof Name&&(i="/"+i.name);break;case"GoTo":n=s.get("D");break;case"Launch":case"GoToR":const h=s.get("F");if(h instanceof Dict){const e=new FileSpec(h,null,!0),{rawFilename:t}=e.serializable;i=t}else"string"==typeof h&&(i=h);const u=fetchRemoteDest(s);u&&"string"==typeof i&&(i=i.split("#",1)[0]+"#"+u);const d=s.get("NewWindow");"boolean"==typeof d&&(t.newWindow=d);break;case"GoToE":const f=s.get("T");let g;if(r&&f instanceof Dict){const e=f.get("R"),t=f.get("N");isName(e,"C")&&"string"==typeof t&&(g=r[stringToPDFString(t,!0)])}if(g){t.attachment=g;const e=fetchRemoteDest(s);e&&(t.attachmentDest=e)}else warn('parseDestDictionary - unimplemented "GoToE" action.');break;case"Named":const p=s.get("N");p instanceof Name&&(t.action=p.name);break;case"SetOCGState":const m=s.get("State"),b=s.get("PreserveRB");if(!Array.isArray(m)||0===m.length)break;const y=[];for(const e of m)if(e instanceof Name)switch(e.name){case"ON":case"OFF":case"Toggle":y.push(e.name)}else e instanceof Ref&&y.push(e.toString());if(y.length!==m.length)break;t.setOCGState={state:y,preserveRB:"boolean"!=typeof b||b};break;case"JavaScript":const w=s.get("JS");let S;w instanceof BaseStream?S=w.getString():"string"==typeof w&&(S=w);const x=S&&recoverJsURL(stringToPDFString(S,!0));if(x){i=x.url;t.newWindow=x.newWindow;break}default:if("JavaScript"===a||"SubmitForm"===a)break;warn(`parseDestDictionary - unsupported action: "${a}".`)}}else e.has("Dest")&&(n=e.get("Dest"));if("string"==typeof i){const e=createValidAbsoluteUrl(i,a,{addDefaultProtocol:!0,tryConvertEncoding:!0});e&&(t.url=e.href);t.unsafeUrl=i}if(n){n instanceof Name&&(n=n.name);"string"==typeof n?t.dest=stringToPDFString(n,!0):Vn(n)&&(t.dest=n)}}}function addChildren(e,t){if(e instanceof Dict)e=e.getRawValues();else if(e instanceof BaseStream)e=e.dict.getRawValues();else if(!Array.isArray(e))return;for(const r of e)((a=r)instanceof Ref||a instanceof Dict||a instanceof BaseStream||Array.isArray(a))&&t.push(r);var a}class ObjectLoader{refSet=new RefSet;constructor(e,t,a){this.dict=e;this.keys=t;this.xref=a}async load(){const{keys:e,dict:t}=this,a=[];for(const r of e){const e=t.getRaw(r);void 0!==e&&a.push(e)}await this.#ge(a);this.refSet=null}async#ge(e){const t=[],a=[];for(;e.length;){let r=e.pop();if(r instanceof Ref){if(this.refSet.has(r))continue;try{this.refSet.put(r);r=this.xref.fetch(r)}catch(e){if(!(e instanceof MissingDataException)){warn(`ObjectLoader.#walk - requesting all data: "${e}".`);await this.xref.stream.manager.requestAllChunks();return}t.push(r);a.push({begin:e.begin,end:e.end})}}if(r instanceof BaseStream){const e=r.getBaseStreams();if(e){let i=!1;for(const t of e)if(!t.isDataLoaded){i=!0;a.push({begin:t.start,end:t.end})}i&&t.push(r)}}addChildren(r,e)}if(a.length){await this.xref.stream.manager.requestRanges(a);for(const e of t)e instanceof Ref&&this.refSet.remove(e);await this.#ge(t)}}static async load(e,t,a){if(a.stream.isDataLoaded)return;const r=new ObjectLoader(e,t,a);await r.load()}}const Kn=Symbol(),Jn=Symbol(),Yn=Symbol(),Zn=Symbol(),Qn=Symbol(),es=Symbol(),ts=Symbol(),as=Symbol(),rs=Symbol(),is=Symbol("content"),ns=Symbol("data"),ss=Symbol(),os=Symbol("extra"),cs=Symbol(),ls=Symbol(),hs=Symbol(),us=Symbol(),ds=Symbol(),fs=Symbol(),gs=Symbol(),ps=Symbol(),ms=Symbol(),bs=Symbol(),ys=Symbol(),ws=Symbol(),Ss=Symbol(),xs=Symbol(),As=Symbol(),ks=Symbol(),Cs=Symbol(),vs=Symbol(),Fs=Symbol(),Is=Symbol(),Os=Symbol(),Ts=Symbol(),Ms=Symbol(),Ds=Symbol(),Bs=Symbol(),Rs=Symbol(),Ns=Symbol(),Es=Symbol(),Ls=Symbol(),js=Symbol(),_s=Symbol(),Us=Symbol(),Xs=Symbol("namespaceId"),qs=Symbol("nodeName"),Hs=Symbol(),Ws=Symbol(),zs=Symbol(),$s=Symbol(),Gs=Symbol(),Vs=Symbol(),Ks=Symbol(),Js=Symbol(),Ys=Symbol("root"),Zs=Symbol(),Qs=Symbol(),eo=Symbol(),to=Symbol(),ao=Symbol(),ro=Symbol(),io=Symbol(),no=Symbol(),so=Symbol(),oo=Symbol(),co=Symbol(),lo=Symbol("uid"),ho=Symbol(),uo={config:{id:0,check:e=>e.startsWith("http://www.xfa.org/schema/xci/")},connectionSet:{id:1,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-connection-set/")},datasets:{id:2,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-data/")},form:{id:3,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-form/")},localeSet:{id:4,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-locale-set/")},pdf:{id:5,check:e=>"http://ns.adobe.com/xdp/pdf/"===e},signature:{id:6,check:e=>"http://www.w3.org/2000/09/xmldsig#"===e},sourceSet:{id:7,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-source-set/")},stylesheet:{id:8,check:e=>"http://www.w3.org/1999/XSL/Transform"===e},template:{id:9,check:e=>e.startsWith("http://www.xfa.org/schema/xfa-template/")},xdc:{id:10,check:e=>e.startsWith("http://www.xfa.org/schema/xdc/")},xdp:{id:11,check:e=>"http://ns.adobe.com/xdp/"===e},xfdf:{id:12,check:e=>"http://ns.adobe.com/xfdf/"===e},xhtml:{id:13,check:e=>"http://www.w3.org/1999/xhtml"===e},xmpmeta:{id:14,check:e=>"http://ns.adobe.com/xmpmeta/"===e}},fo={pt:e=>e,cm:e=>e/2.54*72,mm:e=>e/25.4*72,in:e=>72*e,px:e=>e},go=/([+-]?\d+\.?\d*)(.*)/;function stripQuotes(e){return e.startsWith("'")||e.startsWith('"')?e.slice(1,-1):e}function getInteger({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseInt(e,10);return!isNaN(r)&&a(r)?r:t}function getFloat({data:e,defaultValue:t,validate:a}){if(!e)return t;e=e.trim();const r=parseFloat(e);return!isNaN(r)&&a(r)?r:t}function getKeyword({data:e,defaultValue:t,validate:a}){return e&&a(e=e.trim())?e:t}function getStringOption(e,t){return getKeyword({data:e,defaultValue:t[0],validate:e=>t.includes(e)})}function getMeasurement(e,t="0"){t||="0";if(!e)return getMeasurement(t);const a=e.trim().match(go);if(!a)return getMeasurement(t);const[,r,i]=a,n=parseFloat(r);if(isNaN(n))return getMeasurement(t);if(0===n)return 0;const s=fo[i];return s?s(n):n}function getRatio(e){if(!e)return{num:1,den:1};const t=e.split(":",2).map((e=>parseFloat(e.trim()))).filter((e=>!isNaN(e)));1===t.length&&t.push(1);if(0===t.length)return{num:1,den:1};const[a,r]=t;return{num:a,den:r}}function getRelevant(e){return e?e.trim().split(/\s+/).map((e=>({excluded:"-"===e[0],viewname:e.substring(1)}))):[]}class HTMLResult{static get FAILURE(){return shadow(this,"FAILURE",new HTMLResult(!1,null,null,null))}static get EMPTY(){return shadow(this,"EMPTY",new HTMLResult(!0,null,null,null))}constructor(e,t,a,r){this.success=e;this.html=t;this.bbox=a;this.breakNode=r}isBreak(){return!!this.breakNode}static breakNode(e){return new HTMLResult(!1,null,null,e)}static success(e,t=null){return new HTMLResult(!0,e,t,null)}}class FontFinder{constructor(e){this.fonts=new Map;this.cache=new Map;this.warned=new Set;this.defaultFont=null;this.add(e)}add(e,t=null){for(const t of e)this.addPdfFont(t);for(const e of this.fonts.values())e.regular||(e.regular=e.italic||e.bold||e.bolditalic);if(!t||0===t.size)return;const a=this.fonts.get("PdfJS-Fallback-PdfJS-XFA");for(const e of t)this.fonts.set(e,a)}addPdfFont(e){const t=e.cssFontInfo,a=t.fontFamily;let r=this.fonts.get(a);if(!r){r=Object.create(null);this.fonts.set(a,r);this.defaultFont||(this.defaultFont=r)}let i="";const n=parseFloat(t.fontWeight);0!==parseFloat(t.italicAngle)?i=n>=700?"bolditalic":"italic":n>=700&&(i="bold");if(!i){(e.name.includes("Bold")||e.psName?.includes("Bold"))&&(i="bold");(e.name.includes("Italic")||e.name.endsWith("It")||e.psName?.includes("Italic")||e.psName?.endsWith("It"))&&(i+="italic")}i||(i="regular");r[i]=e}getDefault(){return this.defaultFont}find(e,t=!0){let a=this.fonts.get(e)||this.cache.get(e);if(a)return a;const r=/,|-|_| |bolditalic|bold|italic|regular|it/gi;let i=e.replaceAll(r,"");a=this.fonts.get(i);if(a){this.cache.set(e,a);return a}i=i.toLowerCase();const n=[];for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t);if(0===n.length)for(const[,e]of this.fonts.entries())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(0===n.length){i=i.replaceAll(/psmt|mt/gi,"");for(const[e,t]of this.fonts.entries())e.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(t)}if(0===n.length)for(const e of this.fonts.values())e.regular.name?.replaceAll(r,"").toLowerCase().startsWith(i)&&n.push(e);if(n.length>=1){1!==n.length&&t&&warn(`XFA - Too many choices to guess the correct font: ${e}`);this.cache.set(e,n[0]);return n[0]}if(t&&!this.warned.has(e)){this.warned.add(e);warn(`XFA - Cannot find the font: ${e}`)}return null}}function selectFont(e,t){return"italic"===e.posture?"bold"===e.weight?t.bolditalic:t.italic:"bold"===e.weight?t.bold:t.regular}class FontInfo{constructor(e,t,a,r){this.lineHeight=a;this.paraMargin=t||{top:0,bottom:0,left:0,right:0};if(!e){[this.pdfFont,this.xfaFont]=this.defaultFont(r);return}this.xfaFont={typeface:e.typeface,posture:e.posture,weight:e.weight,size:e.size,letterSpacing:e.letterSpacing};const i=r.find(e.typeface);if(i){this.pdfFont=selectFont(e,i);this.pdfFont||([this.pdfFont,this.xfaFont]=this.defaultFont(r))}else[this.pdfFont,this.xfaFont]=this.defaultFont(r)}defaultFont(e){const t=e.find("Helvetica",!1)||e.find("Myriad Pro",!1)||e.find("Arial",!1)||e.getDefault();if(t?.regular){const e=t.regular;return[e,{typeface:e.cssFontInfo.fontFamily,posture:"normal",weight:"normal",size:10,letterSpacing:0}]}return[null,{typeface:"Courier",posture:"normal",weight:"normal",size:10,letterSpacing:0}]}}class FontSelector{constructor(e,t,a,r){this.fontFinder=r;this.stack=[new FontInfo(e,t,a,r)]}pushData(e,t,a){const r=this.stack.at(-1);for(const t of["typeface","posture","weight","size","letterSpacing"])e[t]||(e[t]=r.xfaFont[t]);for(const e of["top","bottom","left","right"])isNaN(t[e])&&(t[e]=r.paraMargin[e]);const i=new FontInfo(e,t,a||r.lineHeight,this.fontFinder);i.pdfFont||(i.pdfFont=r.pdfFont);this.stack.push(i)}popFont(){this.stack.pop()}topFont(){return this.stack.at(-1)}}class TextMeasure{constructor(e,t,a,r){this.glyphs=[];this.fontSelector=new FontSelector(e,t,a,r);this.extraHeight=0}pushData(e,t,a){this.fontSelector.pushData(e,t,a)}popFont(e){return this.fontSelector.popFont()}addPara(){const e=this.fontSelector.topFont();this.extraHeight+=e.paraMargin.top+e.paraMargin.bottom}addString(e){if(!e)return;const t=this.fontSelector.topFont(),a=t.xfaFont.size;if(t.pdfFont){const r=t.xfaFont.letterSpacing,i=t.pdfFont,n=i.lineHeight||1.2,s=t.lineHeight||Math.max(1.2,n)*a,o=n-(void 0===i.lineGap?.2:i.lineGap),c=Math.max(1,o)*a,l=a/1e3,h=i.defaultWidth||i.charsToGlyphs(" ")[0].width;for(const t of e.split(/[\u2029\n]/)){const e=i.encodeString(t).join(""),a=i.charsToGlyphs(e);for(const e of a){const t=e.width||h;this.glyphs.push([t*l+r,s,c,e.unicode,!1])}this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}else{for(const t of e.split(/[\u2029\n]/)){for(const e of t.split(""))this.glyphs.push([a,1.2*a,a,e,!1]);this.glyphs.push([0,0,0,"\n",!0])}this.glyphs.pop()}}compute(e){let t=-1,a=0,r=0,i=0,n=0,s=0,o=!1,c=!0;for(let l=0,h=this.glyphs.length;le){r=Math.max(r,n);n=0;i+=s;s=m;t=-1;a=0;o=!0;c=!1}else{s=Math.max(m,s);a=n;n+=h;t=l}else if(n+h>e){i+=s;s=m;if(-1!==t){l=t;r=Math.max(r,a);n=0;t=-1;a=0}else{r=Math.max(r,n);n=h}o=!0;c=!1}else{n+=h;s=Math.max(m,s)}}r=Math.max(r,n);i+=s+this.extraHeight;return{width:1.02*r,height:i,isBroken:o}}}const po=/^[^.[]+/,mo=/^[^\]]+/,bo=0,yo=1,wo=2,So=3,xo=4,Ao=new Map([["$data",(e,t)=>e.datasets?e.datasets.data:e],["$record",(e,t)=>(e.datasets?e.datasets.data:e)[ws]()[0]],["$template",(e,t)=>e.template],["$connectionSet",(e,t)=>e.connectionSet],["$form",(e,t)=>e.form],["$layout",(e,t)=>e.layout],["$host",(e,t)=>e.host],["$dataWindow",(e,t)=>e.dataWindow],["$event",(e,t)=>e.event],["!",(e,t)=>e.datasets],["$xfa",(e,t)=>e],["xfa",(e,t)=>e],["$",(e,t)=>t]]),ko=new WeakMap;function parseExpression(e,t,a=!0){let r=e.match(po);if(!r)return null;let[i]=r;const n=[{name:i,cacheName:"."+i,index:0,js:null,formCalc:null,operator:bo}];let s=i.length;for(;s0&&h.push(e)}if(0!==h.length||o||0!==c)e=isFinite(l)?h.filter((e=>le[l])):h.flat();else{const a=t[ks]();if(!(t=a))return null;c=-1;e=[t]}}return 0===e.length?null:e}function createDataNode(e,t,a){const r=parseExpression(a);if(!r)return null;if(r.some((e=>e.operator===yo)))return null;const i=Ao.get(r[0].name);let n=0;if(i){e=i(e,t);n=1}else e=t||e;for(let t=r.length;ne[io]())).join("")}get[Fo](){const e=Object.getPrototypeOf(this);if(!e._attributes){const t=e._attributes=new Set;for(const e of Object.getOwnPropertyNames(this)){if(null===this[e]||this[e]instanceof XFAObject||this[e]instanceof XFAObjectArray)break;t.add(e)}}return shadow(this,Fo,e._attributes)}[Rs](e){let t=this;for(;t;){if(t===e)return!0;t=t[ks]()}return!1}[ks](){return this[jo]}[As](){return this[ks]()}[ws](e=null){return e?this[e]:this[Io]}[ss](){const e=Object.create(null);this[is]&&(e.$content=this[is]);for(const t of Object.getOwnPropertyNames(this)){const a=this[t];null!==a&&(a instanceof XFAObject?e[t]=a[ss]():a instanceof XFAObjectArray?a.isEmpty()||(e[t]=a.dump()):e[t]=a)}return e}[co](){return null}[so](){return HTMLResult.EMPTY}*[Ss](){for(const e of this[ws]())yield e}*[Bo](e,t){for(const a of this[Ss]())if(!e||t===e.has(a[qs])){const e=this[ds](),t=a[so](e);t.success||(this[os].failingNode=a);yield t}}[ls](){return null}[Jn](e,t){this[os].children.push(e)}[ds](){}[Zn]({filter:e=null,include:t=!0}){if(this[os].generator){const e=this[ds](),t=this[os].failingNode[so](e);if(!t.success)return t;t.html&&this[Jn](t.html,t.bbox);delete this[os].failingNode}else this[os].generator=this[Bo](e,t);for(;;){const e=this[os].generator.next();if(e.done)break;const t=e.value;if(!t.success)return t;t.html&&this[Jn](t.html,t.bbox)}this[os].generator=null;return HTMLResult.EMPTY}[to](e){this[Uo]=new Set(Object.keys(e))}[No](e){const t=this[Fo],a=this[Uo];return[...e].filter((e=>t.has(e)&&!a.has(e)))}[Zs](e,t=new Set){for(const a of this[Io])a[_o](e,t)}[_o](e,t){const a=this[Ro](e,t);a?this[Co](a,e,t):this[Zs](e,t)}[Ro](e,t){const{use:a,usehref:r}=this;if(!a&&!r)return null;let i=null,n=null,s=null,o=a;if(r){o=r;r.startsWith("#som(")&&r.endsWith(")")?n=r.slice(5,-1):r.startsWith(".#som(")&&r.endsWith(")")?n=r.slice(6,-1):r.startsWith("#")?s=r.slice(1):r.startsWith(".#")&&(s=r.slice(2))}else a.startsWith("#")?s=a.slice(1):n=a;this.use=this.usehref="";if(s)i=e.get(s);else{i=searchNode(e.get(Ys),this,n,!0,!1);i&&(i=i[0])}if(!i){warn(`XFA - Invalid prototype reference: ${o}.`);return null}if(i[qs]!==this[qs]){warn(`XFA - Incompatible prototype: ${i[qs]} !== ${this[qs]}.`);return null}if(t.has(i)){warn("XFA - Cycle detected in prototypes use.");return null}t.add(i);const c=i[Ro](e,t);c&&i[Co](c,e,t);i[Zs](e,t);t.delete(i);return i}[Co](e,t,a){if(a.has(e)){warn("XFA - Cycle detected in prototypes use.");return}!this[is]&&e[is]&&(this[is]=e[is]);new Set(a).add(e);for(const t of this[No](e[Uo])){this[t]=e[t];this[Uo]&&this[Uo].add(t)}for(const r of Object.getOwnPropertyNames(this)){if(this[Fo].has(r))continue;const i=this[r],n=e[r];if(i instanceof XFAObjectArray){for(const e of i[Io])e[_o](t,a);for(let r=i[Io].length,s=n[Io].length;rXFAObject[Oo](e))):"object"==typeof e&&null!==e?Object.assign({},e):e}[as](){const e=Object.create(Object.getPrototypeOf(this));for(const t of Object.getOwnPropertySymbols(this))try{e[t]=this[t]}catch{shadow(e,t,this[t])}e[lo]=`${e[qs]}${qo++}`;e[Io]=[];for(const t of Object.getOwnPropertyNames(this)){if(this[Fo].has(t)){e[t]=XFAObject[Oo](this[t]);continue}const a=this[t];e[t]=a instanceof XFAObjectArray?new XFAObjectArray(a[Po]):null}for(const t of this[Io]){const a=t[qs],r=t[as]();e[Io].push(r);r[jo]=e;null===e[a]?e[a]=r:e[a][Io].push(r)}return e}[ws](e=null){return e?this[Io].filter((t=>t[qs]===e)):this[Io]}[fs](e){return this[e]}[gs](e,t,a=!0){return Array.from(this[ps](e,t,a))}*[ps](e,t,a=!0){if("parent"!==e){for(const a of this[Io]){a[qs]===e&&(yield a);a.name===e&&(yield a);(t||a[js]())&&(yield*a[ps](e,t,!1))}a&&this[Fo].has(e)&&(yield new XFAAttribute(this,e,this[e]))}else yield this[jo]}}class XFAObjectArray{constructor(e=1/0){this[Po]=e;this[Io]=[]}get isXFAObject(){return!1}get isXFAObjectArray(){return!0}push(e){if(this[Io].length<=this[Po]){this[Io].push(e);return!0}warn(`XFA - node "${e[qs]}" accepts no more than ${this[Po]} children`);return!1}isEmpty(){return 0===this[Io].length}dump(){return 1===this[Io].length?this[Io][0][ss]():this[Io].map((e=>e[ss]()))}[as](){const e=new XFAObjectArray(this[Po]);e[Io]=this[Io].map((e=>e[as]()));return e}get children(){return this[Io]}clear(){this[Io].length=0}}class XFAAttribute{constructor(e,t,a){this[jo]=e;this[qs]=t;this[is]=a;this[rs]=!1;this[lo]="attribute"+qo++}[ks](){return this[jo]}[Bs](){return!0}[ms](){return this[is].trim()}[ao](e){e=e.value||"";this[is]=e.toString()}[io](){return this[is]}[Rs](e){return this[jo]===e||this[jo][Rs](e)}}class XmlObject extends XFAObject{constructor(e,t,a={}){super(e,t);this[is]="";this[Mo]=null;if("#text"!==t){const e=new Map;this[vo]=e;for(const[t,r]of Object.entries(a))e.set(t,new XFAAttribute(this,t,r));if(a.hasOwnProperty(Hs)){const e=a[Hs].xfa.dataNode;void 0!==e&&("dataGroup"===e?this[Mo]=!1:"dataValue"===e&&(this[Mo]=!0))}}this[rs]=!1}[oo](e){const t=this[qs];if("#text"===t){e.push(encodeToXmlString(this[is]));return}const a=utf8StringToString(t),r=this[Xs]===Ho?"xfa:":"";e.push(`<${r}${a}`);for(const[t,a]of this[vo].entries()){const r=utf8StringToString(t);e.push(` ${r}="${encodeToXmlString(a[is])}"`)}null!==this[Mo]&&(this[Mo]?e.push(' xfa:dataNode="dataValue"'):e.push(' xfa:dataNode="dataGroup"'));if(this[is]||0!==this[Io].length){e.push(">");if(this[is])"string"==typeof this[is]?e.push(encodeToXmlString(this[is])):this[is][oo](e);else for(const t of this[Io])t[oo](e);e.push(``)}else e.push("/>")}[Ws](e){if(this[is]){const e=new XmlObject(this[Xs],"#text");this[Yn](e);e[is]=this[is];this[is]=""}this[Yn](e);return!0}[$s](e){this[is]+=e}[cs](){if(this[is]&&this[Io].length>0){const e=new XmlObject(this[Xs],"#text");this[Yn](e);e[is]=this[is];delete this[is]}}[so](){return"#text"===this[qs]?HTMLResult.success({name:"#text",value:this[is]}):HTMLResult.EMPTY}[ws](e=null){return e?this[Io].filter((t=>t[qs]===e)):this[Io]}[us](){return this[vo]}[fs](e){const t=this[vo].get(e);return void 0!==t?t:this[ws](e)}*[ps](e,t){const a=this[vo].get(e);a&&(yield a);for(const a of this[Io]){a[qs]===e&&(yield a);t&&(yield*a[ps](e,t))}}*[hs](e,t){const a=this[vo].get(e);!a||t&&a[rs]||(yield a);for(const a of this[Io])yield*a[hs](e,t)}*[ys](e,t,a){for(const r of this[Io]){r[qs]!==e||a&&r[rs]||(yield r);t&&(yield*r[ys](e,t,a))}}[Bs](){return null===this[Mo]?0===this[Io].length||this[Io][0][Xs]===uo.xhtml.id:this[Mo]}[ms](){return null===this[Mo]?0===this[Io].length?this[is].trim():this[Io][0][Xs]===uo.xhtml.id?this[Io][0][io]().trim():null:this[is].trim()}[ao](e){e=e.value||"";this[is]=e.toString()}[ss](e=!1){const t=Object.create(null);e&&(t.$ns=this[Xs]);this[is]&&(t.$content=this[is]);t.$name=this[qs];t.children=[];for(const a of this[Io])t.children.push(a[ss](e));t.attributes=Object.create(null);for(const[e,a]of this[vo])t.attributes[e]=a[is];return t}}class ContentObject extends XFAObject{constructor(e,t){super(e,t);this[is]=""}[$s](e){this[is]+=e}[cs](){}}class OptionObject extends ContentObject{constructor(e,t,a){super(e,t);this[Lo]=a}[cs](){this[is]=getKeyword({data:this[is],defaultValue:this[Lo][0],validate:e=>this[Lo].includes(e)})}[Qn](e){super[Qn](e);delete this[Lo]}}class StringObject extends ContentObject{[cs](){this[is]=this[is].trim()}}class IntegerObject extends ContentObject{constructor(e,t,a,r){super(e,t);this[Do]=a;this[Xo]=r}[cs](){this[is]=getInteger({data:this[is],defaultValue:this[Do],validate:this[Xo]})}[Qn](e){super[Qn](e);delete this[Do];delete this[Xo]}}class Option01 extends IntegerObject{constructor(e,t){super(e,t,0,(e=>1===e))}}class Option10 extends IntegerObject{constructor(e,t){super(e,t,1,(e=>0===e))}}function measureToString(e){return"string"==typeof e?"0px":Number.isInteger(e)?`${e}px`:`${e.toFixed(2)}px`}const Wo={anchorType(e,t){const a=e[As]();if(a&&(!a.layout||"position"===a.layout)){"transform"in t||(t.transform="");switch(e.anchorType){case"bottomCenter":t.transform+="translate(-50%, -100%)";break;case"bottomLeft":t.transform+="translate(0,-100%)";break;case"bottomRight":t.transform+="translate(-100%,-100%)";break;case"middleCenter":t.transform+="translate(-50%,-50%)";break;case"middleLeft":t.transform+="translate(0,-50%)";break;case"middleRight":t.transform+="translate(-100%,-50%)";break;case"topCenter":t.transform+="translate(-50%,0)";break;case"topRight":t.transform+="translate(-100%,0)"}}},dimensions(e,t){const a=e[As]();let r=e.w;const i=e.h;if(a.layout?.includes("row")){const t=a[os],i=e.colSpan;let n;if(-1===i){n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn));t.currentColumn=0}else{n=Math.sumPrecise(t.columnWidths.slice(t.currentColumn,t.currentColumn+i));t.currentColumn=(t.currentColumn+e.colSpan)%t.columnWidths.length}isNaN(n)||(r=e.w=n)}t.width=""!==r?measureToString(r):"auto";t.height=""!==i?measureToString(i):"auto"},position(e,t){const a=e[As]();if(!a?.layout||"position"===a.layout){t.position="absolute";t.left=measureToString(e.x);t.top=measureToString(e.y)}},rotate(e,t){if(e.rotate){"transform"in t||(t.transform="");t.transform+=`rotate(-${e.rotate}deg)`;t.transformOrigin="top left"}},presence(e,t){switch(e.presence){case"invisible":t.visibility="hidden";break;case"hidden":case"inactive":t.display="none"}},hAlign(e,t){if("para"===e[qs])switch(e.hAlign){case"justifyAll":t.textAlign="justify-all";break;case"radix":t.textAlign="left";break;default:t.textAlign=e.hAlign}else switch(e.hAlign){case"left":t.alignSelf="start";break;case"center":t.alignSelf="center";break;case"right":t.alignSelf="end"}},margin(e,t){e.margin&&(t.margin=e.margin[co]().margin)}};function setMinMaxDimensions(e,t){if("position"===e[As]().layout){e.minW>0&&(t.minWidth=measureToString(e.minW));e.maxW>0&&(t.maxWidth=measureToString(e.maxW));e.minH>0&&(t.minHeight=measureToString(e.minH));e.maxH>0&&(t.maxHeight=measureToString(e.maxH))}}function layoutText(e,t,a,r,i,n){const s=new TextMeasure(t,a,r,i);"string"==typeof e?s.addString(e):e[Gs](s);return s.compute(n)}function layoutNode(e,t){let a=null,r=null,i=!1;if((!e.w||!e.h)&&e.value){let n=0,s=0;if(e.margin){n=e.margin.leftInset+e.margin.rightInset;s=e.margin.topInset+e.margin.bottomInset}let o=null,c=null;if(e.para){c=Object.create(null);o=""===e.para.lineHeight?null:e.para.lineHeight;c.top=""===e.para.spaceAbove?0:e.para.spaceAbove;c.bottom=""===e.para.spaceBelow?0:e.para.spaceBelow;c.left=""===e.para.marginLeft?0:e.para.marginLeft;c.right=""===e.para.marginRight?0:e.para.marginRight}let l=e.font;if(!l){const t=e[Cs]();let a=e[ks]();for(;a&&a!==t;){if(a.font){l=a.font;break}a=a[ks]()}}const h=(e.w||t.width)-n,u=e[vs].fontFinder;if(e.value.exData&&e.value.exData[is]&&"text/html"===e.value.exData.contentType){const t=layoutText(e.value.exData[is],l,c,o,u,h);r=t.width;a=t.height;i=t.isBroken}else{const t=e.value[io]();if(t){const e=layoutText(t,l,c,o,u,h);r=e.width;a=e.height;i=e.isBroken}}null===r||e.w||(r+=n);null===a||e.h||(a+=s)}return{w:r,h:a,isBroken:i}}function computeBbox(e,t,a){let r;if(""!==e.w&&""!==e.h)r=[e.x,e.y,e.w,e.h];else{if(!a)return null;let i=e.w;if(""===i){if(0===e.maxW){const t=e[As]();i="position"===t.layout&&""!==t.w?0:e.minW}else i=Math.min(e.maxW,a.width);t.attributes.style.width=measureToString(i)}let n=e.h;if(""===n){if(0===e.maxH){const t=e[As]();n="position"===t.layout&&""!==t.h?0:e.minH}else n=Math.min(e.maxH,a.height);t.attributes.style.height=measureToString(n)}r=[e.x,e.y,i,n]}return r}function fixDimensions(e){const t=e[As]();if(t.layout?.includes("row")){const a=t[os],r=e.colSpan;let i;i=-1===r?Math.sumPrecise(a.columnWidths.slice(a.currentColumn)):Math.sumPrecise(a.columnWidths.slice(a.currentColumn,a.currentColumn+r));isNaN(i)||(e.w=i)}t.layout&&"position"!==t.layout&&(e.x=e.y=0);"table"===e.layout&&""===e.w&&Array.isArray(e.columnWidths)&&(e.w=Math.sumPrecise(e.columnWidths))}function layoutClass(e){switch(e.layout){case"position":default:return"xfaPosition";case"lr-tb":return"xfaLrTb";case"rl-row":return"xfaRlRow";case"rl-tb":return"xfaRlTb";case"row":return"xfaRow";case"table":return"xfaTable";case"tb":return"xfaTb"}}function toStyle(e,...t){const a=Object.create(null);for(const r of t){const t=e[r];if(null!==t)if(Wo.hasOwnProperty(r))Wo[r](e,a);else if(t instanceof XFAObject){const e=t[co]();e?Object.assign(a,e):warn(`(DEBUG) - XFA - style for ${r} not implemented yet`)}}return a}function createWrapper(e,t){const{attributes:a}=t,{style:r}=a,i={name:"div",attributes:{class:["xfaWrapper"],style:Object.create(null)},children:[]};a.class.push("xfaWrapped");if(e.border){const{widths:a,insets:n}=e.border[os];let s,o,c=n[0],l=n[3];const h=n[0]+n[2],u=n[1]+n[3];switch(e.border.hand){case"even":c-=a[0]/2;l-=a[3]/2;s=`calc(100% + ${(a[1]+a[3])/2-u}px)`;o=`calc(100% + ${(a[0]+a[2])/2-h}px)`;break;case"left":c-=a[0];l-=a[3];s=`calc(100% + ${a[1]+a[3]-u}px)`;o=`calc(100% + ${a[0]+a[2]-h}px)`;break;case"right":s=u?`calc(100% - ${u}px)`:"100%";o=h?`calc(100% - ${h}px)`:"100%"}const d=["xfaBorder"];isPrintOnly(e.border)&&d.push("xfaPrintOnly");const f={name:"div",attributes:{class:d,style:{top:`${c}px`,left:`${l}px`,width:s,height:o}},children:[]};for(const e of["border","borderWidth","borderColor","borderRadius","borderStyle"])if(void 0!==r[e]){f.attributes.style[e]=r[e];delete r[e]}i.children.push(f,t)}else i.children.push(t);for(const e of["background","backgroundClip","top","left","width","height","minWidth","minHeight","maxWidth","maxHeight","transform","transformOrigin","visibility"])if(void 0!==r[e]){i.attributes.style[e]=r[e];delete r[e]}i.attributes.style.position="absolute"===r.position?"absolute":"relative";delete r.position;if(r.alignSelf){i.attributes.style.alignSelf=r.alignSelf;delete r.alignSelf}return i}function fixTextIndent(e){const t=getMeasurement(e.textIndent,"0px");if(t>=0)return;const a="padding"+("left"===("right"===e.textAlign?"right":"left")?"Left":"Right"),r=getMeasurement(e[a],"0px");e[a]=r-t+"px"}function setAccess(e,t){switch(e.access){case"nonInteractive":t.push("xfaNonInteractive");break;case"readOnly":t.push("xfaReadOnly");break;case"protected":t.push("xfaDisabled")}}function isPrintOnly(e){return e.relevant.length>0&&!e.relevant[0].excluded&&"print"===e.relevant[0].viewname}function getCurrentPara(e){const t=e[Cs]()[os].paraStack;return t.length?t.at(-1):null}function setPara(e,t,a){if(a.attributes.class?.includes("xfaRich")){if(t){""===e.h&&(t.height="auto");""===e.w&&(t.width="auto")}const r=getCurrentPara(e);if(r){const e=a.attributes.style;e.display="flex";e.flexDirection="column";switch(r.vAlign){case"top":e.justifyContent="start";break;case"bottom":e.justifyContent="end";break;case"middle":e.justifyContent="center"}const t=r[co]();for(const[a,r]of Object.entries(t))a in e||(e[a]=r)}}}function setFontFamily(e,t,a,r){if(!a){delete r.fontFamily;return}const i=stripQuotes(e.typeface);r.fontFamily=`"${i}"`;const n=a.find(i);if(n){const{fontFamily:a}=n.regular.cssFontInfo;a!==i&&(r.fontFamily=`"${a}"`);const s=getCurrentPara(t);if(s&&""!==s.lineHeight)return;if(r.lineHeight)return;const o=selectFont(e,n);o&&(r.lineHeight=Math.max(1.2,o.lineHeight))}}function fixURL(e){const t=createValidAbsoluteUrl(e,null,{addDefaultProtocol:!0,tryConvertEncoding:!0});return t?t.href:null}function createLine(e,t){return{name:"div",attributes:{class:["lr-tb"===e.layout?"xfaLr":"xfaRl"]},children:t}}function flushHTML(e){if(!e[os])return null;const t={name:"div",attributes:e[os].attributes,children:e[os].children};if(e[os].failingNode){const a=e[os].failingNode[ls]();a&&(e.layout.endsWith("-tb")?t.children.push(createLine(e,[a])):t.children.push(a))}return 0===t.children.length?null:t}function addHTML(e,t,a){const r=e[os],i=r.availableSpace,[n,s,o,c]=a;switch(e.layout){case"position":r.width=Math.max(r.width,n+o);r.height=Math.max(r.height,s+c);r.children.push(t);break;case"lr-tb":case"rl-tb":if(!r.line||1===r.attempt){r.line=createLine(e,[]);r.children.push(r.line);r.numberInLine=0}r.numberInLine+=1;r.line.children.push(t);if(0===r.attempt){r.currentWidth+=o;r.height=Math.max(r.height,r.prevHeight+c)}else{r.currentWidth=o;r.prevHeight=r.height;r.height+=c;r.attempt=0}r.width=Math.max(r.width,r.currentWidth);break;case"rl-row":case"row":{r.children.push(t);r.width+=o;r.height=Math.max(r.height,c);const e=measureToString(r.height);for(const t of r.children)t.attributes.style.height=e;break}case"table":case"tb":r.width=MathClamp(o,r.width,i.width);r.height+=c;r.children.push(t)}}function getAvailableSpace(e){const t=e[os].availableSpace,a=e.margin?e.margin.topInset+e.margin.bottomInset:0,r=e.margin?e.margin.leftInset+e.margin.rightInset:0;switch(e.layout){case"lr-tb":case"rl-tb":return 0===e[os].attempt?{width:t.width-r-e[os].currentWidth,height:t.height-a-e[os].prevHeight}:{width:t.width-r,height:t.height-a-e[os].height};case"rl-row":case"row":return{width:Math.sumPrecise(e[os].columnWidths.slice(e[os].currentColumn)),height:t.height-r};case"table":case"tb":return{width:t.width-r,height:t.height-a-e[os].height};default:return t}}function checkDimensions(e,t){if(null===e[Cs]()[os].firstUnsplittable)return!0;if(0===e.w||0===e.h)return!0;const a=e[As](),r=a[os]?.attempt||0,[,i,n,s]=function getTransformedBBox(e){let t,a,r=""===e.w?NaN:e.w,i=""===e.h?NaN:e.h,[n,s]=[0,0];switch(e.anchorType||""){case"bottomCenter":[n,s]=[r/2,i];break;case"bottomLeft":[n,s]=[0,i];break;case"bottomRight":[n,s]=[r,i];break;case"middleCenter":[n,s]=[r/2,i/2];break;case"middleLeft":[n,s]=[0,i/2];break;case"middleRight":[n,s]=[r,i/2];break;case"topCenter":[n,s]=[r/2,0];break;case"topRight":[n,s]=[r,0]}switch(e.rotate||0){case 0:[t,a]=[-n,-s];break;case 90:[t,a]=[-s,n];[r,i]=[i,-r];break;case 180:[t,a]=[n,s];[r,i]=[-r,-i];break;case 270:[t,a]=[s,-n];[r,i]=[-i,r]}return[e.x+t+Math.min(0,r),e.y+a+Math.min(0,i),Math.abs(r),Math.abs(i)]}(e);switch(a.layout){case"lr-tb":case"rl-tb":return 0===r?e[Cs]()[os].noLayoutFailure?""!==e.w?Math.round(n-t.width)<=2:t.width>2:!(""!==e.h&&Math.round(s-t.height)>2)&&(""!==e.w?Math.round(n-t.width)<=2||0===a[os].numberInLine&&t.height>2:t.width>2):!!e[Cs]()[os].noLayoutFailure||!(""!==e.h&&Math.round(s-t.height)>2)&&((""===e.w||Math.round(n-t.width)<=2||!a[Ls]())&&t.height>2);case"table":case"tb":return!!e[Cs]()[os].noLayoutFailure||(""===e.h||e[Es]()?(""===e.w||Math.round(n-t.width)<=2||!a[Ls]())&&t.height>2:Math.round(s-t.height)<=2);case"position":if(e[Cs]()[os].noLayoutFailure)return!0;if(""===e.h||Math.round(s+i-t.height)<=2)return!0;return s+i>e[Cs]()[os].currentContentArea.h;case"rl-row":case"row":return!!e[Cs]()[os].noLayoutFailure||(""===e.h||Math.round(s-t.height)<=2);default:return!0}}const zo=uo.template.id,$o="http://www.w3.org/2000/svg",Go=/^H(\d+)$/,Vo=new Set(["image/gif","image/jpeg","image/jpg","image/pjpeg","image/png","image/apng","image/x-png","image/bmp","image/x-ms-bmp","image/tiff","image/tif","application/octet-stream"]),Ko=[[[66,77],"image/bmp"],[[255,216,255],"image/jpeg"],[[73,73,42,0],"image/tiff"],[[77,77,0,42],"image/tiff"],[[71,73,70,56,57,97],"image/gif"],[[137,80,78,71,13,10,26,10],"image/png"]];function getBorderDims(e){if(!e||!e.border)return{w:0,h:0};const t=e.border[bs]();return t?{w:t.widths[0]+t.widths[2]+t.insets[0]+t.insets[2],h:t.widths[1]+t.widths[3]+t.insets[1]+t.insets[3]}:{w:0,h:0}}function hasMargin(e){return e.margin&&(e.margin.topInset||e.margin.rightInset||e.margin.bottomInset||e.margin.leftInset)}function _setValue(e,t){if(!e.value){const t=new Value({});e[Yn](t);e.value=t}e.value[ao](t)}function*getContainedChildren(e){for(const t of e[ws]())t instanceof SubformSet?yield*t[Ss]():yield t}function isRequired(e){return"error"===e.validate?.nullTest}function setTabIndex(e){for(;e;){if(!e.traversal){e[ro]=e[ks]()[ro];return}if(e[ro])return;let t=null;for(const a of e.traversal[ws]())if("next"===a.operation){t=a;break}if(!t||!t.ref){e[ro]=e[ks]()[ro];return}const a=e[Cs]();e[ro]=++a[ro];const r=a[Qs](t.ref,e);if(!r)return;e=r[0]}}function applyAssist(e,t){const a=e.assist;if(a){const e=a[so]();e&&(t.title=e);const r=a.role.match(Go);if(r){const e="heading",a=r[1];t.role=e;t["aria-level"]=a}}if("table"===e.layout)t.role="table";else if("row"===e.layout)t.role="row";else{const a=e[ks]();"row"===a.layout&&(t.role="TH"===a.assist?.role?"columnheader":"cell")}}function ariaLabel(e){if(!e.assist)return null;const t=e.assist;return t.speak&&""!==t.speak[is]?t.speak[is]:t.toolTip?t.toolTip[is]:null}function valueToHtml(e){return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:Object.create(null)},children:[{name:"span",attributes:{style:Object.create(null)},value:e}]})}function setFirstUnsplittable(e){const t=e[Cs]();if(null===t[os].firstUnsplittable){t[os].firstUnsplittable=e;t[os].noLayoutFailure=!0}}function unsetFirstUnsplittable(e){const t=e[Cs]();t[os].firstUnsplittable===e&&(t[os].noLayoutFailure=!1)}function handleBreak(e){if(e[os])return!1;e[os]=Object.create(null);if("auto"===e.targetType)return!1;const t=e[Cs]();let a=null;if(e.target){a=t[Qs](e.target,e[ks]());if(!a)return!1;a=a[0]}const{currentPageArea:r,currentContentArea:i}=t[os];if("pageArea"===e.targetType){a instanceof PageArea||(a=null);if(e.startNew){e[os].target=a||r;return!0}if(a&&a!==r){e[os].target=a;return!0}return!1}a instanceof ContentArea||(a=null);const n=a&&a[ks]();let s,o=n;if(e.startNew)if(a){const e=n.contentArea.children,t=e.indexOf(i),r=e.indexOf(a);-1!==t&&te;r[os].noLayoutFailure=!0;const s=t[so](a);e[Jn](s.html,s.bbox);r[os].noLayoutFailure=i;t[As]=n}class AppearanceFilter extends StringObject{constructor(e){super(zo,"appearanceFilter");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Arc extends XFAObject{constructor(e){super(zo,"arc",!0);this.circular=getInteger({data:e.circular,defaultValue:0,validate:e=>1===e});this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.startAngle=getFloat({data:e.startAngle,defaultValue:0,validate:e=>!0});this.sweepAngle=getFloat({data:e.sweepAngle,defaultValue:360,validate:e=>!0});this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null;this.fill=null}[so](){const e=this.edge||new Edge({}),t=e[co](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[co]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;let r;const i={xmlns:$o,style:{width:"100%",height:"100%",overflow:"visible"}};if(360===this.sweepAngle)r={name:"ellipse",attributes:{xmlns:$o,cx:"50%",cy:"50%",rx:"50%",ry:"50%",style:a}};else{const e=this.startAngle*Math.PI/180,t=this.sweepAngle*Math.PI/180,n=this.sweepAngle>180?1:0,[s,o,c,l]=[50*(1+Math.cos(e)),50*(1-Math.sin(e)),50*(1+Math.cos(e+t)),50*(1-Math.sin(e+t))];r={name:"path",attributes:{xmlns:$o,d:`M ${s} ${o} A 50 50 0 ${n} 0 ${c} ${l}`,vectorEffect:"non-scaling-stroke",style:a}};Object.assign(i,{viewBox:"0 0 100 100",preserveAspectRatio:"none"})}const n={name:"svg",children:[r],attributes:i};if(hasMargin(this[ks]()[ks]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[n]});n.attributes.style.position="absolute";return HTMLResult.success(n)}}class Area extends XFAObject{constructor(e){super(zo,"area",!0);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null;this.area=new XFAObjectArray;this.draw=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[Ss](){yield*getContainedChildren(this)}[js](){return!0}[Ds](){return!0}[Jn](e,t){const[a,r,i,n]=t;this[os].width=Math.max(this[os].width,a+i);this[os].height=Math.max(this[os].height,r+n);this[os].children.push(e)}[ds](){return this[os].availableSpace}[so](e){const t=toStyle(this,"position"),a={style:t,id:this[lo],class:["xfaArea"]};isPrintOnly(this)&&a.class.push("xfaPrintOnly");this.name&&(a.xfaName=this.name);const r=[];this[os]={children:r,width:0,height:0,availableSpace:e};const i=this[Zn]({filter:new Set(["area","draw","field","exclGroup","subform","subformSet"]),include:!0});if(!i.success){if(i.isBreak())return i;delete this[os];return HTMLResult.FAILURE}t.width=measureToString(this[os].width);t.height=measureToString(this[os].height);const n={name:"div",attributes:a,children:r},s=[this.x,this.y,this[os].width,this[os].height];delete this[os];return HTMLResult.success(n,s)}}class Assist extends XFAObject{constructor(e){super(zo,"assist",!0);this.id=e.id||"";this.role=e.role||"";this.use=e.use||"";this.usehref=e.usehref||"";this.speak=null;this.toolTip=null}[so](){return this.toolTip?.[is]||null}}class Barcode extends XFAObject{constructor(e){super(zo,"barcode",!0);this.charEncoding=getKeyword({data:e.charEncoding?e.charEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.checksum=getStringOption(e.checksum,["none","1mod10","1mod10_1mod11","2mod10","auto"]);this.dataColumnCount=getInteger({data:e.dataColumnCount,defaultValue:-1,validate:e=>e>=0});this.dataLength=getInteger({data:e.dataLength,defaultValue:-1,validate:e=>e>=0});this.dataPrep=getStringOption(e.dataPrep,["none","flateCompress"]);this.dataRowCount=getInteger({data:e.dataRowCount,defaultValue:-1,validate:e=>e>=0});this.endChar=e.endChar||"";this.errorCorrectionLevel=getInteger({data:e.errorCorrectionLevel,defaultValue:-1,validate:e=>e>=0&&e<=8});this.id=e.id||"";this.moduleHeight=getMeasurement(e.moduleHeight,"5mm");this.moduleWidth=getMeasurement(e.moduleWidth,"0.25mm");this.printCheckDigit=getInteger({data:e.printCheckDigit,defaultValue:0,validate:e=>1===e});this.rowColumnRatio=getRatio(e.rowColumnRatio);this.startChar=e.startChar||"";this.textLocation=getStringOption(e.textLocation,["below","above","aboveEmbedded","belowEmbedded","none"]);this.truncate=getInteger({data:e.truncate,defaultValue:0,validate:e=>1===e});this.type=getStringOption(e.type?e.type.toLowerCase():"",["aztec","codabar","code2of5industrial","code2of5interleaved","code2of5matrix","code2of5standard","code3of9","code3of9extended","code11","code49","code93","code128","code128a","code128b","code128c","code128sscc","datamatrix","ean8","ean8add2","ean8add5","ean13","ean13add2","ean13add5","ean13pwcd","fim","logmars","maxicode","msi","pdf417","pdf417macro","plessey","postauscust2","postauscust3","postausreplypaid","postausstandard","postukrm4scc","postusdpbc","postusimb","postusstandard","postus5zip","qrcode","rfid","rss14","rss14expanded","rss14limited","rss14stacked","rss14stackedomni","rss14truncated","telepen","ucc128","ucc128random","ucc128sscc","upca","upcaadd2","upcaadd5","upcapwcd","upce","upceadd2","upceadd5","upcean2","upcean5","upsmaxicode"]);this.upsMode=getStringOption(e.upsMode,["usCarrier","internationalCarrier","secureSymbol","standardSymbol"]);this.use=e.use||"";this.usehref=e.usehref||"";this.wideNarrowRatio=getRatio(e.wideNarrowRatio);this.encrypt=null;this.extras=null}}class Bind extends XFAObject{constructor(e){super(zo,"bind",!0);this.match=getStringOption(e.match,["once","dataRef","global","none"]);this.ref=e.ref||"";this.picture=null}}class BindItems extends XFAObject{constructor(e){super(zo,"bindItems");this.connection=e.connection||"";this.labelRef=e.labelRef||"";this.ref=e.ref||"";this.valueRef=e.valueRef||""}}class Bookend extends XFAObject{constructor(e){super(zo,"bookend");this.id=e.id||"";this.leader=e.leader||"";this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||""}}class BooleanElement extends Option01{constructor(e){super(zo,"boolean");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[so](e){return valueToHtml(1===this[is]?"1":"0")}}class Border extends XFAObject{constructor(e){super(zo,"border",!0);this.break=getStringOption(e.break,["close","open"]);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.extras=null;this.fill=null;this.margin=null}[bs](){if(!this[os]){const e=this.edge.children.slice();if(e.length<4){const t=e.at(-1)||new Edge({});for(let a=e.length;a<4;a++)e.push(t)}const t=e.map((e=>e.thickness)),a=[0,0,0,0];if(this.margin){a[0]=this.margin.topInset;a[1]=this.margin.rightInset;a[2]=this.margin.bottomInset;a[3]=this.margin.leftInset}this[os]={widths:t,insets:a,edges:e}}return this[os]}[co](){const{edges:e}=this[bs](),t=e.map((e=>{const t=e[co]();t.color||="#000000";return t})),a=Object.create(null);this.margin&&Object.assign(a,this.margin[co]());"visible"===this.fill?.presence&&Object.assign(a,this.fill[co]());if(this.corner.children.some((e=>0!==e.radius))){const e=this.corner.children.map((e=>e[co]()));if(2===e.length||3===e.length){const t=e.at(-1);for(let a=e.length;a<4;a++)e.push(t)}a.borderRadius=e.map((e=>e.radius)).join(" ")}switch(this.presence){case"invisible":case"hidden":a.borderStyle="";break;case"inactive":a.borderStyle="none";break;default:a.borderStyle=t.map((e=>e.style)).join(" ")}a.borderWidth=t.map((e=>e.width)).join(" ");a.borderColor=t.map((e=>e.color)).join(" ");return a}}class Break extends XFAObject{constructor(e){super(zo,"break",!0);this.after=getStringOption(e.after,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.afterTarget=e.afterTarget||"";this.before=getStringOption(e.before,["auto","contentArea","pageArea","pageEven","pageOdd"]);this.beforeTarget=e.beforeTarget||"";this.bookendLeader=e.bookendLeader||"";this.bookendTrailer=e.bookendTrailer||"";this.id=e.id||"";this.overflowLeader=e.overflowLeader||"";this.overflowTarget=e.overflowTarget||"";this.overflowTrailer=e.overflowTrailer||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class BreakAfter extends XFAObject{constructor(e){super(zo,"breakAfter",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}}class BreakBefore extends XFAObject{constructor(e){super(zo,"breakBefore",!0);this.id=e.id||"";this.leader=e.leader||"";this.startNew=getInteger({data:e.startNew,defaultValue:0,validate:e=>1===e});this.target=e.target||"";this.targetType=getStringOption(e.targetType,["auto","contentArea","pageArea"]);this.trailer=e.trailer||"";this.use=e.use||"";this.usehref=e.usehref||"";this.script=null}[so](e){this[os]={};return HTMLResult.FAILURE}}class Button extends XFAObject{constructor(e){super(zo,"button",!0);this.highlight=getStringOption(e.highlight,["inverted","none","outline","push"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[so](e){const t=this[ks]()[ks](),a={name:"button",attributes:{id:this[lo],class:["xfaButton"],style:{}},children:[]};for(const e of t.event.children){if("click"!==e.activity||!e.script)continue;const t=recoverJsURL(e.script[is]);if(!t)continue;const r=fixURL(t.url);r&&a.children.push({name:"a",attributes:{id:"link"+this[lo],href:r,newWindow:t.newWindow,class:["xfaLink"],style:{}},children:[]})}return HTMLResult.success(a)}}class Calculate extends XFAObject{constructor(e){super(zo,"calculate",!0);this.id=e.id||"";this.override=getStringOption(e.override,["disabled","error","ignore","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.script=null}}class Caption extends XFAObject{constructor(e){super(zo,"caption",!0);this.id=e.id||"";this.placement=getStringOption(e.placement,["left","bottom","inline","right","top"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.reserve=Math.ceil(getMeasurement(e.reserve));this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.font=null;this.margin=null;this.para=null;this.value=null}[ao](e){_setValue(this,e)}[bs](e){if(!this[os]){let{width:t,height:a}=e;switch(this.placement){case"left":case"right":case"inline":t=this.reserve<=0?t:this.reserve;break;case"top":case"bottom":a=this.reserve<=0?a:this.reserve}this[os]=layoutNode(this,{width:t,height:a})}return this[os]}[so](e){if(!this.value)return HTMLResult.EMPTY;this[Ks]();const t=this.value[so](e).html;if(!t){this[Vs]();return HTMLResult.EMPTY}const a=this.reserve;if(this.reserve<=0){const{w:t,h:a}=this[bs](e);switch(this.placement){case"left":case"right":case"inline":this.reserve=t;break;case"top":case"bottom":this.reserve=a}}const r=[];"string"==typeof t?r.push({name:"#text",value:t}):r.push(t);const i=toStyle(this,"font","margin","visibility");switch(this.placement){case"left":case"right":this.reserve>0&&(i.width=measureToString(this.reserve));break;case"top":case"bottom":this.reserve>0&&(i.height=measureToString(this.reserve))}setPara(this,null,t);this[Vs]();this.reserve=a;return HTMLResult.success({name:"div",attributes:{style:i,class:["xfaCaption"]},children:r})}}class Certificate extends StringObject{constructor(e){super(zo,"certificate");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Certificates extends XFAObject{constructor(e){super(zo,"certificates",!0);this.credentialServerPolicy=getStringOption(e.credentialServerPolicy,["optional","required"]);this.id=e.id||"";this.url=e.url||"";this.urlPolicy=e.urlPolicy||"";this.use=e.use||"";this.usehref=e.usehref||"";this.encryption=null;this.issuers=null;this.keyUsage=null;this.oids=null;this.signing=null;this.subjectDNs=null}}class CheckButton extends XFAObject{constructor(e){super(zo,"checkButton",!0);this.id=e.id||"";this.mark=getStringOption(e.mark,["default","check","circle","cross","diamond","square","star"]);this.shape=getStringOption(e.shape,["square","round"]);this.size=getMeasurement(e.size,"10pt");this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"margin"),a=measureToString(this.size);t.width=t.height=a;let r,i,n;const s=this[ks]()[ks](),o=s.items.children.length&&s.items.children[0][so]().html||[],c={on:(void 0!==o[0]?o[0]:"on").toString(),off:(void 0!==o[1]?o[1]:"off").toString()},l=(s.value?.[io]()||"off")===c.on||void 0,h=s[As](),u=s[lo];let d;if(h instanceof ExclGroup){n=h[lo];r="radio";i="xfaRadio";d=h[ns]?.[lo]||h[lo]}else{r="checkbox";i="xfaCheckbox";d=s[ns]?.[lo]||s[lo]}const f={name:"input",attributes:{class:[i],style:t,fieldId:u,dataId:d,type:r,checked:l,xfaOn:c.on,xfaOff:c.off,"aria-label":ariaLabel(s),"aria-required":!1}};n&&(f.attributes.name=n);if(isRequired(s)){f.attributes["aria-required"]=!0;f.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[f]})}}class ChoiceList extends XFAObject{constructor(e){super(zo,"choiceList",!0);this.commitOn=getStringOption(e.commitOn,["select","exit"]);this.id=e.id||"";this.open=getStringOption(e.open,["userControl","always","multiSelect","onEntry"]);this.textEntry=getInteger({data:e.textEntry,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","margin"),a=this[ks]()[ks](),r={fontSize:`calc(${a.font?.size||10}px * var(--total-scale-factor))`},i=[];if(a.items.children.length>0){const e=a.items;let t=0,n=0;if(2===e.children.length){t=e.children[0].save;n=1-t}const s=e.children[t][so]().html,o=e.children[n][so]().html;let c=!1;const l=a.value?.[io]()||"";for(let e=0,t=s.length;eMathClamp(parseInt(e.trim(),10),0,255))).map((e=>isNaN(e)?0:e));if(n.length<3)return{r:a,g:r,b:i};[a,r,i]=n;return{r:a,g:r,b:i}}(e.value):"";this.extras=null}[Fs](){return!1}[co](){return this.value?Util.makeHexColor(this.value.r,this.value.g,this.value.b):null}}class Comb extends XFAObject{constructor(e){super(zo,"comb");this.id=e.id||"";this.numberOfCells=getInteger({data:e.numberOfCells,defaultValue:0,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||""}}class Connect extends XFAObject{constructor(e){super(zo,"connect",!0);this.connection=e.connection||"";this.id=e.id||"";this.ref=e.ref||"";this.usage=getStringOption(e.usage,["exportAndImport","exportOnly","importOnly"]);this.use=e.use||"";this.usehref=e.usehref||"";this.picture=null}}class ContentArea extends XFAObject{constructor(e){super(zo,"contentArea",!0);this.h=getMeasurement(e.h);this.id=e.id||"";this.name=e.name||"";this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=getMeasurement(e.w);this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.desc=null;this.extras=null}[so](e){const t={left:measureToString(this.x),top:measureToString(this.y),width:measureToString(this.w),height:measureToString(this.h)},a=["xfaContentarea"];isPrintOnly(this)&&a.push("xfaPrintOnly");return HTMLResult.success({name:"div",children:[],attributes:{style:t,class:a,id:this[lo]}})}}class Corner extends XFAObject{constructor(e){super(zo,"corner",!0);this.id=e.id||"";this.inverted=getInteger({data:e.inverted,defaultValue:0,validate:e=>1===e});this.join=getStringOption(e.join,["square","round"]);this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.radius=getMeasurement(e.radius);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](){const e=toStyle(this,"visibility");e.radius=measureToString("square"===this.join?0:this.radius);return e}}class DateElement extends ContentObject{constructor(e){super(zo,"date");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class DateTime extends ContentObject{constructor(e){super(zo,"dateTime");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class DateTimeEdit extends XFAObject{constructor(e){super(zo,"dateTimeEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.picker=getStringOption(e.picker,["host","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin"),a=this[ks]()[ks](),r={name:"input",attributes:{type:"text",fieldId:a[lo],dataId:a[ns]?.[lo]||a[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Decimal extends ContentObject{constructor(e){super(zo,"decimal");this.fracDigits=getInteger({data:e.fracDigits,defaultValue:2,validate:e=>!0});this.id=e.id||"";this.leadDigits=getInteger({data:e.leadDigits,defaultValue:-1,validate:e=>!0});this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseFloat(this[is].trim());this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class DefaultUi extends XFAObject{constructor(e){super(zo,"defaultUi",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class Desc extends XFAObject{constructor(e){super(zo,"desc",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class DigestMethod extends OptionObject{constructor(e){super(zo,"digestMethod",["","SHA1","SHA256","SHA512","RIPEMD160"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class DigestMethods extends XFAObject{constructor(e){super(zo,"digestMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.digestMethod=new XFAObjectArray}}class Draw extends XFAObject{constructor(e){super(zo,"draw",!0);this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.border=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.value=null;this.setProperty=new XFAObjectArray}[ao](e){_setValue(this,e)}[so](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;fixDimensions(this);this[Ks]();const t=this.w,a=this.h,{w:r,h:i,isBroken:n}=layoutNode(this,e);if(r&&""===this.w){if(n&&this[As]()[Ls]()){this[Vs]();return HTMLResult.FAILURE}this.w=r}i&&""===this.h&&(this.h=i);setFirstUnsplittable(this);if(!checkDimensions(this,e)){this.w=t;this.h=a;this[Vs]();return HTMLResult.FAILURE}unsetFirstUnsplittable(this);const s=toStyle(this,"font","hAlign","dimensions","position","presence","rotate","anchorType","border","margin");setMinMaxDimensions(this,s);if(s.margin){s.padding=s.margin;delete s.margin}const o=["xfaDraw"];this.font&&o.push("xfaFont");isPrintOnly(this)&&o.push("xfaPrintOnly");const c={style:s,id:this[lo],class:o};this.name&&(c.xfaName=this.name);const l={name:"div",attributes:c,children:[]};applyAssist(this,c);const h=computeBbox(this,l,e),u=this.value?this.value[so](e).html:null;if(null===u){this.w=t;this.h=a;this[Vs]();return HTMLResult.success(createWrapper(this,l),h)}l.children.push(u);setPara(this,s,u);this.w=t;this.h=a;this[Vs]();return HTMLResult.success(createWrapper(this,l),h)}}class Edge extends XFAObject{constructor(e){super(zo,"edge",!0);this.cap=getStringOption(e.cap,["square","butt","round"]);this.id=e.id||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.stroke=getStringOption(e.stroke,["solid","dashDot","dashDotDot","dashed","dotted","embossed","etched","lowered","raised"]);this.thickness=getMeasurement(e.thickness,"0.5pt");this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](){const e=toStyle(this,"visibility");Object.assign(e,{linecap:this.cap,width:measureToString(this.thickness),color:this.color?this.color[co]():"#000000",style:""});if("visible"!==this.presence)e.style="none";else switch(this.stroke){case"solid":e.style="solid";break;case"dashDot":case"dashDotDot":case"dashed":e.style="dashed";break;case"dotted":e.style="dotted";break;case"embossed":e.style="ridge";break;case"etched":e.style="groove";break;case"lowered":e.style="inset";break;case"raised":e.style="outset"}return e}}class Encoding extends OptionObject{constructor(e){super(zo,"encoding",["adbe.x509.rsa_sha1","adbe.pkcs7.detached","adbe.pkcs7.sha1"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Encodings extends XFAObject{constructor(e){super(zo,"encodings",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encoding=new XFAObjectArray}}class Encrypt extends XFAObject{constructor(e){super(zo,"encrypt",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=null}}class EncryptData extends XFAObject{constructor(e){super(zo,"encryptData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["encrypt","decrypt"]);this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Encryption extends XFAObject{constructor(e){super(zo,"encryption",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class EncryptionMethod extends OptionObject{constructor(e){super(zo,"encryptionMethod",["","AES256-CBC","TRIPLEDES-CBC","AES128-CBC","AES192-CBC"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EncryptionMethods extends XFAObject{constructor(e){super(zo,"encryptionMethods",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.encryptionMethod=new XFAObjectArray}}class Event extends XFAObject{constructor(e){super(zo,"event",!0);this.activity=getStringOption(e.activity,["click","change","docClose","docReady","enter","exit","full","indexChange","initialize","mouseDown","mouseEnter","mouseExit","mouseUp","postExecute","postOpen","postPrint","postSave","postSign","postSubmit","preExecute","preOpen","prePrint","preSave","preSign","preSubmit","ready","validationState"]);this.id=e.id||"";this.listen=getStringOption(e.listen,["refOnly","refAndDescendents"]);this.name=e.name||"";this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.encryptData=null;this.execute=null;this.script=null;this.signData=null;this.submit=null}}class ExData extends ContentObject{constructor(e){super(zo,"exData");this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.maxLength=getInteger({data:e.maxLength,defaultValue:-1,validate:e=>e>=-1});this.name=e.name||"";this.rid=e.rid||"";this.transferEncoding=getStringOption(e.transferEncoding,["none","base64","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[Ms](){return"text/html"===this.contentType}[Ws](e){if("text/html"===this.contentType&&e[Xs]===uo.xhtml.id){this[is]=e;return!0}if("text/xml"===this.contentType){this[is]=e;return!0}return!1}[so](e){return"text/html"===this.contentType&&this[is]?this[is][so](e):HTMLResult.EMPTY}}class ExObject extends XFAObject{constructor(e){super(zo,"exObject",!0);this.archive=e.archive||"";this.classId=e.classId||"";this.codeBase=e.codeBase||"";this.codeType=e.codeType||"";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}}class ExclGroup extends XFAObject{constructor(e){super(zo,"exclGroup",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.accessKey=e.accessKey||"";this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.margin=null;this.para=null;this.traversal=null;this.validate=null;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.field=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ds](){return!0}[Fs](){return!0}[ao](e){for(const t of this.field.children){if(!t.value){const e=new Value({});t[Yn](e);t.value=e}t.value[ao](e)}}[Ls](){return this.layout.endsWith("-tb")&&0===this[os].attempt&&this[os].numberInLine>0||this[ks]()[Ls]()}[Es](){const e=this[As]();if(!e[Es]())return!1;if(void 0!==this[os]._isSplittable)return this[os]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[os]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[os].numberInLine)return!1;this[os]._isSplittable=!0;return!0}[ls](){return flushHTML(this)}[Jn](e,t){addHTML(this,e,t)}[ds](){return getAvailableSpace(this)}[so](e){setTabIndex(this);if("hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[lo],class:[]};setAccess(this,a.class);this[os]||=Object.create(null);Object.assign(this[os],{children:t,attributes:a,attempt:0,line:null,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[Es]();r||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const i=new Set(["field"]);if(this.layout.includes("row")){const e=this[As]().columnWidths;if(Array.isArray(e)&&e.length>0){this[os].columnWidths=e;this[os].currentColumn=0}}const n=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),s=["xfaExclgroup"],o=layoutClass(this);o&&s.push(o);isPrintOnly(this)&&s.push("xfaPrintOnly");a.style=n;a.class=s;this.name&&(a.xfaName=this.name);this[Ks]();const c="lr-tb"===this.layout||"rl-tb"===this.layout,l=c?2:1;for(;this[os].attempte>=1||-1===e});this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.rotate=getInteger({data:e.rotate,defaultValue:0,validate:e=>e%90==0});this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.border=null;this.calculate=null;this.caption=null;this.desc=null;this.extras=null;this.font=null;this.format=null;this.items=new XFAObjectArray(2);this.keep=null;this.margin=null;this.para=null;this.traversal=null;this.ui=null;this.validate=null;this.value=null;this.bindItems=new XFAObjectArray;this.connect=new XFAObjectArray;this.event=new XFAObjectArray;this.setProperty=new XFAObjectArray}[Ds](){return!0}[ao](e){_setValue(this,e)}[so](e){setTabIndex(this);if(!this.ui){this.ui=new Ui({});this.ui[vs]=this[vs];this[Yn](this.ui);let e;switch(this.items.children.length){case 0:e=new TextEdit({});this.ui.textEdit=e;break;case 1:e=new CheckButton({});this.ui.checkButton=e;break;case 2:e=new ChoiceList({});this.ui.choiceList=e}this.ui[Yn](e)}if(!this.ui||"hidden"===this.presence||"inactive"===this.presence||0===this.h||0===this.w)return HTMLResult.EMPTY;this.caption&&delete this.caption[os];this[Ks]();const t=this.caption?this.caption[so](e).html:null,a=this.w,r=this.h;let i=0,n=0;if(this.margin){i=this.margin.leftInset+this.margin.rightInset;n=this.margin.topInset+this.margin.bottomInset}let s=null;if(""===this.w||""===this.h){let t=null,a=null,r=0,o=0;if(this.ui.checkButton)r=o=this.ui.checkButton.size;else{const{w:t,h:a}=layoutNode(this,e);if(null!==t){r=t;o=a}else o=function fonts_getMetrics(e,t=!1){let a=null;if(e){const t=stripQuotes(e.typeface),r=e[vs].fontFinder.find(t);a=selectFont(e,r)}if(!a)return{lineHeight:12,lineGap:2,lineNoGap:10};const r=e.size||10,i=a.lineHeight?Math.max(t?0:1.2,a.lineHeight):1.2,n=void 0===a.lineGap?.2:a.lineGap;return{lineHeight:i*r,lineGap:n*r,lineNoGap:Math.max(1,i-n)*r}}(this.font,!0).lineNoGap}s=getBorderDims(this.ui[bs]());r+=s.w;o+=s.h;if(this.caption){const{w:i,h:n,isBroken:s}=this.caption[bs](e);if(s&&this[As]()[Ls]()){this[Vs]();return HTMLResult.FAILURE}t=i;a=n;switch(this.caption.placement){case"left":case"right":case"inline":t+=r;break;case"top":case"bottom":a+=o}}else{t=r;a=o}if(t&&""===this.w){t+=i;this.w=Math.min(this.maxW<=0?1/0:this.maxW,this.minW+1e>=1&&e<=5});this.appearanceFilter=null;this.certificates=null;this.digestMethods=null;this.encodings=null;this.encryptionMethods=null;this.handler=null;this.lockDocument=null;this.mdp=null;this.reasons=null;this.timeStamp=null}}class Float extends ContentObject{constructor(e){super(zo,"float");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseFloat(this[is].trim());this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class template_Font extends XFAObject{constructor(e){super(zo,"font",!0);this.baselineShift=getMeasurement(e.baselineShift);this.fontHorizontalScale=getFloat({data:e.fontHorizontalScale,defaultValue:100,validate:e=>e>=0});this.fontVerticalScale=getFloat({data:e.fontVerticalScale,defaultValue:100,validate:e=>e>=0});this.id=e.id||"";this.kerningMode=getStringOption(e.kerningMode,["none","pair"]);this.letterSpacing=getMeasurement(e.letterSpacing,"0");this.lineThrough=getInteger({data:e.lineThrough,defaultValue:0,validate:e=>1===e||2===e});this.lineThroughPeriod=getStringOption(e.lineThroughPeriod,["all","word"]);this.overline=getInteger({data:e.overline,defaultValue:0,validate:e=>1===e||2===e});this.overlinePeriod=getStringOption(e.overlinePeriod,["all","word"]);this.posture=getStringOption(e.posture,["normal","italic"]);this.size=getMeasurement(e.size,"10pt");this.typeface=e.typeface||"Courier";this.underline=getInteger({data:e.underline,defaultValue:0,validate:e=>1===e||2===e});this.underlinePeriod=getStringOption(e.underlinePeriod,["all","word"]);this.use=e.use||"";this.usehref=e.usehref||"";this.weight=getStringOption(e.weight,["normal","bold"]);this.extras=null;this.fill=null}[Qn](e){super[Qn](e);this[vs].usedTypefaces.add(this.typeface)}[co](){const e=toStyle(this,"fill"),t=e.color;if(t)if("#000000"===t)delete e.color;else if(!t.startsWith("#")){e.background=t;e.backgroundClip="text";e.color="transparent"}this.baselineShift&&(e.verticalAlign=measureToString(this.baselineShift));e.fontKerning="none"===this.kerningMode?"none":"normal";e.letterSpacing=measureToString(this.letterSpacing);if(0!==this.lineThrough){e.textDecoration="line-through";2===this.lineThrough&&(e.textDecorationStyle="double")}if(0!==this.overline){e.textDecoration="overline";2===this.overline&&(e.textDecorationStyle="double")}e.fontStyle=this.posture;e.fontSize=measureToString(.99*this.size);setFontFamily(this,this,this[vs].fontFinder,e);if(0!==this.underline){e.textDecoration="underline";2===this.underline&&(e.textDecorationStyle="double")}e.fontWeight=this.weight;return e}}class Format extends XFAObject{constructor(e){super(zo,"format",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null}}class Handler extends StringObject{constructor(e){super(zo,"handler");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Hyphenation extends XFAObject{constructor(e){super(zo,"hyphenation");this.excludeAllCaps=getInteger({data:e.excludeAllCaps,defaultValue:0,validate:e=>1===e});this.excludeInitialCap=getInteger({data:e.excludeInitialCap,defaultValue:0,validate:e=>1===e});this.hyphenate=getInteger({data:e.hyphenate,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.pushCharacterCount=getInteger({data:e.pushCharacterCount,defaultValue:3,validate:e=>e>=0});this.remainCharacterCount=getInteger({data:e.remainCharacterCount,defaultValue:3,validate:e=>e>=0});this.use=e.use||"";this.usehref=e.usehref||"";this.wordCharacterCount=getInteger({data:e.wordCharacterCount,defaultValue:7,validate:e=>e>=0})}}class Image extends StringObject{constructor(e){super(zo,"image");this.aspect=getStringOption(e.aspect,["fit","actual","height","none","width"]);this.contentType=e.contentType||"";this.href=e.href||"";this.id=e.id||"";this.name=e.name||"";this.transferEncoding=getStringOption(e.transferEncoding,["base64","none","package"]);this.use=e.use||"";this.usehref=e.usehref||""}[so](){if(this.contentType&&!Vo.has(this.contentType.toLowerCase()))return HTMLResult.EMPTY;let e=this[vs].images?.get(this.href);if(!e&&(this.href||!this[is]))return HTMLResult.EMPTY;e||"base64"!==this.transferEncoding||(e=function fromBase64Util(e){return Uint8Array.fromBase64?Uint8Array.fromBase64(e):stringToBytes(atob(e))}(this[is]));if(!e)return HTMLResult.EMPTY;if(!this.contentType){for(const[t,a]of Ko)if(e.length>t.length&&t.every(((t,a)=>t===e[a]))){this.contentType=a;break}if(!this.contentType)return HTMLResult.EMPTY}const t=new Blob([e],{type:this.contentType});let a;switch(this.aspect){case"fit":case"actual":break;case"height":a={height:"100%",objectFit:"fill"};break;case"none":a={width:"100%",height:"100%",objectFit:"fill"};break;case"width":a={width:"100%",objectFit:"fill"}}const r=this[ks]();return HTMLResult.success({name:"img",attributes:{class:["xfaImage"],style:a,src:URL.createObjectURL(t),alt:r?ariaLabel(r[ks]()):null}})}}class ImageEdit extends XFAObject{constructor(e){super(zo,"imageEdit",!0);this.data=getStringOption(e.data,["link","embed"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}[so](e){return"embed"===this.data?HTMLResult.success({name:"div",children:[],attributes:{}}):HTMLResult.EMPTY}}class Integer extends ContentObject{constructor(e){super(zo,"integer");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=parseInt(this[is].trim(),10);this[is]=isNaN(e)?null:e}[so](e){return valueToHtml(null!==this[is]?this[is].toString():"")}}class Issuers extends XFAObject{constructor(e){super(zo,"issuers",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Items extends XFAObject{constructor(e){super(zo,"items",!0);this.id=e.id||"";this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.ref=e.ref||"";this.save=getInteger({data:e.save,defaultValue:0,validate:e=>1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[so](){const e=[];for(const t of this[ws]())e.push(t[io]());return HTMLResult.success(e)}}class Keep extends XFAObject{constructor(e){super(zo,"keep",!0);this.id=e.id||"";const t=["none","contentArea","pageArea"];this.intact=getStringOption(e.intact,t);this.next=getStringOption(e.next,t);this.previous=getStringOption(e.previous,t);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}}class KeyUsage extends XFAObject{constructor(e){super(zo,"keyUsage");const t=["","yes","no"];this.crlSign=getStringOption(e.crlSign,t);this.dataEncipherment=getStringOption(e.dataEncipherment,t);this.decipherOnly=getStringOption(e.decipherOnly,t);this.digitalSignature=getStringOption(e.digitalSignature,t);this.encipherOnly=getStringOption(e.encipherOnly,t);this.id=e.id||"";this.keyAgreement=getStringOption(e.keyAgreement,t);this.keyCertSign=getStringOption(e.keyCertSign,t);this.keyEncipherment=getStringOption(e.keyEncipherment,t);this.nonRepudiation=getStringOption(e.nonRepudiation,t);this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Line extends XFAObject{constructor(e){super(zo,"line",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.slope=getStringOption(e.slope,["\\","/"]);this.use=e.use||"";this.usehref=e.usehref||"";this.edge=null}[so](){const e=this[ks]()[ks](),t=this.edge||new Edge({}),a=t[co](),r=Object.create(null),i="visible"===t.presence?t.thickness:0;r.strokeWidth=measureToString(i);r.stroke=a.color;let n,s,o,c,l="100%",h="100%";if(e.w<=i){[n,s,o,c]=["50%",0,"50%","100%"];l=r.strokeWidth}else if(e.h<=i){[n,s,o,c]=[0,"50%","100%","50%"];h=r.strokeWidth}else"\\"===this.slope?[n,s,o,c]=[0,0,"100%","100%"]:[n,s,o,c]=[0,"100%","100%",0];const u={name:"svg",children:[{name:"line",attributes:{xmlns:$o,x1:n,y1:s,x2:o,y2:c,style:r}}],attributes:{xmlns:$o,width:l,height:h,style:{overflow:"visible"}}};if(hasMargin(e))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[u]});u.attributes.style.position="absolute";return HTMLResult.success(u)}}class Linear extends XFAObject{constructor(e){super(zo,"linear",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toRight","toBottom","toLeft","toTop"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";return`linear-gradient(${this.type.replace(/([RBLT])/," $1").toLowerCase()}, ${e}, ${this.color?this.color[co]():"#000000"})`}}class LockDocument extends ContentObject{constructor(e){super(zo,"lockDocument");this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}[cs](){this[is]=getStringOption(this[is],["auto","0","1"])}}class Manifest extends XFAObject{constructor(e){super(zo,"manifest",!0);this.action=getStringOption(e.action,["include","all","exclude"]);this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.ref=new XFAObjectArray}}class Margin extends XFAObject{constructor(e){super(zo,"margin",!0);this.bottomInset=getMeasurement(e.bottomInset,"0");this.id=e.id||"";this.leftInset=getMeasurement(e.leftInset,"0");this.rightInset=getMeasurement(e.rightInset,"0");this.topInset=getMeasurement(e.topInset,"0");this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[co](){return{margin:measureToString(this.topInset)+" "+measureToString(this.rightInset)+" "+measureToString(this.bottomInset)+" "+measureToString(this.leftInset)}}}class Mdp extends XFAObject{constructor(e){super(zo,"mdp");this.id=e.id||"";this.permissions=getInteger({data:e.permissions,defaultValue:2,validate:e=>1===e||3===e});this.signatureType=getStringOption(e.signatureType,["filler","author"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Medium extends XFAObject{constructor(e){super(zo,"medium");this.id=e.id||"";this.imagingBBox=function getBBox(e){const t=-1;if(!e)return{x:t,y:t,width:t,height:t};const a=e.split(",",4).map((e=>getMeasurement(e.trim(),"-1")));if(a.length<4||a[2]<0||a[3]<0)return{x:t,y:t,width:t,height:t};const[r,i,n,s]=a;return{x:r,y:i,width:n,height:s}}(e.imagingBBox);this.long=getMeasurement(e.long);this.orientation=getStringOption(e.orientation,["portrait","landscape"]);this.short=getMeasurement(e.short);this.stock=e.stock||"";this.trayIn=getStringOption(e.trayIn,["auto","delegate","pageFront"]);this.trayOut=getStringOption(e.trayOut,["auto","delegate"]);this.use=e.use||"";this.usehref=e.usehref||""}}class Message extends XFAObject{constructor(e){super(zo,"message",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.text=new XFAObjectArray}}class NumericEdit extends XFAObject{constructor(e){super(zo,"numericEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin"),a=this[ks]()[ks](),r={name:"input",attributes:{type:"text",fieldId:a[lo],dataId:a[ns]?.[lo]||a[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(a),"aria-required":!1}};if(isRequired(a)){r.attributes["aria-required"]=!0;r.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[r]})}}class Occur extends XFAObject{constructor(e){super(zo,"occur",!0);this.id=e.id||"";this.initial=""!==e.initial?getInteger({data:e.initial,defaultValue:"",validate:e=>!0}):"";this.max=""!==e.max?getInteger({data:e.max,defaultValue:1,validate:e=>!0}):"";this.min=""!==e.min?getInteger({data:e.min,defaultValue:1,validate:e=>!0}):"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[Qn](){const e=this[ks](),t=this.min;""===this.min&&(this.min=e instanceof PageArea||e instanceof PageSet?0:1);""===this.max&&(this.max=""===t?e instanceof PageArea||e instanceof PageSet?-1:1:this.min);-1!==this.max&&this.max!0});this.name=e.name||"";this.numbered=getInteger({data:e.numbered,defaultValue:1,validate:e=>!0});this.oddOrEven=getStringOption(e.oddOrEven,["any","even","odd"]);this.pagePosition=getStringOption(e.pagePosition,["any","first","last","only","rest"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.desc=null;this.extras=null;this.medium=null;this.occur=null;this.area=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.draw=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.subform=new XFAObjectArray}[_s](){if(!this[os]){this[os]={numberOfUse:0};return!0}return!this.occur||-1===this.occur.max||this[os].numberOfUsee.oddOrEven===t&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&e.pagePosition===a));if(r)return r;r=this.pageArea.children.find((e=>"any"===e.oddOrEven&&"any"===e.pagePosition));return r||this.pageArea.children[0]}}class Para extends XFAObject{constructor(e){super(zo,"para",!0);this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.lineHeight=e.lineHeight?getMeasurement(e.lineHeight,"0pt"):"";this.marginLeft=e.marginLeft?getMeasurement(e.marginLeft,"0pt"):"";this.marginRight=e.marginRight?getMeasurement(e.marginRight,"0pt"):"";this.orphans=getInteger({data:e.orphans,defaultValue:0,validate:e=>e>=0});this.preserve=e.preserve||"";this.radixOffset=e.radixOffset?getMeasurement(e.radixOffset,"0pt"):"";this.spaceAbove=e.spaceAbove?getMeasurement(e.spaceAbove,"0pt"):"";this.spaceBelow=e.spaceBelow?getMeasurement(e.spaceBelow,"0pt"):"";this.tabDefault=e.tabDefault?getMeasurement(this.tabDefault):"";this.tabStops=(e.tabStops||"").trim().split(/\s+/).map(((e,t)=>t%2==1?getMeasurement(e):e));this.textIndent=e.textIndent?getMeasurement(e.textIndent,"0pt"):"";this.use=e.use||"";this.usehref=e.usehref||"";this.vAlign=getStringOption(e.vAlign,["top","bottom","middle"]);this.widows=getInteger({data:e.widows,defaultValue:0,validate:e=>e>=0});this.hyphenation=null}[co](){const e=toStyle(this,"hAlign");""!==this.marginLeft&&(e.paddingLeft=measureToString(this.marginLeft));""!==this.marginRight&&(e.paddingRight=measureToString(this.marginRight));""!==this.spaceAbove&&(e.paddingTop=measureToString(this.spaceAbove));""!==this.spaceBelow&&(e.paddingBottom=measureToString(this.spaceBelow));if(""!==this.textIndent){e.textIndent=measureToString(this.textIndent);fixTextIndent(e)}this.lineHeight>0&&(e.lineHeight=measureToString(this.lineHeight));""!==this.tabDefault&&(e.tabSize=measureToString(this.tabDefault));this.tabStops.length;this.hyphenatation&&Object.assign(e,this.hyphenatation[co]());return e}}class PasswordEdit extends XFAObject{constructor(e){super(zo,"passwordEdit",!0);this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.passwordChar=e.passwordChar||"*";this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.margin=null}}class template_Pattern extends XFAObject{constructor(e){super(zo,"pattern",!0);this.id=e.id||"";this.type=getStringOption(e.type,["crossHatch","crossDiagonal","diagonalLeft","diagonalRight","horizontal","vertical"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";const t=this.color?this.color[co]():"#000000",a="repeating-linear-gradient",r=`${e},${e} 5px,${t} 5px,${t} 10px`;switch(this.type){case"crossHatch":return`${a}(to top,${r}) ${a}(to right,${r})`;case"crossDiagonal":return`${a}(45deg,${r}) ${a}(-45deg,${r})`;case"diagonalLeft":return`${a}(45deg,${r})`;case"diagonalRight":return`${a}(-45deg,${r})`;case"horizontal":return`${a}(to top,${r})`;case"vertical":return`${a}(to right,${r})`}return""}}class Picture extends StringObject{constructor(e){super(zo,"picture");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Proto extends XFAObject{constructor(e){super(zo,"proto",!0);this.appearanceFilter=new XFAObjectArray;this.arc=new XFAObjectArray;this.area=new XFAObjectArray;this.assist=new XFAObjectArray;this.barcode=new XFAObjectArray;this.bindItems=new XFAObjectArray;this.bookend=new XFAObjectArray;this.boolean=new XFAObjectArray;this.border=new XFAObjectArray;this.break=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.button=new XFAObjectArray;this.calculate=new XFAObjectArray;this.caption=new XFAObjectArray;this.certificate=new XFAObjectArray;this.certificates=new XFAObjectArray;this.checkButton=new XFAObjectArray;this.choiceList=new XFAObjectArray;this.color=new XFAObjectArray;this.comb=new XFAObjectArray;this.connect=new XFAObjectArray;this.contentArea=new XFAObjectArray;this.corner=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.dateTimeEdit=new XFAObjectArray;this.decimal=new XFAObjectArray;this.defaultUi=new XFAObjectArray;this.desc=new XFAObjectArray;this.digestMethod=new XFAObjectArray;this.digestMethods=new XFAObjectArray;this.draw=new XFAObjectArray;this.edge=new XFAObjectArray;this.encoding=new XFAObjectArray;this.encodings=new XFAObjectArray;this.encrypt=new XFAObjectArray;this.encryptData=new XFAObjectArray;this.encryption=new XFAObjectArray;this.encryptionMethod=new XFAObjectArray;this.encryptionMethods=new XFAObjectArray;this.event=new XFAObjectArray;this.exData=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.execute=new XFAObjectArray;this.extras=new XFAObjectArray;this.field=new XFAObjectArray;this.fill=new XFAObjectArray;this.filter=new XFAObjectArray;this.float=new XFAObjectArray;this.font=new XFAObjectArray;this.format=new XFAObjectArray;this.handler=new XFAObjectArray;this.hyphenation=new XFAObjectArray;this.image=new XFAObjectArray;this.imageEdit=new XFAObjectArray;this.integer=new XFAObjectArray;this.issuers=new XFAObjectArray;this.items=new XFAObjectArray;this.keep=new XFAObjectArray;this.keyUsage=new XFAObjectArray;this.line=new XFAObjectArray;this.linear=new XFAObjectArray;this.lockDocument=new XFAObjectArray;this.manifest=new XFAObjectArray;this.margin=new XFAObjectArray;this.mdp=new XFAObjectArray;this.medium=new XFAObjectArray;this.message=new XFAObjectArray;this.numericEdit=new XFAObjectArray;this.occur=new XFAObjectArray;this.oid=new XFAObjectArray;this.oids=new XFAObjectArray;this.overflow=new XFAObjectArray;this.pageArea=new XFAObjectArray;this.pageSet=new XFAObjectArray;this.para=new XFAObjectArray;this.passwordEdit=new XFAObjectArray;this.pattern=new XFAObjectArray;this.picture=new XFAObjectArray;this.radial=new XFAObjectArray;this.reason=new XFAObjectArray;this.reasons=new XFAObjectArray;this.rectangle=new XFAObjectArray;this.ref=new XFAObjectArray;this.script=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.signData=new XFAObjectArray;this.signature=new XFAObjectArray;this.signing=new XFAObjectArray;this.solid=new XFAObjectArray;this.speak=new XFAObjectArray;this.stipple=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray;this.subjectDN=new XFAObjectArray;this.subjectDNs=new XFAObjectArray;this.submit=new XFAObjectArray;this.text=new XFAObjectArray;this.textEdit=new XFAObjectArray;this.time=new XFAObjectArray;this.timeStamp=new XFAObjectArray;this.toolTip=new XFAObjectArray;this.traversal=new XFAObjectArray;this.traverse=new XFAObjectArray;this.ui=new XFAObjectArray;this.validate=new XFAObjectArray;this.value=new XFAObjectArray;this.variables=new XFAObjectArray}}class Radial extends XFAObject{constructor(e){super(zo,"radial",!0);this.id=e.id||"";this.type=getStringOption(e.type,["toEdge","toCenter"]);this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){e=e?e[co]():"#FFFFFF";const t=this.color?this.color[co]():"#000000";return`radial-gradient(circle at center, ${"toEdge"===this.type?`${e},${t}`:`${t},${e}`})`}}class Reason extends StringObject{constructor(e){super(zo,"reason");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Reasons extends XFAObject{constructor(e){super(zo,"reasons",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.reason=new XFAObjectArray}}class Rectangle extends XFAObject{constructor(e){super(zo,"rectangle",!0);this.hand=getStringOption(e.hand,["even","left","right"]);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.corner=new XFAObjectArray(4);this.edge=new XFAObjectArray(4);this.fill=null}[so](){const e=this.edge.children.length?this.edge.children[0]:new Edge({}),t=e[co](),a=Object.create(null);"visible"===this.fill?.presence?Object.assign(a,this.fill[co]()):a.fill="transparent";a.strokeWidth=measureToString("visible"===e.presence?e.thickness:0);a.stroke=t.color;const r=(this.corner.children.length?this.corner.children[0]:new Corner({}))[co](),i={name:"svg",children:[{name:"rect",attributes:{xmlns:$o,width:"100%",height:"100%",x:0,y:0,rx:r.radius,ry:r.radius,style:a}}],attributes:{xmlns:$o,style:{overflow:"visible"},width:"100%",height:"100%"}};if(hasMargin(this[ks]()[ks]()))return HTMLResult.success({name:"div",attributes:{style:{display:"inline",width:"100%",height:"100%"}},children:[i]});i.attributes.style.position="absolute";return HTMLResult.success(i)}}class RefElement extends StringObject{constructor(e){super(zo,"ref");this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Script extends StringObject{constructor(e){super(zo,"script");this.binding=e.binding||"";this.contentType=e.contentType||"";this.id=e.id||"";this.name=e.name||"";this.runAt=getStringOption(e.runAt,["client","both","server"]);this.use=e.use||"";this.usehref=e.usehref||""}}class SetProperty extends XFAObject{constructor(e){super(zo,"setProperty");this.connection=e.connection||"";this.ref=e.ref||"";this.target=e.target||""}}class SignData extends XFAObject{constructor(e){super(zo,"signData",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["sign","clear","verify"]);this.ref=e.ref||"";this.target=e.target||"";this.use=e.use||"";this.usehref=e.usehref||"";this.filter=null;this.manifest=null}}class Signature extends XFAObject{constructor(e){super(zo,"signature",!0);this.id=e.id||"";this.type=getStringOption(e.type,["PDF1.3","PDF1.6"]);this.use=e.use||"";this.usehref=e.usehref||"";this.border=null;this.extras=null;this.filter=null;this.manifest=null;this.margin=null}}class Signing extends XFAObject{constructor(e){super(zo,"signing",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.certificate=new XFAObjectArray}}class Solid extends XFAObject{constructor(e){super(zo,"solid",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null}[co](e){return e?e[co]():"#FFFFFF"}}class Speak extends StringObject{constructor(e){super(zo,"speak");this.disable=getInteger({data:e.disable,defaultValue:0,validate:e=>1===e});this.id=e.id||"";this.priority=getStringOption(e.priority,["custom","caption","name","toolTip"]);this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Stipple extends XFAObject{constructor(e){super(zo,"stipple",!0);this.id=e.id||"";this.rate=getInteger({data:e.rate,defaultValue:50,validate:e=>e>=0&&e<=100});this.use=e.use||"";this.usehref=e.usehref||"";this.color=null;this.extras=null}[co](e){const t=this.rate/100;return Util.makeHexColor(Math.round(e.value.r*(1-t)+this.value.r*t),Math.round(e.value.g*(1-t)+this.value.g*t),Math.round(e.value.b*(1-t)+this.value.b*t))}}class Subform extends XFAObject{constructor(e){super(zo,"subform",!0);this.access=getStringOption(e.access,["open","nonInteractive","protected","readOnly"]);this.allowMacro=getInteger({data:e.allowMacro,defaultValue:0,validate:e=>1===e});this.anchorType=getStringOption(e.anchorType,["topLeft","bottomCenter","bottomLeft","bottomRight","middleCenter","middleLeft","middleRight","topCenter","topRight"]);this.colSpan=getInteger({data:e.colSpan,defaultValue:1,validate:e=>e>=1||-1===e});this.columnWidths=(e.columnWidths||"").trim().split(/\s+/).map((e=>"-1"===e?-1:getMeasurement(e)));this.h=e.h?getMeasurement(e.h):"";this.hAlign=getStringOption(e.hAlign,["left","center","justify","justifyAll","radix","right"]);this.id=e.id||"";this.layout=getStringOption(e.layout,["position","lr-tb","rl-row","rl-tb","row","table","tb"]);this.locale=e.locale||"";this.maxH=getMeasurement(e.maxH,"0pt");this.maxW=getMeasurement(e.maxW,"0pt");this.mergeMode=getStringOption(e.mergeMode,["consumeData","matchTemplate"]);this.minH=getMeasurement(e.minH,"0pt");this.minW=getMeasurement(e.minW,"0pt");this.name=e.name||"";this.presence=getStringOption(e.presence,["visible","hidden","inactive","invisible"]);this.relevant=getRelevant(e.relevant);this.restoreState=getStringOption(e.restoreState,["manual","auto"]);this.scope=getStringOption(e.scope,["name","none"]);this.use=e.use||"";this.usehref=e.usehref||"";this.w=e.w?getMeasurement(e.w):"";this.x=getMeasurement(e.x,"0pt");this.y=getMeasurement(e.y,"0pt");this.assist=null;this.bind=null;this.bookend=null;this.border=null;this.break=null;this.calculate=null;this.desc=null;this.extras=null;this.keep=null;this.margin=null;this.occur=null;this.overflow=null;this.pageSet=null;this.para=null;this.traversal=null;this.validate=null;this.variables=null;this.area=new XFAObjectArray;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.connect=new XFAObjectArray;this.draw=new XFAObjectArray;this.event=new XFAObjectArray;this.exObject=new XFAObjectArray;this.exclGroup=new XFAObjectArray;this.field=new XFAObjectArray;this.proto=new XFAObjectArray;this.setProperty=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}[As](){const e=this[ks]();return e instanceof SubformSet?e[As]():e}[Ds](){return!0}[Ls](){return this.layout.endsWith("-tb")&&0===this[os].attempt&&this[os].numberInLine>0||this[ks]()[Ls]()}*[Ss](){yield*getContainedChildren(this)}[ls](){return flushHTML(this)}[Jn](e,t){addHTML(this,e,t)}[ds](){return getAvailableSpace(this)}[Es](){const e=this[As]();if(!e[Es]())return!1;if(void 0!==this[os]._isSplittable)return this[os]._isSplittable;if("position"===this.layout||this.layout.includes("row")){this[os]._isSplittable=!1;return!1}if(this.keep&&"none"!==this.keep.intact){this[os]._isSplittable=!1;return!1}if(e.layout?.endsWith("-tb")&&0!==e[os].numberInLine)return!1;this[os]._isSplittable=!0;return!0}[so](e){setTabIndex(this);if(this.break){if("auto"!==this.break.after||""!==this.break.afterTarget){const e=new BreakAfter({targetType:this.break.after,target:this.break.afterTarget,startNew:this.break.startNew.toString()});e[vs]=this[vs];this[Yn](e);this.breakAfter.push(e)}if("auto"!==this.break.before||""!==this.break.beforeTarget){const e=new BreakBefore({targetType:this.break.before,target:this.break.beforeTarget,startNew:this.break.startNew.toString()});e[vs]=this[vs];this[Yn](e);this.breakBefore.push(e)}if(""!==this.break.overflowTarget){const e=new Overflow({target:this.break.overflowTarget,leader:this.break.overflowLeader,trailer:this.break.overflowTrailer});e[vs]=this[vs];this[Yn](e);this.overflow.push(e)}this[Js](this.break);this.break=null}if("hidden"===this.presence||"inactive"===this.presence)return HTMLResult.EMPTY;(this.breakBefore.children.length>1||this.breakAfter.children.length>1)&&warn("XFA - Several breakBefore or breakAfter in subforms: please file a bug.");if(this.breakBefore.children.length>=1){const e=this.breakBefore.children[0];if(handleBreak(e))return HTMLResult.breakNode(e)}if(this[os]?.afterBreakAfter)return HTMLResult.EMPTY;fixDimensions(this);const t=[],a={id:this[lo],class:[]};setAccess(this,a.class);this[os]||=Object.create(null);Object.assign(this[os],{children:t,line:null,attributes:a,attempt:0,numberInLine:0,availableSpace:{width:Math.min(this.w||1/0,e.width),height:Math.min(this.h||1/0,e.height)},width:0,height:0,prevHeight:0,currentWidth:0});const r=this[Cs](),i=r[os].noLayoutFailure,n=this[Es]();n||setFirstUnsplittable(this);if(!checkDimensions(this,e))return HTMLResult.FAILURE;const s=new Set(["area","draw","exclGroup","field","subform","subformSet"]);if(this.layout.includes("row")){const e=this[As]().columnWidths;if(Array.isArray(e)&&e.length>0){this[os].columnWidths=e;this[os].currentColumn=0}}const o=toStyle(this,"anchorType","dimensions","position","presence","border","margin","hAlign"),c=["xfaSubform"],l=layoutClass(this);l&&c.push(l);a.style=o;a.class=c;this.name&&(a.xfaName=this.name);if(this.overflow){const t=this.overflow[bs]();if(t.addLeader){t.addLeader=!1;handleOverflow(this,t.leader,e)}}this[Ks]();const h="lr-tb"===this.layout||"rl-tb"===this.layout,u=h?2:1;for(;this[os].attempt=1){const e=this.breakAfter.children[0];if(handleBreak(e)){this[os].afterBreakAfter=y;return HTMLResult.breakNode(e)}}delete this[os];return y}}class SubformSet extends XFAObject{constructor(e){super(zo,"subformSet",!0);this.id=e.id||"";this.name=e.name||"";this.relation=getStringOption(e.relation,["ordered","choice","unordered"]);this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.bookend=null;this.break=null;this.desc=null;this.extras=null;this.occur=null;this.overflow=null;this.breakAfter=new XFAObjectArray;this.breakBefore=new XFAObjectArray;this.subform=new XFAObjectArray;this.subformSet=new XFAObjectArray}*[Ss](){yield*getContainedChildren(this)}[As](){let e=this[ks]();for(;!(e instanceof Subform);)e=e[ks]();return e}[Ds](){return!0}}class SubjectDN extends ContentObject{constructor(e){super(zo,"subjectDN");this.delimiter=e.delimiter||",";this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){this[is]=new Map(this[is].split(this.delimiter).map((e=>{(e=e.split("=",2))[0]=e[0].trim();return e})))}}class SubjectDNs extends XFAObject{constructor(e){super(zo,"subjectDNs",!0);this.id=e.id||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||"";this.subjectDN=new XFAObjectArray}}class Submit extends XFAObject{constructor(e){super(zo,"submit",!0);this.embedPDF=getInteger({data:e.embedPDF,defaultValue:0,validate:e=>1===e});this.format=getStringOption(e.format,["xdp","formdata","pdf","urlencoded","xfd","xml"]);this.id=e.id||"";this.target=e.target||"";this.textEncoding=getKeyword({data:e.textEncoding?e.textEncoding.toLowerCase():"",defaultValue:"",validate:e=>["utf-8","big-five","fontspecific","gbk","gb-18030","gb-2312","ksc-5601","none","shift-jis","ucs-2","utf-16"].includes(e)||e.match(/iso-8859-\d{2}/)});this.use=e.use||"";this.usehref=e.usehref||"";this.xdpContent=e.xdpContent||"";this.encrypt=null;this.encryptData=new XFAObjectArray;this.signData=new XFAObjectArray}}class Template extends XFAObject{constructor(e){super(zo,"template",!0);this.baseProfile=getStringOption(e.baseProfile,["full","interactiveForms"]);this.extras=null;this.subform=new XFAObjectArray}[cs](){0===this.subform.children.length&&warn("XFA - No subforms in template node.");this.subform.children.length>=2&&warn("XFA - Several subforms in template node: please file a bug.");this[ro]=5e3}[Es](){return!0}[Qs](e,t){return e.startsWith("#")?[this[Is].get(e.slice(1))]:searchNode(this,t,e,!0,!0)}*[no](){if(!this.subform.children.length)return HTMLResult.success({name:"div",children:[]});this[os]={overflowNode:null,firstUnsplittable:null,currentContentArea:null,currentPageArea:null,noLayoutFailure:!1,pageNumber:1,pagePosition:"first",oddOrEven:"odd",blankOrNotBlank:"nonBlank",paraStack:[]};const e=this.subform.children[0];e.pageSet[es]();const t=e.pageSet.pageArea.children,a={name:"div",children:[]};let r=null,i=null,n=null;if(e.breakBefore.children.length>=1){i=e.breakBefore.children[0];n=i.target}else if(e.subform.children.length>=1&&e.subform.children[0].breakBefore.children.length>=1){i=e.subform.children[0].breakBefore.children[0];n=i.target}else if(e.break?.beforeTarget){i=e.break;n=i.beforeTarget}else if(e.subform.children.length>=1&&e.subform.children[0].break?.beforeTarget){i=e.subform.children[0].break;n=i.beforeTarget}if(i){const e=this[Qs](n,i[ks]());if(e instanceof PageArea){r=e;i[os]={}}}r||=t[0];r[os]={numberOfUse:1};const s=r[ks]();s[os]={numberOfUse:1,pageIndex:s.pageArea.children.indexOf(r),pageSetIndex:0};let o,c=null,l=null,h=!0,u=0,d=0;for(;;){if(h)u=0;else{a.children.pop();if(3==++u){warn("XFA - Something goes wrong: please file a bug.");return a}}o=null;this[os].currentPageArea=r;const t=r[so]().html;a.children.push(t);if(c){this[os].noLayoutFailure=!0;t.children.push(c[so](r[os].space).html);c=null}if(l){this[os].noLayoutFailure=!0;t.children.push(l[so](r[os].space).html);l=null}const i=r.contentArea.children,n=t.children.filter((e=>e.attributes.class.includes("xfaContentarea")));h=!1;this[os].firstUnsplittable=null;this[os].noLayoutFailure=!1;const flush=t=>{const a=e[ls]();if(a){h||=a.children?.length>0;n[t].children.push(a)}};for(let t=d,r=i.length;t0;n[t].children.push(u.html)}else!h&&a.children.length>1&&a.children.pop();return a}if(u.isBreak()){const e=u.breakNode;flush(t);if("auto"===e.targetType)continue;if(e.leader){c=this[Qs](e.leader,e[ks]());c=c?c[0]:null}if(e.trailer){l=this[Qs](e.trailer,e[ks]());l=l?l[0]:null}if("pageArea"===e.targetType){o=e[os].target;t=1/0}else if(e[os].target){o=e[os].target;d=e[os].index+1;t=1/0}else t=e[os].index}else if(this[os].overflowNode){const e=this[os].overflowNode;this[os].overflowNode=null;const a=e[bs](),r=a.target;a.addLeader=null!==a.leader;a.addTrailer=null!==a.trailer;flush(t);const n=t;t=1/0;if(r instanceof PageArea)o=r;else if(r instanceof ContentArea){const e=i.indexOf(r);if(-1!==e)e>n?t=e-1:d=e;else{o=r[ks]();d=o.contentArea.children.indexOf(r)}}}else flush(t)}this[os].pageNumber+=1;o&&(o[_s]()?o[os].numberOfUse+=1:o=null);r=o||r[xs]();yield null}}}class Text extends ContentObject{constructor(e){super(zo,"text");this.id=e.id||"";this.maxChars=getInteger({data:e.maxChars,defaultValue:0,validate:e=>e>=0});this.name=e.name||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}[Kn](){return!0}[Ws](e){if(e[Xs]===uo.xhtml.id){this[is]=e;return!0}warn(`XFA - Invalid content in Text: ${e[qs]}.`);return!1}[$s](e){this[is]instanceof XFAObject||super[$s](e)}[cs](){"string"==typeof this[is]&&(this[is]=this[is].replaceAll("\r\n","\n"))}[bs](){return"string"==typeof this[is]?this[is].split(/[\u2029\u2028\n]/).filter((e=>!!e)).join("\n"):this[is][io]()}[so](e){if("string"==typeof this[is]){const e=valueToHtml(this[is]).html;if(this[is].includes("\u2029")){e.name="div";e.children=[];this[is].split("\u2029").map((e=>e.split(/[\u2028\n]/).flatMap((e=>[{name:"span",value:e},{name:"br"}])))).forEach((t=>{e.children.push({name:"p",children:t})}))}else if(/[\u2028\n]/.test(this[is])){e.name="div";e.children=[];this[is].split(/[\u2028\n]/).forEach((t=>{e.children.push({name:"span",value:t},{name:"br"})}))}return HTMLResult.success(e)}return this[is][so](e)}}class TextEdit extends XFAObject{constructor(e){super(zo,"textEdit",!0);this.allowRichText=getInteger({data:e.allowRichText,defaultValue:0,validate:e=>1===e});this.hScrollPolicy=getStringOption(e.hScrollPolicy,["auto","off","on"]);this.id=e.id||"";this.multiLine=getInteger({data:e.multiLine,defaultValue:"",validate:e=>0===e||1===e});this.use=e.use||"";this.usehref=e.usehref||"";this.vScrollPolicy=getStringOption(e.vScrollPolicy,["auto","off","on"]);this.border=null;this.comb=null;this.extras=null;this.margin=null}[so](e){const t=toStyle(this,"border","font","margin");let a;const r=this[ks]()[ks]();""===this.multiLine&&(this.multiLine=r instanceof Draw?1:0);a=1===this.multiLine?{name:"textarea",attributes:{dataId:r[ns]?.[lo]||r[lo],fieldId:r[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}}:{name:"input",attributes:{type:"text",dataId:r[ns]?.[lo]||r[lo],fieldId:r[lo],class:["xfaTextfield"],style:t,"aria-label":ariaLabel(r),"aria-required":!1}};if(isRequired(r)){a.attributes["aria-required"]=!0;a.attributes.required=!0}return HTMLResult.success({name:"label",attributes:{class:["xfaLabel"]},children:[a]})}}class Time extends StringObject{constructor(e){super(zo,"time");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}[cs](){const e=this[is].trim();this[is]=e?new Date(e):null}[so](e){return valueToHtml(this[is]?this[is].toString():"")}}class TimeStamp extends XFAObject{constructor(e){super(zo,"timeStamp");this.id=e.id||"";this.server=e.server||"";this.type=getStringOption(e.type,["optional","required"]);this.use=e.use||"";this.usehref=e.usehref||""}}class ToolTip extends StringObject{constructor(e){super(zo,"toolTip");this.id=e.id||"";this.rid=e.rid||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Traversal extends XFAObject{constructor(e){super(zo,"traversal",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.traverse=new XFAObjectArray}}class Traverse extends XFAObject{constructor(e){super(zo,"traverse",!0);this.id=e.id||"";this.operation=getStringOption(e.operation,["next","back","down","first","left","right","up"]);this.ref=e.ref||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.script=null}get name(){return this.operation}[js](){return!1}}class Ui extends XFAObject{constructor(e){super(zo,"ui",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.picture=null;this.barcode=null;this.button=null;this.checkButton=null;this.choiceList=null;this.dateTimeEdit=null;this.defaultUi=null;this.imageEdit=null;this.numericEdit=null;this.passwordEdit=null;this.signature=null;this.textEdit=null}[bs](){if(void 0===this[os]){for(const e of Object.getOwnPropertyNames(this)){if("extras"===e||"picture"===e)continue;const t=this[e];if(t instanceof XFAObject){this[os]=t;return t}}this[os]=null}return this[os]}[so](e){const t=this[bs]();return t?t[so](e):HTMLResult.EMPTY}}class Validate extends XFAObject{constructor(e){super(zo,"validate",!0);this.formatTest=getStringOption(e.formatTest,["warning","disabled","error"]);this.id=e.id||"";this.nullTest=getStringOption(e.nullTest,["disabled","error","warning"]);this.scriptTest=getStringOption(e.scriptTest,["error","disabled","warning"]);this.use=e.use||"";this.usehref=e.usehref||"";this.extras=null;this.message=null;this.picture=null;this.script=null}}class Value extends XFAObject{constructor(e){super(zo,"value",!0);this.id=e.id||"";this.override=getInteger({data:e.override,defaultValue:0,validate:e=>1===e});this.relevant=getRelevant(e.relevant);this.use=e.use||"";this.usehref=e.usehref||"";this.arc=null;this.boolean=null;this.date=null;this.dateTime=null;this.decimal=null;this.exData=null;this.float=null;this.image=null;this.integer=null;this.line=null;this.rectangle=null;this.text=null;this.time=null}[ao](e){const t=this[ks]();if(t instanceof Field&&t.ui?.imageEdit){if(!this.image){this.image=new Image({});this[Yn](this.image)}this.image[is]=e[is];return}const a=e[qs];if(null===this[a]){for(const e of Object.getOwnPropertyNames(this)){const t=this[e];if(t instanceof XFAObject){this[e]=null;this[Js](t)}}this[e[qs]]=e;this[Yn](e)}else this[a][is]=e[is]}[io](){if(this.exData)return"string"==typeof this.exData[is]?this.exData[is].trim():this.exData[is][io]().trim();for(const e of Object.getOwnPropertyNames(this)){if("image"===e)continue;const t=this[e];if(t instanceof XFAObject)return(t[is]||"").toString().trim()}return null}[so](e){for(const t of Object.getOwnPropertyNames(this)){const a=this[t];if(a instanceof XFAObject)return a[so](e)}return HTMLResult.EMPTY}}class Variables extends XFAObject{constructor(e){super(zo,"variables",!0);this.id=e.id||"";this.use=e.use||"";this.usehref=e.usehref||"";this.boolean=new XFAObjectArray;this.date=new XFAObjectArray;this.dateTime=new XFAObjectArray;this.decimal=new XFAObjectArray;this.exData=new XFAObjectArray;this.float=new XFAObjectArray;this.image=new XFAObjectArray;this.integer=new XFAObjectArray;this.manifest=new XFAObjectArray;this.script=new XFAObjectArray;this.text=new XFAObjectArray;this.time=new XFAObjectArray}[js](){return!0}}class TemplateNamespace{static[ho](e,t){if(TemplateNamespace.hasOwnProperty(e)){const a=TemplateNamespace[e](t);a[to](t);return a}}static appearanceFilter(e){return new AppearanceFilter(e)}static arc(e){return new Arc(e)}static area(e){return new Area(e)}static assist(e){return new Assist(e)}static barcode(e){return new Barcode(e)}static bind(e){return new Bind(e)}static bindItems(e){return new BindItems(e)}static bookend(e){return new Bookend(e)}static boolean(e){return new BooleanElement(e)}static border(e){return new Border(e)}static break(e){return new Break(e)}static breakAfter(e){return new BreakAfter(e)}static breakBefore(e){return new BreakBefore(e)}static button(e){return new Button(e)}static calculate(e){return new Calculate(e)}static caption(e){return new Caption(e)}static certificate(e){return new Certificate(e)}static certificates(e){return new Certificates(e)}static checkButton(e){return new CheckButton(e)}static choiceList(e){return new ChoiceList(e)}static color(e){return new Color(e)}static comb(e){return new Comb(e)}static connect(e){return new Connect(e)}static contentArea(e){return new ContentArea(e)}static corner(e){return new Corner(e)}static date(e){return new DateElement(e)}static dateTime(e){return new DateTime(e)}static dateTimeEdit(e){return new DateTimeEdit(e)}static decimal(e){return new Decimal(e)}static defaultUi(e){return new DefaultUi(e)}static desc(e){return new Desc(e)}static digestMethod(e){return new DigestMethod(e)}static digestMethods(e){return new DigestMethods(e)}static draw(e){return new Draw(e)}static edge(e){return new Edge(e)}static encoding(e){return new Encoding(e)}static encodings(e){return new Encodings(e)}static encrypt(e){return new Encrypt(e)}static encryptData(e){return new EncryptData(e)}static encryption(e){return new Encryption(e)}static encryptionMethod(e){return new EncryptionMethod(e)}static encryptionMethods(e){return new EncryptionMethods(e)}static event(e){return new Event(e)}static exData(e){return new ExData(e)}static exObject(e){return new ExObject(e)}static exclGroup(e){return new ExclGroup(e)}static execute(e){return new Execute(e)}static extras(e){return new Extras(e)}static field(e){return new Field(e)}static fill(e){return new Fill(e)}static filter(e){return new Filter(e)}static float(e){return new Float(e)}static font(e){return new template_Font(e)}static format(e){return new Format(e)}static handler(e){return new Handler(e)}static hyphenation(e){return new Hyphenation(e)}static image(e){return new Image(e)}static imageEdit(e){return new ImageEdit(e)}static integer(e){return new Integer(e)}static issuers(e){return new Issuers(e)}static items(e){return new Items(e)}static keep(e){return new Keep(e)}static keyUsage(e){return new KeyUsage(e)}static line(e){return new Line(e)}static linear(e){return new Linear(e)}static lockDocument(e){return new LockDocument(e)}static manifest(e){return new Manifest(e)}static margin(e){return new Margin(e)}static mdp(e){return new Mdp(e)}static medium(e){return new Medium(e)}static message(e){return new Message(e)}static numericEdit(e){return new NumericEdit(e)}static occur(e){return new Occur(e)}static oid(e){return new Oid(e)}static oids(e){return new Oids(e)}static overflow(e){return new Overflow(e)}static pageArea(e){return new PageArea(e)}static pageSet(e){return new PageSet(e)}static para(e){return new Para(e)}static passwordEdit(e){return new PasswordEdit(e)}static pattern(e){return new template_Pattern(e)}static picture(e){return new Picture(e)}static proto(e){return new Proto(e)}static radial(e){return new Radial(e)}static reason(e){return new Reason(e)}static reasons(e){return new Reasons(e)}static rectangle(e){return new Rectangle(e)}static ref(e){return new RefElement(e)}static script(e){return new Script(e)}static setProperty(e){return new SetProperty(e)}static signData(e){return new SignData(e)}static signature(e){return new Signature(e)}static signing(e){return new Signing(e)}static solid(e){return new Solid(e)}static speak(e){return new Speak(e)}static stipple(e){return new Stipple(e)}static subform(e){return new Subform(e)}static subformSet(e){return new SubformSet(e)}static subjectDN(e){return new SubjectDN(e)}static subjectDNs(e){return new SubjectDNs(e)}static submit(e){return new Submit(e)}static template(e){return new Template(e)}static text(e){return new Text(e)}static textEdit(e){return new TextEdit(e)}static time(e){return new Time(e)}static timeStamp(e){return new TimeStamp(e)}static toolTip(e){return new ToolTip(e)}static traversal(e){return new Traversal(e)}static traverse(e){return new Traverse(e)}static ui(e){return new Ui(e)}static validate(e){return new Validate(e)}static value(e){return new Value(e)}static variables(e){return new Variables(e)}}const Jo=uo.datasets.id;function createText(e){const t=new Text({});t[is]=e;return t}class Binder{constructor(e){this.root=e;this.datasets=e.datasets;this.data=e.datasets?.data||new XmlObject(uo.datasets.id,"data");this.emptyMerge=0===this.data[ws]().length;this.root.form=this.form=e.template[as]()}_isConsumeData(){return!this.emptyMerge&&this._mergeMode}_isMatchTemplate(){return!this._isConsumeData()}bind(){this._bindElement(this.form,this.data);return this.form}getData(){return this.data}_bindValue(e,t,a){e[ns]=t;if(e[Fs]())if(t[Bs]()){const a=t[ms]();e[ao](createText(a))}else if(e instanceof Field&&"multiSelect"===e.ui?.choiceList?.open){const a=t[ws]().map((e=>e[is].trim())).join("\n");e[ao](createText(a))}else this._isConsumeData()&&warn("XFA - Nodes haven't the same type.");else!t[Bs]()||this._isMatchTemplate()?this._bindElement(e,t):warn("XFA - Nodes haven't the same type.")}_findDataByNameToConsume(e,t,a,r){if(!e)return null;let i,n;for(let r=0;r<3;r++){i=a[ys](e,!1,!0);for(;;){n=i.next().value;if(!n)break;if(t===n[Bs]())return n}if(a[Xs]===uo.datasets.id&&"data"===a[qs])break;a=a[ks]()}if(!r)return null;i=this.data[ys](e,!0,!1);n=i.next().value;if(n)return n;i=this.data[hs](e,!0);n=i.next().value;return n?.[Bs]()?n:null}_setProperties(e,t){if(e.hasOwnProperty("setProperty"))for(const{ref:a,target:r,connection:i}of e.setProperty.children){if(i)continue;if(!a)continue;const n=searchNode(this.root,t,a,!1,!1);if(!n){warn(`XFA - Invalid reference: ${a}.`);continue}const[s]=n;if(!s[Rs](this.data)){warn("XFA - Invalid node: must be a data node.");continue}const o=searchNode(this.root,e,r,!1,!1);if(!o){warn(`XFA - Invalid target: ${r}.`);continue}const[c]=o;if(!c[Rs](e)){warn("XFA - Invalid target: must be a property or subproperty.");continue}const l=c[ks]();if(c instanceof SetProperty||l instanceof SetProperty){warn("XFA - Invalid target: cannot be a setProperty or one of its properties.");continue}if(c instanceof BindItems||l instanceof BindItems){warn("XFA - Invalid target: cannot be a bindItems or one of its properties.");continue}const h=s[io](),u=c[qs];if(c instanceof XFAAttribute){const e=Object.create(null);e[u]=h;const t=Reflect.construct(Object.getPrototypeOf(l).constructor,[e]);l[u]=t[u]}else if(c.hasOwnProperty(is)){c[ns]=s;c[is]=h;c[cs]()}else warn("XFA - Invalid node to use in setProperty")}}_bindItems(e,t){if(!e.hasOwnProperty("items")||!e.hasOwnProperty("bindItems")||e.bindItems.isEmpty())return;for(const t of e.items.children)e[Js](t);e.items.clear();const a=new Items({}),r=new Items({});e[Yn](a);e.items.push(a);e[Yn](r);e.items.push(r);for(const{ref:i,labelRef:n,valueRef:s,connection:o}of e.bindItems.children){if(o)continue;if(!i)continue;const e=searchNode(this.root,t,i,!1,!1);if(e)for(const t of e){if(!t[Rs](this.datasets)){warn(`XFA - Invalid ref (${i}): must be a datasets child.`);continue}const e=searchNode(this.root,t,n,!0,!1);if(!e){warn(`XFA - Invalid label: ${n}.`);continue}const[o]=e;if(!o[Rs](this.datasets)){warn("XFA - Invalid label: must be a datasets child.");continue}const c=searchNode(this.root,t,s,!0,!1);if(!c){warn(`XFA - Invalid value: ${s}.`);continue}const[l]=c;if(!l[Rs](this.datasets)){warn("XFA - Invalid value: must be a datasets child.");continue}const h=createText(o[io]()),u=createText(l[io]());a[Yn](h);a.text.push(h);r[Yn](u);r.text.push(u)}else warn(`XFA - Invalid reference: ${i}.`)}}_bindOccurrences(e,t,a){let r;if(t.length>1){r=e[as]();r[Js](r.occur);r.occur=null}this._bindValue(e,t[0],a);this._setProperties(e,t[0]);this._bindItems(e,t[0]);if(1===t.length)return;const i=e[ks](),n=e[qs],s=i[Os](e);for(let e=1,o=t.length;et.name===e.name)).length:a[r].children.length;const n=a[Os](e)+1,s=t.initial-i;if(s){const t=e[as]();t[Js](t.occur);t.occur=null;a[r].push(t);a[Ts](n,t);for(let e=1;e0)this._bindOccurrences(r,[e[0]],null);else if(this.emptyMerge){const e=t[Xs]===Jo?-1:t[Xs],a=r[ns]=new XmlObject(e,r.name||"root");t[Yn](a);this._bindElement(r,a)}continue}if(!r[Ds]())continue;let e=!1,i=null,n=null,s=null;if(r.bind){switch(r.bind.match){case"none":this._setAndBind(r,t);continue;case"global":e=!0;break;case"dataRef":if(!r.bind.ref){warn(`XFA - ref is empty in node ${r[qs]}.`);this._setAndBind(r,t);continue}n=r.bind.ref}r.bind.picture&&(i=r.bind.picture[is])}const[o,c]=this._getOccurInfo(r);if(n){s=searchNode(this.root,t,n,!0,!1);if(null===s){s=createDataNode(this.data,t,n);if(!s)continue;this._isConsumeData()&&(s[rs]=!0);this._setAndBind(r,s);continue}this._isConsumeData()&&(s=s.filter((e=>!e[rs])));s.length>c?s=s.slice(0,c):0===s.length&&(s=null);s&&this._isConsumeData()&&s.forEach((e=>{e[rs]=!0}))}else{if(!r.name){this._setAndBind(r,t);continue}if(this._isConsumeData()){const a=[];for(;a.length0?a:null}else{s=t[ys](r.name,!1,this.emptyMerge).next().value;if(!s){if(0===o){a.push(r);continue}const e=t[Xs]===Jo?-1:t[Xs];s=r[ns]=new XmlObject(e,r.name);this.emptyMerge&&(s[rs]=!0);t[Yn](s);this._setAndBind(r,s);continue}this.emptyMerge&&(s[rs]=!0);s=[s]}}s?this._bindOccurrences(r,s,i):o>0?this._setAndBind(r,t):a.push(r)}a.forEach((e=>e[ks]()[Js](e)))}}class DataHandler{constructor(e,t){this.data=t;this.dataset=e.datasets||null}serialize(e){const t=[[-1,this.data[ws]()]];for(;t.length>0;){const a=t.at(-1),[r,i]=a;if(r+1===i.length){t.pop();continue}const n=i[++a[0]],s=e.get(n[lo]);if(s)n[ao](s);else{const t=n[us]();for(const a of t.values()){const t=e.get(a[lo]);if(t){a[ao](t);break}}}const o=n[ws]();o.length>0&&t.push([-1,o])}const a=[''];if(this.dataset)for(const e of this.dataset[ws]())"data"!==e[qs]&&e[oo](a);this.data[oo](a);a.push("");return a.join("")}}const Yo=uo.config.id;class Acrobat extends XFAObject{constructor(e){super(Yo,"acrobat",!0);this.acrobat7=null;this.autoSave=null;this.common=null;this.validate=null;this.validateApprovalSignatures=null;this.submitUrl=new XFAObjectArray}}class Acrobat7 extends XFAObject{constructor(e){super(Yo,"acrobat7",!0);this.dynamicRender=null}}class ADBE_JSConsole extends OptionObject{constructor(e){super(Yo,"ADBE_JSConsole",["delegate","Enable","Disable"])}}class ADBE_JSDebugger extends OptionObject{constructor(e){super(Yo,"ADBE_JSDebugger",["delegate","Enable","Disable"])}}class AddSilentPrint extends Option01{constructor(e){super(Yo,"addSilentPrint")}}class AddViewerPreferences extends Option01{constructor(e){super(Yo,"addViewerPreferences")}}class AdjustData extends Option10{constructor(e){super(Yo,"adjustData")}}class AdobeExtensionLevel extends IntegerObject{constructor(e){super(Yo,"adobeExtensionLevel",0,(e=>e>=1&&e<=8))}}class Agent extends XFAObject{constructor(e){super(Yo,"agent",!0);this.name=e.name?e.name.trim():"";this.common=new XFAObjectArray}}class AlwaysEmbed extends ContentObject{constructor(e){super(Yo,"alwaysEmbed")}}class Amd extends StringObject{constructor(e){super(Yo,"amd")}}class config_Area extends XFAObject{constructor(e){super(Yo,"area");this.level=getInteger({data:e.level,defaultValue:0,validate:e=>e>=1&&e<=3});this.name=getStringOption(e.name,["","barcode","coreinit","deviceDriver","font","general","layout","merge","script","signature","sourceSet","templateCache"])}}class Attributes extends OptionObject{constructor(e){super(Yo,"attributes",["preserve","delegate","ignore"])}}class AutoSave extends OptionObject{constructor(e){super(Yo,"autoSave",["disabled","enabled"])}}class Base extends StringObject{constructor(e){super(Yo,"base")}}class BatchOutput extends XFAObject{constructor(e){super(Yo,"batchOutput");this.format=getStringOption(e.format,["none","concat","zip","zipCompress"])}}class BehaviorOverride extends ContentObject{constructor(e){super(Yo,"behaviorOverride")}[cs](){this[is]=new Map(this[is].trim().split(/\s+/).filter((e=>e.includes(":"))).map((e=>e.split(":",2))))}}class Cache extends XFAObject{constructor(e){super(Yo,"cache",!0);this.templateCache=null}}class Change extends Option01{constructor(e){super(Yo,"change")}}class Common extends XFAObject{constructor(e){super(Yo,"common",!0);this.data=null;this.locale=null;this.localeSet=null;this.messaging=null;this.suppressBanner=null;this.template=null;this.validationMessaging=null;this.versionControl=null;this.log=new XFAObjectArray}}class Compress extends XFAObject{constructor(e){super(Yo,"compress");this.scope=getStringOption(e.scope,["imageOnly","document"])}}class CompressLogicalStructure extends Option01{constructor(e){super(Yo,"compressLogicalStructure")}}class CompressObjectStream extends Option10{constructor(e){super(Yo,"compressObjectStream")}}class Compression extends XFAObject{constructor(e){super(Yo,"compression",!0);this.compressLogicalStructure=null;this.compressObjectStream=null;this.level=null;this.type=null}}class Config extends XFAObject{constructor(e){super(Yo,"config",!0);this.acrobat=null;this.present=null;this.trace=null;this.agent=new XFAObjectArray}}class Conformance extends OptionObject{constructor(e){super(Yo,"conformance",["A","B"])}}class ContentCopy extends Option01{constructor(e){super(Yo,"contentCopy")}}class Copies extends IntegerObject{constructor(e){super(Yo,"copies",1,(e=>e>=1))}}class Creator extends StringObject{constructor(e){super(Yo,"creator")}}class CurrentPage extends IntegerObject{constructor(e){super(Yo,"currentPage",0,(e=>e>=0))}}class Data extends XFAObject{constructor(e){super(Yo,"data",!0);this.adjustData=null;this.attributes=null;this.incrementalLoad=null;this.outputXSL=null;this.range=null;this.record=null;this.startNode=null;this.uri=null;this.window=null;this.xsl=null;this.excludeNS=new XFAObjectArray;this.transform=new XFAObjectArray}}class Debug extends XFAObject{constructor(e){super(Yo,"debug",!0);this.uri=null}}class DefaultTypeface extends ContentObject{constructor(e){super(Yo,"defaultTypeface");this.writingScript=getStringOption(e.writingScript,["*","Arabic","Cyrillic","EastEuropeanRoman","Greek","Hebrew","Japanese","Korean","Roman","SimplifiedChinese","Thai","TraditionalChinese","Vietnamese"])}}class Destination extends OptionObject{constructor(e){super(Yo,"destination",["pdf","pcl","ps","webClient","zpl"])}}class DocumentAssembly extends Option01{constructor(e){super(Yo,"documentAssembly")}}class Driver extends XFAObject{constructor(e){super(Yo,"driver",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class DuplexOption extends OptionObject{constructor(e){super(Yo,"duplexOption",["simplex","duplexFlipLongEdge","duplexFlipShortEdge"])}}class DynamicRender extends OptionObject{constructor(e){super(Yo,"dynamicRender",["forbidden","required"])}}class Embed extends Option01{constructor(e){super(Yo,"embed")}}class config_Encrypt extends Option01{constructor(e){super(Yo,"encrypt")}}class config_Encryption extends XFAObject{constructor(e){super(Yo,"encryption",!0);this.encrypt=null;this.encryptionLevel=null;this.permissions=null}}class EncryptionLevel extends OptionObject{constructor(e){super(Yo,"encryptionLevel",["40bit","128bit"])}}class Enforce extends StringObject{constructor(e){super(Yo,"enforce")}}class Equate extends XFAObject{constructor(e){super(Yo,"equate");this.force=getInteger({data:e.force,defaultValue:1,validate:e=>0===e});this.from=e.from||"";this.to=e.to||""}}class EquateRange extends XFAObject{constructor(e){super(Yo,"equateRange");this.from=e.from||"";this.to=e.to||"";this._unicodeRange=e.unicodeRange||""}get unicodeRange(){const e=[],t=/U\+([0-9a-fA-F]+)/,a=this._unicodeRange;for(let r of a.split(",").map((e=>e.trim())).filter((e=>!!e))){r=r.split("-",2).map((e=>{const a=e.match(t);return a?parseInt(a[1],16):0}));1===r.length&&r.push(r[0]);e.push(r)}return shadow(this,"unicodeRange",e)}}class Exclude extends ContentObject{constructor(e){super(Yo,"exclude")}[cs](){this[is]=this[is].trim().split(/\s+/).filter((e=>e&&["calculate","close","enter","exit","initialize","ready","validate"].includes(e)))}}class ExcludeNS extends StringObject{constructor(e){super(Yo,"excludeNS")}}class FlipLabel extends OptionObject{constructor(e){super(Yo,"flipLabel",["usePrinterSetting","on","off"])}}class config_FontInfo extends XFAObject{constructor(e){super(Yo,"fontInfo",!0);this.embed=null;this.map=null;this.subsetBelow=null;this.alwaysEmbed=new XFAObjectArray;this.defaultTypeface=new XFAObjectArray;this.neverEmbed=new XFAObjectArray}}class FormFieldFilling extends Option01{constructor(e){super(Yo,"formFieldFilling")}}class GroupParent extends StringObject{constructor(e){super(Yo,"groupParent")}}class IfEmpty extends OptionObject{constructor(e){super(Yo,"ifEmpty",["dataValue","dataGroup","ignore","remove"])}}class IncludeXDPContent extends StringObject{constructor(e){super(Yo,"includeXDPContent")}}class IncrementalLoad extends OptionObject{constructor(e){super(Yo,"incrementalLoad",["none","forwardOnly"])}}class IncrementalMerge extends Option01{constructor(e){super(Yo,"incrementalMerge")}}class Interactive extends Option01{constructor(e){super(Yo,"interactive")}}class Jog extends OptionObject{constructor(e){super(Yo,"jog",["usePrinterSetting","none","pageSet"])}}class LabelPrinter extends XFAObject{constructor(e){super(Yo,"labelPrinter",!0);this.name=getStringOption(e.name,["zpl","dpl","ipl","tcpl"]);this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class Layout extends OptionObject{constructor(e){super(Yo,"layout",["paginate","panel"])}}class Level extends IntegerObject{constructor(e){super(Yo,"level",0,(e=>e>0))}}class Linearized extends Option01{constructor(e){super(Yo,"linearized")}}class Locale extends StringObject{constructor(e){super(Yo,"locale")}}class LocaleSet extends StringObject{constructor(e){super(Yo,"localeSet")}}class Log extends XFAObject{constructor(e){super(Yo,"log",!0);this.mode=null;this.threshold=null;this.to=null;this.uri=null}}class MapElement extends XFAObject{constructor(e){super(Yo,"map",!0);this.equate=new XFAObjectArray;this.equateRange=new XFAObjectArray}}class MediumInfo extends XFAObject{constructor(e){super(Yo,"mediumInfo",!0);this.map=null}}class config_Message extends XFAObject{constructor(e){super(Yo,"message",!0);this.msgId=null;this.severity=null}}class Messaging extends XFAObject{constructor(e){super(Yo,"messaging",!0);this.message=new XFAObjectArray}}class Mode extends OptionObject{constructor(e){super(Yo,"mode",["append","overwrite"])}}class ModifyAnnots extends Option01{constructor(e){super(Yo,"modifyAnnots")}}class MsgId extends IntegerObject{constructor(e){super(Yo,"msgId",1,(e=>e>=1))}}class NameAttr extends StringObject{constructor(e){super(Yo,"nameAttr")}}class NeverEmbed extends ContentObject{constructor(e){super(Yo,"neverEmbed")}}class NumberOfCopies extends IntegerObject{constructor(e){super(Yo,"numberOfCopies",null,(e=>e>=2&&e<=5))}}class OpenAction extends XFAObject{constructor(e){super(Yo,"openAction",!0);this.destination=null}}class Output extends XFAObject{constructor(e){super(Yo,"output",!0);this.to=null;this.type=null;this.uri=null}}class OutputBin extends StringObject{constructor(e){super(Yo,"outputBin")}}class OutputXSL extends XFAObject{constructor(e){super(Yo,"outputXSL",!0);this.uri=null}}class Overprint extends OptionObject{constructor(e){super(Yo,"overprint",["none","both","draw","field"])}}class Packets extends StringObject{constructor(e){super(Yo,"packets")}[cs](){"*"!==this[is]&&(this[is]=this[is].trim().split(/\s+/).filter((e=>["config","datasets","template","xfdf","xslt"].includes(e))))}}class PageOffset extends XFAObject{constructor(e){super(Yo,"pageOffset");this.x=getInteger({data:e.x,defaultValue:"useXDCSetting",validate:e=>!0});this.y=getInteger({data:e.y,defaultValue:"useXDCSetting",validate:e=>!0})}}class PageRange extends StringObject{constructor(e){super(Yo,"pageRange")}[cs](){const e=this[is].trim().split(/\s+/).map((e=>parseInt(e,10))),t=[];for(let a=0,r=e.length;a!1))}}class Pcl extends XFAObject{constructor(e){super(Yo,"pcl",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.pageOffset=null;this.staple=null;this.xdc=null}}class Pdf extends XFAObject{constructor(e){super(Yo,"pdf",!0);this.name=e.name||"";this.adobeExtensionLevel=null;this.batchOutput=null;this.compression=null;this.creator=null;this.encryption=null;this.fontInfo=null;this.interactive=null;this.linearized=null;this.openAction=null;this.pdfa=null;this.producer=null;this.renderPolicy=null;this.scriptModel=null;this.silentPrint=null;this.submitFormat=null;this.tagged=null;this.version=null;this.viewerPreferences=null;this.xdc=null}}class Pdfa extends XFAObject{constructor(e){super(Yo,"pdfa",!0);this.amd=null;this.conformance=null;this.includeXDPContent=null;this.part=null}}class Permissions extends XFAObject{constructor(e){super(Yo,"permissions",!0);this.accessibleContent=null;this.change=null;this.contentCopy=null;this.documentAssembly=null;this.formFieldFilling=null;this.modifyAnnots=null;this.plaintextMetadata=null;this.print=null;this.printHighQuality=null}}class PickTrayByPDFSize extends Option01{constructor(e){super(Yo,"pickTrayByPDFSize")}}class config_Picture extends StringObject{constructor(e){super(Yo,"picture")}}class PlaintextMetadata extends Option01{constructor(e){super(Yo,"plaintextMetadata")}}class Presence extends OptionObject{constructor(e){super(Yo,"presence",["preserve","dissolve","dissolveStructure","ignore","remove"])}}class Present extends XFAObject{constructor(e){super(Yo,"present",!0);this.behaviorOverride=null;this.cache=null;this.common=null;this.copies=null;this.destination=null;this.incrementalMerge=null;this.layout=null;this.output=null;this.overprint=null;this.pagination=null;this.paginationOverride=null;this.script=null;this.validate=null;this.xdp=null;this.driver=new XFAObjectArray;this.labelPrinter=new XFAObjectArray;this.pcl=new XFAObjectArray;this.pdf=new XFAObjectArray;this.ps=new XFAObjectArray;this.submitUrl=new XFAObjectArray;this.webClient=new XFAObjectArray;this.zpl=new XFAObjectArray}}class Print extends Option01{constructor(e){super(Yo,"print")}}class PrintHighQuality extends Option01{constructor(e){super(Yo,"printHighQuality")}}class PrintScaling extends OptionObject{constructor(e){super(Yo,"printScaling",["appdefault","noScaling"])}}class PrinterName extends StringObject{constructor(e){super(Yo,"printerName")}}class Producer extends StringObject{constructor(e){super(Yo,"producer")}}class Ps extends XFAObject{constructor(e){super(Yo,"ps",!0);this.name=e.name||"";this.batchOutput=null;this.fontInfo=null;this.jog=null;this.mediumInfo=null;this.outputBin=null;this.staple=null;this.xdc=null}}class Range extends ContentObject{constructor(e){super(Yo,"range")}[cs](){this[is]=this[is].split(",",2).map((e=>e.split("-").map((e=>parseInt(e.trim(),10))))).filter((e=>e.every((e=>!isNaN(e))))).map((e=>{1===e.length&&e.push(e[0]);return e}))}}class Record extends ContentObject{constructor(e){super(Yo,"record")}[cs](){this[is]=this[is].trim();const e=parseInt(this[is],10);!isNaN(e)&&e>=0&&(this[is]=e)}}class Relevant extends ContentObject{constructor(e){super(Yo,"relevant")}[cs](){this[is]=this[is].trim().split(/\s+/)}}class Rename extends ContentObject{constructor(e){super(Yo,"rename")}[cs](){this[is]=this[is].trim();(this[is].toLowerCase().startsWith("xml")||new RegExp("[\\p{L}_][\\p{L}\\d._\\p{M}-]*","u").test(this[is]))&&warn("XFA - Rename: invalid XFA name")}}class RenderPolicy extends OptionObject{constructor(e){super(Yo,"renderPolicy",["server","client"])}}class RunScripts extends OptionObject{constructor(e){super(Yo,"runScripts",["both","client","none","server"])}}class config_Script extends XFAObject{constructor(e){super(Yo,"script",!0);this.currentPage=null;this.exclude=null;this.runScripts=null}}class ScriptModel extends OptionObject{constructor(e){super(Yo,"scriptModel",["XFA","none"])}}class Severity extends OptionObject{constructor(e){super(Yo,"severity",["ignore","error","information","trace","warning"])}}class SilentPrint extends XFAObject{constructor(e){super(Yo,"silentPrint",!0);this.addSilentPrint=null;this.printerName=null}}class Staple extends XFAObject{constructor(e){super(Yo,"staple");this.mode=getStringOption(e.mode,["usePrinterSetting","on","off"])}}class StartNode extends StringObject{constructor(e){super(Yo,"startNode")}}class StartPage extends IntegerObject{constructor(e){super(Yo,"startPage",0,(e=>!0))}}class SubmitFormat extends OptionObject{constructor(e){super(Yo,"submitFormat",["html","delegate","fdf","xml","pdf"])}}class SubmitUrl extends StringObject{constructor(e){super(Yo,"submitUrl")}}class SubsetBelow extends IntegerObject{constructor(e){super(Yo,"subsetBelow",100,(e=>e>=0&&e<=100))}}class SuppressBanner extends Option01{constructor(e){super(Yo,"suppressBanner")}}class Tagged extends Option01{constructor(e){super(Yo,"tagged")}}class config_Template extends XFAObject{constructor(e){super(Yo,"template",!0);this.base=null;this.relevant=null;this.startPage=null;this.uri=null;this.xsl=null}}class Threshold extends OptionObject{constructor(e){super(Yo,"threshold",["trace","error","information","warning"])}}class To extends OptionObject{constructor(e){super(Yo,"to",["null","memory","stderr","stdout","system","uri"])}}class TemplateCache extends XFAObject{constructor(e){super(Yo,"templateCache");this.maxEntries=getInteger({data:e.maxEntries,defaultValue:5,validate:e=>e>=0})}}class Trace extends XFAObject{constructor(e){super(Yo,"trace",!0);this.area=new XFAObjectArray}}class Transform extends XFAObject{constructor(e){super(Yo,"transform",!0);this.groupParent=null;this.ifEmpty=null;this.nameAttr=null;this.picture=null;this.presence=null;this.rename=null;this.whitespace=null}}class Type extends OptionObject{constructor(e){super(Yo,"type",["none","ascii85","asciiHex","ccittfax","flate","lzw","runLength","native","xdp","mergedXDP"])}}class Uri extends StringObject{constructor(e){super(Yo,"uri")}}class config_Validate extends OptionObject{constructor(e){super(Yo,"validate",["preSubmit","prePrint","preExecute","preSave"])}}class ValidateApprovalSignatures extends ContentObject{constructor(e){super(Yo,"validateApprovalSignatures")}[cs](){this[is]=this[is].trim().split(/\s+/).filter((e=>["docReady","postSign"].includes(e)))}}class ValidationMessaging extends OptionObject{constructor(e){super(Yo,"validationMessaging",["allMessagesIndividually","allMessagesTogether","firstMessageOnly","noMessages"])}}class Version extends OptionObject{constructor(e){super(Yo,"version",["1.7","1.6","1.5","1.4","1.3","1.2"])}}class VersionControl extends XFAObject{constructor(e){super(Yo,"VersionControl");this.outputBelow=getStringOption(e.outputBelow,["warn","error","update"]);this.sourceAbove=getStringOption(e.sourceAbove,["warn","error"]);this.sourceBelow=getStringOption(e.sourceBelow,["update","maintain"])}}class ViewerPreferences extends XFAObject{constructor(e){super(Yo,"viewerPreferences",!0);this.ADBE_JSConsole=null;this.ADBE_JSDebugger=null;this.addViewerPreferences=null;this.duplexOption=null;this.enforce=null;this.numberOfCopies=null;this.pageRange=null;this.pickTrayByPDFSize=null;this.printScaling=null}}class WebClient extends XFAObject{constructor(e){super(Yo,"webClient",!0);this.name=e.name?e.name.trim():"";this.fontInfo=null;this.xdc=null}}class Whitespace extends OptionObject{constructor(e){super(Yo,"whitespace",["preserve","ltrim","normalize","rtrim","trim"])}}class Window extends ContentObject{constructor(e){super(Yo,"window")}[cs](){const e=this[is].split(",",2).map((e=>parseInt(e.trim(),10)));if(e.some((e=>isNaN(e))))this[is]=[0,0];else{1===e.length&&e.push(e[0]);this[is]=e}}}class Xdc extends XFAObject{constructor(e){super(Yo,"xdc",!0);this.uri=new XFAObjectArray;this.xsl=new XFAObjectArray}}class Xdp extends XFAObject{constructor(e){super(Yo,"xdp",!0);this.packets=null}}class Xsl extends XFAObject{constructor(e){super(Yo,"xsl",!0);this.debug=null;this.uri=null}}class Zpl extends XFAObject{constructor(e){super(Yo,"zpl",!0);this.name=e.name?e.name.trim():"";this.batchOutput=null;this.flipLabel=null;this.fontInfo=null;this.xdc=null}}class ConfigNamespace{static[ho](e,t){if(ConfigNamespace.hasOwnProperty(e))return ConfigNamespace[e](t)}static acrobat(e){return new Acrobat(e)}static acrobat7(e){return new Acrobat7(e)}static ADBE_JSConsole(e){return new ADBE_JSConsole(e)}static ADBE_JSDebugger(e){return new ADBE_JSDebugger(e)}static addSilentPrint(e){return new AddSilentPrint(e)}static addViewerPreferences(e){return new AddViewerPreferences(e)}static adjustData(e){return new AdjustData(e)}static adobeExtensionLevel(e){return new AdobeExtensionLevel(e)}static agent(e){return new Agent(e)}static alwaysEmbed(e){return new AlwaysEmbed(e)}static amd(e){return new Amd(e)}static area(e){return new config_Area(e)}static attributes(e){return new Attributes(e)}static autoSave(e){return new AutoSave(e)}static base(e){return new Base(e)}static batchOutput(e){return new BatchOutput(e)}static behaviorOverride(e){return new BehaviorOverride(e)}static cache(e){return new Cache(e)}static change(e){return new Change(e)}static common(e){return new Common(e)}static compress(e){return new Compress(e)}static compressLogicalStructure(e){return new CompressLogicalStructure(e)}static compressObjectStream(e){return new CompressObjectStream(e)}static compression(e){return new Compression(e)}static config(e){return new Config(e)}static conformance(e){return new Conformance(e)}static contentCopy(e){return new ContentCopy(e)}static copies(e){return new Copies(e)}static creator(e){return new Creator(e)}static currentPage(e){return new CurrentPage(e)}static data(e){return new Data(e)}static debug(e){return new Debug(e)}static defaultTypeface(e){return new DefaultTypeface(e)}static destination(e){return new Destination(e)}static documentAssembly(e){return new DocumentAssembly(e)}static driver(e){return new Driver(e)}static duplexOption(e){return new DuplexOption(e)}static dynamicRender(e){return new DynamicRender(e)}static embed(e){return new Embed(e)}static encrypt(e){return new config_Encrypt(e)}static encryption(e){return new config_Encryption(e)}static encryptionLevel(e){return new EncryptionLevel(e)}static enforce(e){return new Enforce(e)}static equate(e){return new Equate(e)}static equateRange(e){return new EquateRange(e)}static exclude(e){return new Exclude(e)}static excludeNS(e){return new ExcludeNS(e)}static flipLabel(e){return new FlipLabel(e)}static fontInfo(e){return new config_FontInfo(e)}static formFieldFilling(e){return new FormFieldFilling(e)}static groupParent(e){return new GroupParent(e)}static ifEmpty(e){return new IfEmpty(e)}static includeXDPContent(e){return new IncludeXDPContent(e)}static incrementalLoad(e){return new IncrementalLoad(e)}static incrementalMerge(e){return new IncrementalMerge(e)}static interactive(e){return new Interactive(e)}static jog(e){return new Jog(e)}static labelPrinter(e){return new LabelPrinter(e)}static layout(e){return new Layout(e)}static level(e){return new Level(e)}static linearized(e){return new Linearized(e)}static locale(e){return new Locale(e)}static localeSet(e){return new LocaleSet(e)}static log(e){return new Log(e)}static map(e){return new MapElement(e)}static mediumInfo(e){return new MediumInfo(e)}static message(e){return new config_Message(e)}static messaging(e){return new Messaging(e)}static mode(e){return new Mode(e)}static modifyAnnots(e){return new ModifyAnnots(e)}static msgId(e){return new MsgId(e)}static nameAttr(e){return new NameAttr(e)}static neverEmbed(e){return new NeverEmbed(e)}static numberOfCopies(e){return new NumberOfCopies(e)}static openAction(e){return new OpenAction(e)}static output(e){return new Output(e)}static outputBin(e){return new OutputBin(e)}static outputXSL(e){return new OutputXSL(e)}static overprint(e){return new Overprint(e)}static packets(e){return new Packets(e)}static pageOffset(e){return new PageOffset(e)}static pageRange(e){return new PageRange(e)}static pagination(e){return new Pagination(e)}static paginationOverride(e){return new PaginationOverride(e)}static part(e){return new Part(e)}static pcl(e){return new Pcl(e)}static pdf(e){return new Pdf(e)}static pdfa(e){return new Pdfa(e)}static permissions(e){return new Permissions(e)}static pickTrayByPDFSize(e){return new PickTrayByPDFSize(e)}static picture(e){return new config_Picture(e)}static plaintextMetadata(e){return new PlaintextMetadata(e)}static presence(e){return new Presence(e)}static present(e){return new Present(e)}static print(e){return new Print(e)}static printHighQuality(e){return new PrintHighQuality(e)}static printScaling(e){return new PrintScaling(e)}static printerName(e){return new PrinterName(e)}static producer(e){return new Producer(e)}static ps(e){return new Ps(e)}static range(e){return new Range(e)}static record(e){return new Record(e)}static relevant(e){return new Relevant(e)}static rename(e){return new Rename(e)}static renderPolicy(e){return new RenderPolicy(e)}static runScripts(e){return new RunScripts(e)}static script(e){return new config_Script(e)}static scriptModel(e){return new ScriptModel(e)}static severity(e){return new Severity(e)}static silentPrint(e){return new SilentPrint(e)}static staple(e){return new Staple(e)}static startNode(e){return new StartNode(e)}static startPage(e){return new StartPage(e)}static submitFormat(e){return new SubmitFormat(e)}static submitUrl(e){return new SubmitUrl(e)}static subsetBelow(e){return new SubsetBelow(e)}static suppressBanner(e){return new SuppressBanner(e)}static tagged(e){return new Tagged(e)}static template(e){return new config_Template(e)}static templateCache(e){return new TemplateCache(e)}static threshold(e){return new Threshold(e)}static to(e){return new To(e)}static trace(e){return new Trace(e)}static transform(e){return new Transform(e)}static type(e){return new Type(e)}static uri(e){return new Uri(e)}static validate(e){return new config_Validate(e)}static validateApprovalSignatures(e){return new ValidateApprovalSignatures(e)}static validationMessaging(e){return new ValidationMessaging(e)}static version(e){return new Version(e)}static versionControl(e){return new VersionControl(e)}static viewerPreferences(e){return new ViewerPreferences(e)}static webClient(e){return new WebClient(e)}static whitespace(e){return new Whitespace(e)}static window(e){return new Window(e)}static xdc(e){return new Xdc(e)}static xdp(e){return new Xdp(e)}static xsl(e){return new Xsl(e)}static zpl(e){return new Zpl(e)}}const Zo=uo.connectionSet.id;class ConnectionSet extends XFAObject{constructor(e){super(Zo,"connectionSet",!0);this.wsdlConnection=new XFAObjectArray;this.xmlConnection=new XFAObjectArray;this.xsdConnection=new XFAObjectArray}}class EffectiveInputPolicy extends XFAObject{constructor(e){super(Zo,"effectiveInputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class EffectiveOutputPolicy extends XFAObject{constructor(e){super(Zo,"effectiveOutputPolicy");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class Operation extends StringObject{constructor(e){super(Zo,"operation");this.id=e.id||"";this.input=e.input||"";this.name=e.name||"";this.output=e.output||"";this.use=e.use||"";this.usehref=e.usehref||""}}class RootElement extends StringObject{constructor(e){super(Zo,"rootElement");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAction extends StringObject{constructor(e){super(Zo,"soapAction");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class SoapAddress extends StringObject{constructor(e){super(Zo,"soapAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class connection_set_Uri extends StringObject{constructor(e){super(Zo,"uri");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlAddress extends StringObject{constructor(e){super(Zo,"wsdlAddress");this.id=e.id||"";this.name=e.name||"";this.use=e.use||"";this.usehref=e.usehref||""}}class WsdlConnection extends XFAObject{constructor(e){super(Zo,"wsdlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.effectiveInputPolicy=null;this.effectiveOutputPolicy=null;this.operation=null;this.soapAction=null;this.soapAddress=null;this.wsdlAddress=null}}class XmlConnection extends XFAObject{constructor(e){super(Zo,"xmlConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.uri=null}}class XsdConnection extends XFAObject{constructor(e){super(Zo,"xsdConnection",!0);this.dataDescription=e.dataDescription||"";this.name=e.name||"";this.rootElement=null;this.uri=null}}class ConnectionSetNamespace{static[ho](e,t){if(ConnectionSetNamespace.hasOwnProperty(e))return ConnectionSetNamespace[e](t)}static connectionSet(e){return new ConnectionSet(e)}static effectiveInputPolicy(e){return new EffectiveInputPolicy(e)}static effectiveOutputPolicy(e){return new EffectiveOutputPolicy(e)}static operation(e){return new Operation(e)}static rootElement(e){return new RootElement(e)}static soapAction(e){return new SoapAction(e)}static soapAddress(e){return new SoapAddress(e)}static uri(e){return new connection_set_Uri(e)}static wsdlAddress(e){return new WsdlAddress(e)}static wsdlConnection(e){return new WsdlConnection(e)}static xmlConnection(e){return new XmlConnection(e)}static xsdConnection(e){return new XsdConnection(e)}}const Qo=uo.datasets.id;class datasets_Data extends XmlObject{constructor(e){super(Qo,"data",e)}[Ns](){return!0}}class Datasets extends XFAObject{constructor(e){super(Qo,"datasets",!0);this.data=null;this.Signature=null}[Ws](e){const t=e[qs];("data"===t&&e[Xs]===Qo||"Signature"===t&&e[Xs]===uo.signature.id)&&(this[t]=e);this[Yn](e)}}class DatasetsNamespace{static[ho](e,t){if(DatasetsNamespace.hasOwnProperty(e))return DatasetsNamespace[e](t)}static datasets(e){return new Datasets(e)}static data(e){return new datasets_Data(e)}}const ec=uo.localeSet.id;class CalendarSymbols extends XFAObject{constructor(e){super(ec,"calendarSymbols",!0);this.name="gregorian";this.dayNames=new XFAObjectArray(2);this.eraNames=null;this.meridiemNames=null;this.monthNames=new XFAObjectArray(2)}}class CurrencySymbol extends StringObject{constructor(e){super(ec,"currencySymbol");this.name=getStringOption(e.name,["symbol","isoname","decimal"])}}class CurrencySymbols extends XFAObject{constructor(e){super(ec,"currencySymbols",!0);this.currencySymbol=new XFAObjectArray(3)}}class DatePattern extends StringObject{constructor(e){super(ec,"datePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class DatePatterns extends XFAObject{constructor(e){super(ec,"datePatterns",!0);this.datePattern=new XFAObjectArray(4)}}class DateTimeSymbols extends ContentObject{constructor(e){super(ec,"dateTimeSymbols")}}class Day extends StringObject{constructor(e){super(ec,"day")}}class DayNames extends XFAObject{constructor(e){super(ec,"dayNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.day=new XFAObjectArray(7)}}class Era extends StringObject{constructor(e){super(ec,"era")}}class EraNames extends XFAObject{constructor(e){super(ec,"eraNames",!0);this.era=new XFAObjectArray(2)}}class locale_set_Locale extends XFAObject{constructor(e){super(ec,"locale",!0);this.desc=e.desc||"";this.name="isoname";this.calendarSymbols=null;this.currencySymbols=null;this.datePatterns=null;this.dateTimeSymbols=null;this.numberPatterns=null;this.numberSymbols=null;this.timePatterns=null;this.typeFaces=null}}class locale_set_LocaleSet extends XFAObject{constructor(e){super(ec,"localeSet",!0);this.locale=new XFAObjectArray}}class Meridiem extends StringObject{constructor(e){super(ec,"meridiem")}}class MeridiemNames extends XFAObject{constructor(e){super(ec,"meridiemNames",!0);this.meridiem=new XFAObjectArray(2)}}class Month extends StringObject{constructor(e){super(ec,"month")}}class MonthNames extends XFAObject{constructor(e){super(ec,"monthNames",!0);this.abbr=getInteger({data:e.abbr,defaultValue:0,validate:e=>1===e});this.month=new XFAObjectArray(12)}}class NumberPattern extends StringObject{constructor(e){super(ec,"numberPattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class NumberPatterns extends XFAObject{constructor(e){super(ec,"numberPatterns",!0);this.numberPattern=new XFAObjectArray(4)}}class NumberSymbol extends StringObject{constructor(e){super(ec,"numberSymbol");this.name=getStringOption(e.name,["decimal","grouping","percent","minus","zero"])}}class NumberSymbols extends XFAObject{constructor(e){super(ec,"numberSymbols",!0);this.numberSymbol=new XFAObjectArray(5)}}class TimePattern extends StringObject{constructor(e){super(ec,"timePattern");this.name=getStringOption(e.name,["full","long","med","short"])}}class TimePatterns extends XFAObject{constructor(e){super(ec,"timePatterns",!0);this.timePattern=new XFAObjectArray(4)}}class TypeFace extends XFAObject{constructor(e){super(ec,"typeFace",!0);this.name=""|e.name}}class TypeFaces extends XFAObject{constructor(e){super(ec,"typeFaces",!0);this.typeFace=new XFAObjectArray}}class LocaleSetNamespace{static[ho](e,t){if(LocaleSetNamespace.hasOwnProperty(e))return LocaleSetNamespace[e](t)}static calendarSymbols(e){return new CalendarSymbols(e)}static currencySymbol(e){return new CurrencySymbol(e)}static currencySymbols(e){return new CurrencySymbols(e)}static datePattern(e){return new DatePattern(e)}static datePatterns(e){return new DatePatterns(e)}static dateTimeSymbols(e){return new DateTimeSymbols(e)}static day(e){return new Day(e)}static dayNames(e){return new DayNames(e)}static era(e){return new Era(e)}static eraNames(e){return new EraNames(e)}static locale(e){return new locale_set_Locale(e)}static localeSet(e){return new locale_set_LocaleSet(e)}static meridiem(e){return new Meridiem(e)}static meridiemNames(e){return new MeridiemNames(e)}static month(e){return new Month(e)}static monthNames(e){return new MonthNames(e)}static numberPattern(e){return new NumberPattern(e)}static numberPatterns(e){return new NumberPatterns(e)}static numberSymbol(e){return new NumberSymbol(e)}static numberSymbols(e){return new NumberSymbols(e)}static timePattern(e){return new TimePattern(e)}static timePatterns(e){return new TimePatterns(e)}static typeFace(e){return new TypeFace(e)}static typeFaces(e){return new TypeFaces(e)}}const tc=uo.signature.id;class signature_Signature extends XFAObject{constructor(e){super(tc,"signature",!0)}}class SignatureNamespace{static[ho](e,t){if(SignatureNamespace.hasOwnProperty(e))return SignatureNamespace[e](t)}static signature(e){return new signature_Signature(e)}}const ac=uo.stylesheet.id;class Stylesheet extends XFAObject{constructor(e){super(ac,"stylesheet",!0)}}class StylesheetNamespace{static[ho](e,t){if(StylesheetNamespace.hasOwnProperty(e))return StylesheetNamespace[e](t)}static stylesheet(e){return new Stylesheet(e)}}const rc=uo.xdp.id;class xdp_Xdp extends XFAObject{constructor(e){super(rc,"xdp",!0);this.uuid=e.uuid||"";this.timeStamp=e.timeStamp||"";this.config=null;this.connectionSet=null;this.datasets=null;this.localeSet=null;this.stylesheet=new XFAObjectArray;this.template=null}[zs](e){const t=uo[e[qs]];return t&&e[Xs]===t.id}}class XdpNamespace{static[ho](e,t){if(XdpNamespace.hasOwnProperty(e))return XdpNamespace[e](t)}static xdp(e){return new xdp_Xdp(e)}}const ic=uo.xhtml.id,nc=Symbol(),sc=new Set(["color","font","font-family","font-size","font-stretch","font-style","font-weight","margin","margin-bottom","margin-left","margin-right","margin-top","letter-spacing","line-height","orphans","page-break-after","page-break-before","page-break-inside","tab-interval","tab-stop","text-align","text-decoration","text-indent","vertical-align","widows","kerning-mode","xfa-font-horizontal-scale","xfa-font-vertical-scale","xfa-spacerun","xfa-tab-stops"]),oc=new Map([["page-break-after","breakAfter"],["page-break-before","breakBefore"],["page-break-inside","breakInside"],["kerning-mode",e=>"none"===e?"none":"normal"],["xfa-font-horizontal-scale",e=>`scaleX(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-font-vertical-scale",e=>`scaleY(${Math.max(0,parseInt(e)/100).toFixed(2)})`],["xfa-spacerun",""],["xfa-tab-stops",""],["font-size",(e,t)=>measureToString(.99*(e=t.fontSize=Math.abs(getMeasurement(e))))],["letter-spacing",e=>measureToString(getMeasurement(e))],["line-height",e=>measureToString(getMeasurement(e))],["margin",e=>measureToString(getMeasurement(e))],["margin-bottom",e=>measureToString(getMeasurement(e))],["margin-left",e=>measureToString(getMeasurement(e))],["margin-right",e=>measureToString(getMeasurement(e))],["margin-top",e=>measureToString(getMeasurement(e))],["text-indent",e=>measureToString(getMeasurement(e))],["font-family",e=>e],["vertical-align",e=>measureToString(getMeasurement(e))]]),cc=/\s+/g,lc=/[\r\n]+/g,hc=/\r\n?/g;function mapStyle(e,t,a){const r=Object.create(null);if(!e)return r;const i=Object.create(null);for(const[t,a]of e.split(";").map((e=>e.split(":",2)))){const e=oc.get(t);if(""===e)continue;let n=a;e&&(n="string"==typeof e?e:e(a,i));t.endsWith("scale")?r.transform=r.transform?`${r[t]} ${n}`:n:r[t.replaceAll(/-([a-zA-Z])/g,((e,t)=>t.toUpperCase()))]=n}r.fontFamily&&setFontFamily({typeface:r.fontFamily,weight:r.fontWeight||"normal",posture:r.fontStyle||"normal",size:i.fontSize||0},t,t[vs].fontFinder,r);if(a&&r.verticalAlign&&"0px"!==r.verticalAlign&&r.fontSize){const e=.583,t=.333,a=getMeasurement(r.fontSize);r.fontSize=measureToString(a*e);r.verticalAlign=measureToString(Math.sign(getMeasurement(r.verticalAlign))*a*t)}a&&r.fontSize&&(r.fontSize=`calc(${r.fontSize} * var(--total-scale-factor))`);fixTextIndent(r);return r}const uc=new Set(["body","html"]);class XhtmlObject extends XmlObject{constructor(e,t){super(ic,t);this[nc]=!1;this.style=e.style||""}[Qn](e){super[Qn](e);this.style=function checkStyle(e){return e.style?e.style.split(";").filter((e=>!!e.trim())).map((e=>e.split(":",2).map((e=>e.trim())))).filter((([t,a])=>{"font-family"===t&&e[vs].usedTypefaces.add(a);return sc.has(t)})).map((e=>e.join(":"))).join(";"):""}(this)}[Kn](){return!uc.has(this[qs])}[$s](e,t=!1){if(t)this[nc]=!0;else{e=e.replaceAll(lc,"");this.style.includes("xfa-spacerun:yes")||(e=e.replaceAll(cc," "))}e&&(this[is]+=e)}[Gs](e,t=!0){const a=Object.create(null),r={top:NaN,bottom:NaN,left:NaN,right:NaN};let i=null;for(const[e,t]of this.style.split(";").map((e=>e.split(":",2))))switch(e){case"font-family":a.typeface=stripQuotes(t);break;case"font-size":a.size=getMeasurement(t);break;case"font-weight":a.weight=t;break;case"font-style":a.posture=t;break;case"letter-spacing":a.letterSpacing=getMeasurement(t);break;case"margin":const e=t.split(/ \t/).map((e=>getMeasurement(e)));switch(e.length){case 1:r.top=r.bottom=r.left=r.right=e[0];break;case 2:r.top=r.bottom=e[0];r.left=r.right=e[1];break;case 3:r.top=e[0];r.bottom=e[2];r.left=r.right=e[1];break;case 4:r.top=e[0];r.left=e[1];r.bottom=e[2];r.right=e[3]}break;case"margin-top":r.top=getMeasurement(t);break;case"margin-bottom":r.bottom=getMeasurement(t);break;case"margin-left":r.left=getMeasurement(t);break;case"margin-right":r.right=getMeasurement(t);break;case"line-height":i=getMeasurement(t)}e.pushData(a,r,i);if(this[is])e.addString(this[is]);else for(const t of this[ws]())"#text"!==t[qs]?t[Gs](e):e.addString(t[is]);t&&e.popFont()}[so](e){const t=[];this[os]={children:t};this[Zn]({});if(0===t.length&&!this[is])return HTMLResult.EMPTY;let a;a=this[nc]?this[is]?this[is].replaceAll(hc,"\n"):void 0:this[is]||void 0;return HTMLResult.success({name:this[qs],attributes:{href:this.href,style:mapStyle(this.style,this,this[nc])},children:t,value:a})}}class A extends XhtmlObject{constructor(e){super(e,"a");this.href=fixURL(e.href)||""}}class B extends XhtmlObject{constructor(e){super(e,"b")}[Gs](e){e.pushFont({weight:"bold"});super[Gs](e);e.popFont()}}class Body extends XhtmlObject{constructor(e){super(e,"body")}[so](e){const t=super[so](e),{html:a}=t;if(!a)return HTMLResult.EMPTY;a.name="div";a.attributes.class=["xfaRich"];return t}}class Br extends XhtmlObject{constructor(e){super(e,"br")}[io](){return"\n"}[Gs](e){e.addString("\n")}[so](e){return HTMLResult.success({name:"br"})}}class Html extends XhtmlObject{constructor(e){super(e,"html")}[so](e){const t=[];this[os]={children:t};this[Zn]({});if(0===t.length)return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},value:this[is]||""});if(1===t.length){const e=t[0];if(e.attributes?.class.includes("xfaRich"))return HTMLResult.success(e)}return HTMLResult.success({name:"div",attributes:{class:["xfaRich"],style:{}},children:t})}}class I extends XhtmlObject{constructor(e){super(e,"i")}[Gs](e){e.pushFont({posture:"italic"});super[Gs](e);e.popFont()}}class Li extends XhtmlObject{constructor(e){super(e,"li")}}class Ol extends XhtmlObject{constructor(e){super(e,"ol")}}class P extends XhtmlObject{constructor(e){super(e,"p")}[Gs](e){super[Gs](e,!1);e.addString("\n");e.addPara();e.popFont()}[io](){return this[ks]()[ws]().at(-1)===this?super[io]():super[io]()+"\n"}}class Span extends XhtmlObject{constructor(e){super(e,"span")}}class Sub extends XhtmlObject{constructor(e){super(e,"sub")}}class Sup extends XhtmlObject{constructor(e){super(e,"sup")}}class Ul extends XhtmlObject{constructor(e){super(e,"ul")}}class XhtmlNamespace{static[ho](e,t){if(XhtmlNamespace.hasOwnProperty(e))return XhtmlNamespace[e](t)}static a(e){return new A(e)}static b(e){return new B(e)}static body(e){return new Body(e)}static br(e){return new Br(e)}static html(e){return new Html(e)}static i(e){return new I(e)}static li(e){return new Li(e)}static ol(e){return new Ol(e)}static p(e){return new P(e)}static span(e){return new Span(e)}static sub(e){return new Sub(e)}static sup(e){return new Sup(e)}static ul(e){return new Ul(e)}}const dc={config:ConfigNamespace,connection:ConnectionSetNamespace,datasets:DatasetsNamespace,localeSet:LocaleSetNamespace,signature:SignatureNamespace,stylesheet:StylesheetNamespace,template:TemplateNamespace,xdp:XdpNamespace,xhtml:XhtmlNamespace};class UnknownNamespace{constructor(e){this.namespaceId=e}[ho](e,t){return new XmlObject(this.namespaceId,e,t)}}class Root extends XFAObject{constructor(e){super(-1,"root",Object.create(null));this.element=null;this[Is]=e}[Ws](e){this.element=e;return!0}[cs](){super[cs]();if(this.element.template instanceof Template){this[Is].set(Ys,this.element);this.element.template[Zs](this[Is]);this.element.template[Is]=this[Is]}}}class Empty extends XFAObject{constructor(){super(-1,"",Object.create(null))}[Ws](e){return!1}}class Builder{constructor(e=null){this._namespaceStack=[];this._nsAgnosticLevel=0;this._namespacePrefixes=new Map;this._namespaces=new Map;this._nextNsId=Math.max(...Object.values(uo).map((({id:e})=>e)));this._currentNamespace=e||new UnknownNamespace(++this._nextNsId)}buildRoot(e){return new Root(e)}build({nsPrefix:e,name:t,attributes:a,namespace:r,prefixes:i}){const n=null!==r;if(n){this._namespaceStack.push(this._currentNamespace);this._currentNamespace=this._searchNamespace(r)}i&&this._addNamespacePrefix(i);if(a.hasOwnProperty(Hs)){const e=dc.datasets,t=a[Hs];let r=null;for(const[a,i]of Object.entries(t)){if(this._getNamespaceToUse(a)===e){r={xfa:i};break}}r?a[Hs]=r:delete a[Hs]}const s=this._getNamespaceToUse(e),o=s?.[ho](t,a)||new Empty;o[Ns]()&&this._nsAgnosticLevel++;(n||i||o[Ns]())&&(o[ts]={hasNamespace:n,prefixes:i,nsAgnostic:o[Ns]()});return o}isNsAgnostic(){return this._nsAgnosticLevel>0}_searchNamespace(e){let t=this._namespaces.get(e);if(t)return t;for(const[a,{check:r}]of Object.entries(uo))if(r(e)){t=dc[a];if(t){this._namespaces.set(e,t);return t}break}t=new UnknownNamespace(++this._nextNsId);this._namespaces.set(e,t);return t}_addNamespacePrefix(e){for(const{prefix:t,value:a}of e){const e=this._searchNamespace(a);let r=this._namespacePrefixes.get(t);if(!r){r=[];this._namespacePrefixes.set(t,r)}r.push(e)}}_getNamespaceToUse(e){if(!e)return this._currentNamespace;const t=this._namespacePrefixes.get(e);if(t?.length>0)return t.at(-1);warn(`Unknown namespace prefix: ${e}.`);return null}clean(e){const{hasNamespace:t,prefixes:a,nsAgnostic:r}=e;t&&(this._currentNamespace=this._namespaceStack.pop());a&&a.forEach((({prefix:e})=>{this._namespacePrefixes.get(e).pop()}));r&&this._nsAgnosticLevel--}}class XFAParser extends XMLParserBase{constructor(e=null,t=!1){super();this._builder=new Builder(e);this._stack=[];this._globalData={usedTypefaces:new Set};this._ids=new Map;this._current=this._builder.buildRoot(this._ids);this._errorCode=Pn;this._whiteRegex=/^\s+$/;this._nbsps=/\xa0+/g;this._richText=t}parse(e){this.parseXml(e);if(this._errorCode===Pn){this._current[cs]();return this._current.element}}onText(e){e=e.replace(this._nbsps,(e=>e.slice(1)+" "));this._richText||this._current[Kn]()?this._current[$s](e,this._richText):this._whiteRegex.test(e)||this._current[$s](e.trim())}onCdata(e){this._current[$s](e)}_mkAttributes(e,t){let a=null,r=null;const i=Object.create({});for(const{name:n,value:s}of e)if("xmlns"===n)a?warn(`XFA - multiple namespace definition in <${t}>`):a=s;else if(n.startsWith("xmlns:")){const e=n.substring(6);r??=[];r.push({prefix:e,value:s})}else{const e=n.indexOf(":");if(-1===e)i[n]=s;else{const t=i[Hs]??=Object.create(null),[a,r]=[n.slice(0,e),n.slice(e+1)];(t[a]||=Object.create(null))[r]=s}}return[a,r,i]}_getNameAndPrefix(e,t){const a=e.indexOf(":");return-1===a?[e,null]:[e.substring(a+1),t?"":e.substring(0,a)]}onBeginElement(e,t,a){const[r,i,n]=this._mkAttributes(t,e),[s,o]=this._getNameAndPrefix(e,this._builder.isNsAgnostic()),c=this._builder.build({nsPrefix:o,name:s,attributes:n,namespace:r,prefixes:i});c[vs]=this._globalData;if(a){c[cs]();this._current[Ws](c)&&c[eo](this._ids);c[Qn](this._builder)}else{this._stack.push(this._current);this._current=c}}onEndElement(e){const t=this._current;if(t[Ms]()&&"string"==typeof t[is]){const e=new XFAParser;e._globalData=this._globalData;const a=e.parse(t[is]);t[is]=null;t[Ws](a)}t[cs]();this._current=this._stack.pop();this._current[Ws](t)&&t[eo](this._ids);t[Qn](this._builder)}onError(e){this._errorCode=e}}class XFAFactory{constructor(e){try{this.root=(new XFAParser).parse(XFAFactory._createDocument(e));const t=new Binder(this.root);this.form=t.bind();this.dataHandler=new DataHandler(this.root,t.getData());this.form[vs].template=this.form}catch(e){warn(`XFA - an error occurred during parsing and binding: ${e}`)}}isValid(){return!(!this.root||!this.form)}_createPagesHelper(){const e=this.form[no]();return new Promise(((t,a)=>{const nextIteration=()=>{try{const a=e.next();a.done?t(a.value):setTimeout(nextIteration,0)}catch(e){a(e)}};setTimeout(nextIteration,0)}))}async _createPages(){try{this.pages=await this._createPagesHelper();this.dims=this.pages.children.map((e=>{const{width:t,height:a}=e.attributes.style;return[0,0,parseInt(t),parseInt(a)]}))}catch(e){warn(`XFA - an error occurred during layout: ${e}`)}}getBoundingBox(e){return this.dims[e]}async getNumPages(){this.pages||await this._createPages();return this.dims.length}setImages(e){this.form[vs].images=e}setFonts(e){this.form[vs].fontFinder=new FontFinder(e);const t=[];for(let e of this.form[vs].usedTypefaces){e=stripQuotes(e);this.form[vs].fontFinder.find(e)||t.push(e)}return t.length>0?t:null}appendFonts(e,t){this.form[vs].fontFinder.add(e,t)}async getPages(){this.pages||await this._createPages();const e=this.pages;this.pages=null;return e}serializeData(e){return this.dataHandler.serialize(e)}static _createDocument(e){return e["/xdp:xdp"]?Object.values(e).join(""):e["xdp:xdp"]}static getRichTextAsHtml(e){if(!e||"string"!=typeof e)return null;try{let t=new XFAParser(XhtmlNamespace,!0).parse(e);if(!["body","xhtml"].includes(t[qs])){const e=XhtmlNamespace.body({});e[Yn](t);t=e}const a=t[so]();if(!a.success)return null;const{html:r}=a,{attributes:i}=r;if(i){i.class&&(i.class=i.class.filter((e=>!e.startsWith("xfa"))));i.dir="auto"}return{html:r,str:t[io]()}}catch(e){warn(`XFA - an error occurred during parsing of rich text: ${e}`)}return null}}class AnnotationFactory{static createGlobals(e){return Promise.all([e.ensureCatalog("acroForm"),e.ensureDoc("xfaDatasets"),e.ensureCatalog("structTreeRoot"),e.ensureCatalog("baseUrl"),e.ensureCatalog("attachments"),e.ensureCatalog("globalColorSpaceCache")]).then((([t,a,r,i,n,s])=>({pdfManager:e,acroForm:t instanceof Dict?t:Dict.empty,xfaDatasets:a,structTreeRoot:r,baseUrl:i,attachments:n,globalColorSpaceCache:s})),(e=>{warn(`createGlobals: "${e}".`);return null}))}static async create(e,t,a,r,i,n,s){const o=i?await this._getPageIndex(e,t,a.pdfManager):null;return a.pdfManager.ensure(this,"_create",[e,t,a,r,i,n,o,s])}static _create(e,t,a,r,i=!1,n=null,s=null,o=null){const c=e.fetchIfRef(t);if(!(c instanceof Dict))return;const{acroForm:l,pdfManager:h}=a,u=t instanceof Ref?t.toString():`annot_${r.createObjId()}`;let d=c.get("Subtype");d=d instanceof Name?d.name:null;const f={xref:e,ref:t,dict:c,subtype:d,id:u,annotationGlobals:a,collectFields:i,orphanFields:n,needAppearances:!i&&!0===l.get("NeedAppearances"),pageIndex:s,evaluatorOptions:h.evaluatorOptions,pageRef:o};switch(d){case"Link":return new LinkAnnotation(f);case"Text":return new TextAnnotation(f);case"Widget":let e=getInheritableProperty({dict:c,key:"FT"});e=e instanceof Name?e.name:null;switch(e){case"Tx":return new TextWidgetAnnotation(f);case"Btn":return new ButtonWidgetAnnotation(f);case"Ch":return new ChoiceWidgetAnnotation(f);case"Sig":return new SignatureWidgetAnnotation(f)}warn(`Unimplemented widget field type "${e}", falling back to base field type.`);return new WidgetAnnotation(f);case"Popup":return new PopupAnnotation(f);case"FreeText":return new FreeTextAnnotation(f);case"Line":return new LineAnnotation(f);case"Square":return new SquareAnnotation(f);case"Circle":return new CircleAnnotation(f);case"PolyLine":return new PolylineAnnotation(f);case"Polygon":return new PolygonAnnotation(f);case"Caret":return new CaretAnnotation(f);case"Ink":return new InkAnnotation(f);case"Highlight":return new HighlightAnnotation(f);case"Underline":return new UnderlineAnnotation(f);case"Squiggly":return new SquigglyAnnotation(f);case"StrikeOut":return new StrikeOutAnnotation(f);case"Stamp":return new StampAnnotation(f);case"FileAttachment":return new FileAttachmentAnnotation(f);default:i||warn(d?`Unimplemented annotation type "${d}", falling back to base annotation.`:"Annotation is missing the required /Subtype.");return new Annotation(f)}}static async _getPageIndex(e,t,a){try{const r=await e.fetchIfRefAsync(t);if(!(r instanceof Dict))return-1;const i=r.getRaw("P");if(i instanceof Ref)try{return await a.ensureCatalog("getPageIndex",[i])}catch(e){info(`_getPageIndex -- not a valid page reference: "${e}".`)}if(r.has("Kids"))return-1;const n=await a.ensureDoc("numPages");for(let e=0;ee/255))}function getQuadPoints(e,t){const a=e.getArray("QuadPoints");if(!isNumberArray(a,null)||0===a.length||a.length%8>0)return null;const r=new Float32Array(a.length);for(let e=0,i=a.length;et[2]||gt[3]))return null;r.set([d,p,f,p,d,g,f,g],e)}return r}function getTransformMatrix(e,t,a){const r=new Float32Array([1/0,1/0,-1/0,-1/0]);Util.axialAlignedBoundingBox(t,a,r);const[i,n,s,o]=r;if(i===s||n===o)return[1,0,0,1,e[0],e[1]];const c=(e[2]-e[0])/(s-i),l=(e[3]-e[1])/(o-n);return[c,0,0,l,e[0]-i*c,e[1]-n*l]}class Annotation{constructor(e){const{dict:t,xref:a,annotationGlobals:r,ref:i,orphanFields:n}=e,s=n?.get(i);s&&t.set("Parent",s);this.setTitle(t.get("T"));this.setContents(t.get("Contents"));this.setModificationDate(t.get("M"));this.setFlags(t.get("F"));this.setRectangle(t.getArray("Rect"));this.setColor(t.getArray("C"));this.setBorderStyle(t);this.setAppearance(t);this.setOptionalContent(t);const o=t.get("MK");this.setBorderAndBackgroundColors(o);this.setRotation(o,t);this.ref=e.ref instanceof Ref?e.ref:null;this._streams=[];this.appearance&&this._streams.push(this.appearance);const c=!!(this.flags&ee),l=!!(this.flags&te);this.data={annotationFlags:this.flags,borderStyle:this.borderStyle,color:this.color,backgroundColor:this.backgroundColor,borderColor:this.borderColor,rotation:this.rotation,contentsObj:this._contents,hasAppearance:!!this.appearance,id:e.id,modificationDate:this.modificationDate,rect:this.rectangle,subtype:e.subtype,hasOwnCanvas:!1,noRotate:!!(this.flags&Z),noHTML:c&&l,isEditable:!1,structParent:-1};if(r.structTreeRoot){let a=t.get("StructParent");this.data.structParent=a=Number.isInteger(a)&&a>=0?a:-1;r.structTreeRoot.addAnnotationIdToPage(e.pageRef,a)}if(e.collectFields){const r=t.get("Kids");if(Array.isArray(r)){const e=[];for(const t of r)t instanceof Ref&&e.push(t.toString());0!==e.length&&(this.data.kidIds=e)}this.data.actions=collectActions(a,t,ye);this.data.fieldName=this._constructFieldName(t);this.data.pageIndex=e.pageIndex}const h=t.get("IT");h instanceof Name&&(this.data.it=h.name);this._isOffscreenCanvasSupported=e.evaluatorOptions.isOffscreenCanvasSupported;this._fallbackFontDict=null;this._needAppearances=!1}_hasFlag(e,t){return!!(e&t)}_buildFlags(e,t){let{flags:a}=this;if(void 0===e){if(void 0===t)return;return t?a&~Y:a&~J|Y}if(e){a|=Y;return t?a&~Q|J:a&~J|Q}a&=~(J|Q);return t?a&~Y:a|Y}_isViewable(e){return!this._hasFlag(e,K)&&!this._hasFlag(e,Q)}_isPrintable(e){return this._hasFlag(e,Y)&&!this._hasFlag(e,J)&&!this._hasFlag(e,K)}mustBeViewed(e,t){const a=e?.get(this.data.id)?.noView;return void 0!==a?!a:this.viewable&&!this._hasFlag(this.flags,J)}mustBePrinted(e){const t=e?.get(this.data.id)?.noPrint;return void 0!==t?!t:this.printable}mustBeViewedWhenEditing(e,t=null){return e?!this.data.isEditable:!t?.has(this.data.id)}get viewable(){return null!==this.data.quadPoints&&(0===this.flags||this._isViewable(this.flags))}get printable(){return null!==this.data.quadPoints&&(0!==this.flags&&this._isPrintable(this.flags))}_parseStringHelper(e){const t="string"==typeof e?stringToPDFString(e):"";return{str:t,dir:t&&"rtl"===bidi(t).dir?"rtl":"ltr"}}setDefaultAppearance(e){const{dict:t,annotationGlobals:a}=e,r=getInheritableProperty({dict:t,key:"DA"})||a.acroForm.get("DA");this._defaultAppearance="string"==typeof r?r:"";this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance)}setTitle(e){this._title=this._parseStringHelper(e)}setContents(e){this._contents=this._parseStringHelper(e)}setModificationDate(e){this.modificationDate="string"==typeof e?e:null}setFlags(e){this.flags=Number.isInteger(e)&&e>0?e:0;this.flags&K&&"Annotation"!==this.constructor.name&&(this.flags^=K)}hasFlag(e){return this._hasFlag(this.flags,e)}setRectangle(e){this.rectangle=lookupNormalRect(e,[0,0,0,0])}setColor(e){this.color=getRgbColor(e)}setLineEndings(e){this.lineEndings=["None","None"];if(Array.isArray(e)&&2===e.length)for(let t=0;t<2;t++){const a=e[t];if(a instanceof Name)switch(a.name){case"None":continue;case"Square":case"Circle":case"Diamond":case"OpenArrow":case"ClosedArrow":case"Butt":case"ROpenArrow":case"RClosedArrow":case"Slash":this.lineEndings[t]=a.name;continue}warn(`Ignoring invalid lineEnding: ${a}`)}}setRotation(e,t){this.rotation=0;let a=e instanceof Dict?e.get("R")||0:t.get("Rotate")||0;if(Number.isInteger(a)&&0!==a){a%=360;a<0&&(a+=360);a%90==0&&(this.rotation=a)}}setBorderAndBackgroundColors(e){if(e instanceof Dict){this.borderColor=getRgbColor(e.getArray("BC"),null);this.backgroundColor=getRgbColor(e.getArray("BG"),null)}else this.borderColor=this.backgroundColor=null}setBorderStyle(e){this.borderStyle=new AnnotationBorderStyle;if(e instanceof Dict)if(e.has("BS")){const t=e.get("BS");if(t instanceof Dict){const e=t.get("Type");if(!e||isName(e,"Border")){this.borderStyle.setWidth(t.get("W"),this.rectangle);this.borderStyle.setStyle(t.get("S"));this.borderStyle.setDashArray(t.getArray("D"))}}}else if(e.has("Border")){const t=e.getArray("Border");if(Array.isArray(t)&&t.length>=3){this.borderStyle.setHorizontalCornerRadius(t[0]);this.borderStyle.setVerticalCornerRadius(t[1]);this.borderStyle.setWidth(t[2],this.rectangle);4===t.length&&this.borderStyle.setDashArray(t[3],!0)}}else this.borderStyle.setWidth(0)}setAppearance(e){this.appearance=null;const t=e.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(a instanceof BaseStream){this.appearance=a;return}if(!(a instanceof Dict))return;const r=e.get("AS");if(!(r instanceof Name&&a.has(r.name)))return;const i=a.get(r.name);i instanceof BaseStream&&(this.appearance=i)}setOptionalContent(e){this.oc=null;const t=e.get("OC");t instanceof Name?warn("setOptionalContent: Support for /Name-entry is not implemented."):t instanceof Dict&&(this.oc=t)}async loadResources(e,t){const a=await t.dict.getAsync("Resources");a&&await ObjectLoader.load(a,e,a.xref);return a}async getOperatorList(e,t,a,r){const{hasOwnCanvas:i,id:n,rect:o}=this.data;let c=this.appearance;const l=!!(i&&a&s);if(l&&(0===this.width||0===this.height)){this.data.hasOwnCanvas=!1;return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}if(!c){if(!l)return{opList:new OperatorList,separateForm:!1,separateCanvas:!1};c=new StringStream("");c.dict=new Dict}const h=c.dict,u=await this.loadResources(Ia,c),d=lookupRect(h.getArray("BBox"),[0,0,1,1]),f=lookupMatrix(h.getArray("Matrix"),Fa),g=getTransformMatrix(o,d,f),p=new OperatorList;let m;this.oc&&(m=await e.parseMarkedContentProps(this.oc,null));void 0!==m&&p.addOp(jt,["OC",m]);p.addOp($t,[n,o,g,f,l]);await e.getOperatorList({stream:c,task:t,resources:u,operatorList:p,fallbackFontDict:this._fallbackFontDict});p.addOp(Gt,[]);void 0!==m&&p.addOp(_t,[]);this.reset();return{opList:p,separateForm:!1,separateCanvas:l}}async save(e,t,a,r){return null}get hasTextContent(){return!1}async extractTextContent(e,t,a){if(!this.appearance)return;const r=await this.loadResources(Oa,this.appearance),i=[],n=[];let s=null;const o={desiredSize:Math.Infinity,ready:!0,enqueue(e,t){for(const t of e.items)if(void 0!==t.str){s||=t.transform.slice(-2);n.push(t.str);if(t.hasEOL){i.push(n.join("").trimEnd());n.length=0}}}};await e.getTextContent({stream:this.appearance,task:t,resources:r,includeMarkedContent:!0,keepWhiteSpace:!0,sink:o,viewBox:a});this.reset();n.length&&i.push(n.join("").trimEnd());if(i.length>1||i[0]){const e=this.appearance.dict,t=lookupRect(e.getArray("BBox"),null),a=lookupMatrix(e.getArray("Matrix"),null);this.data.textPosition=this._transformPoint(s,t,a);this.data.textContent=i}}_transformPoint(e,t,a){const{rect:r}=this.data;t||=[0,0,1,1];a||=[1,0,0,1,0,0];const i=getTransformMatrix(r,t,a);i[4]-=r[0];i[5]-=r[1];const n=e.slice();Util.applyTransform(n,i);Util.applyTransform(n,a);return n}getFieldObject(){return this.data.kidIds?{id:this.data.id,actions:this.data.actions,name:this.data.fieldName,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,type:"",kidIds:this.data.kidIds,page:this.data.pageIndex,rotation:this.rotation}:null}reset(){for(const e of this._streams)e.reset()}_constructFieldName(e){if(!e.has("T")&&!e.has("Parent")){warn("Unknown field name, falling back to empty field name.");return""}if(!e.has("Parent"))return stringToPDFString(e.get("T"));const t=[];e.has("T")&&t.unshift(stringToPDFString(e.get("T")));let a=e;const r=new RefSet;e.objId&&r.put(e.objId);for(;a.has("Parent");){a=a.get("Parent");if(!(a instanceof Dict)||a.objId&&r.has(a.objId))break;a.objId&&r.put(a.objId);a.has("T")&&t.unshift(stringToPDFString(a.get("T")))}return t.join(".")}get width(){return this.data.rect[2]-this.data.rect[0]}get height(){return this.data.rect[3]-this.data.rect[1]}}class AnnotationBorderStyle{constructor(){this.width=1;this.rawWidth=1;this.style=fe;this.dashArray=[3];this.horizontalCornerRadius=0;this.verticalCornerRadius=0}setWidth(e,t=[0,0,0,0]){if(e instanceof Name)this.width=0;else if("number"==typeof e){if(e>0){this.rawWidth=e;const a=(t[2]-t[0])/2,r=(t[3]-t[1])/2;if(a>0&&r>0&&(e>a||e>r)){warn(`AnnotationBorderStyle.setWidth - ignoring width: ${e}`);e=1}}this.width=e}}setStyle(e){if(e instanceof Name)switch(e.name){case"S":this.style=fe;break;case"D":this.style=ge;break;case"B":this.style=pe;break;case"I":this.style=me;break;case"U":this.style=be}}setDashArray(e,t=!1){if(Array.isArray(e)){let a=!0,r=!0;for(const t of e){if(!(+t>=0)){a=!1;break}t>0&&(r=!1)}if(0===e.length||a&&!r){this.dashArray=e;t&&this.setStyle(Name.get("D"))}else this.width=0}else e&&(this.width=0)}setHorizontalCornerRadius(e){Number.isInteger(e)&&(this.horizontalCornerRadius=e)}setVerticalCornerRadius(e){Number.isInteger(e)&&(this.verticalCornerRadius=e)}}class MarkupAnnotation extends Annotation{constructor(e){super(e);const{dict:t}=e;if(t.has("IRT")){const e=t.getRaw("IRT");this.data.inReplyTo=e instanceof Ref?e.toString():null;const a=t.get("RT");this.data.replyType=a instanceof Name?a.name:V}let a=null;if(this.data.replyType===G){const e=t.get("IRT");this.setTitle(e.get("T"));this.data.titleObj=this._title;this.setContents(e.get("Contents"));this.data.contentsObj=this._contents;if(e.has("CreationDate")){this.setCreationDate(e.get("CreationDate"));this.data.creationDate=this.creationDate}else this.data.creationDate=null;if(e.has("M")){this.setModificationDate(e.get("M"));this.data.modificationDate=this.modificationDate}else this.data.modificationDate=null;a=e.getRaw("Popup");if(e.has("C")){this.setColor(e.getArray("C"));this.data.color=this.color}else this.data.color=null}else{this.data.titleObj=this._title;this.setCreationDate(t.get("CreationDate"));this.data.creationDate=this.creationDate;a=t.getRaw("Popup");t.has("C")||(this.data.color=null)}this.data.popupRef=a instanceof Ref?a.toString():null;t.has("RC")&&(this.data.richText=XFAFactory.getRichTextAsHtml(t.get("RC")))}setCreationDate(e){this.creationDate="string"==typeof e?e:null}_setDefaultAppearance({xref:e,extra:t,strokeColor:a,fillColor:r,blendMode:i,strokeAlpha:n,fillAlpha:s,pointsCallback:o}){const c=this.data.rect=[1/0,1/0,-1/0,-1/0],l=["q"];t&&l.push(t);a&&l.push(`${a[0]} ${a[1]} ${a[2]} RG`);r&&l.push(`${r[0]} ${r[1]} ${r[2]} rg`);const h=this.data.quadPoints||Float32Array.from([this.rectangle[0],this.rectangle[3],this.rectangle[2],this.rectangle[3],this.rectangle[0],this.rectangle[1],this.rectangle[2],this.rectangle[1]]);for(let e=0,t=h.length;e"string"==typeof e)).map((e=>stringToPDFString(e))):e instanceof Name?stringToPDFString(e.name):"string"==typeof e?stringToPDFString(e):null}hasFieldFlag(e){return!!(this.data.fieldFlags&e)}_isViewable(e){return!0}mustBeViewed(e,t){return t?this.viewable:super.mustBeViewed(e,t)&&!this._hasFlag(this.flags,Q)}getRotationMatrix(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);return 0===t?Fa:getRotationMatrix(t,this.width,this.height)}getBorderAndBackgroundAppearances(e){let t=e?.get(this.data.id)?.rotation;void 0===t&&(t=this.rotation);if(!this.backgroundColor&&!this.borderColor)return"";const a=0===t||180===t?`0 0 ${this.width} ${this.height} re`:`0 0 ${this.height} ${this.width} re`;let r="";this.backgroundColor&&(r=`${getPdfColor(this.backgroundColor,!0)} ${a} f `);if(this.borderColor){r+=`${this.borderStyle.width||1} w ${getPdfColor(this.borderColor,!1)} ${a} S `}return r}async getOperatorList(e,t,a,r){if(a&l&&!(this instanceof SignatureWidgetAnnotation)&&!this.data.noHTML&&!this.data.hasOwnCanvas)return{opList:new OperatorList,separateForm:!0,separateCanvas:!1};if(!this._hasText)return super.getOperatorList(e,t,a,r);const i=await this._getAppearance(e,t,a,r);if(this.appearance&&null===i)return super.getOperatorList(e,t,a,r);const n=new OperatorList;if(!this._defaultAppearance||null===i)return{opList:n,separateForm:!1,separateCanvas:!1};const o=!!(this.data.hasOwnCanvas&&a&s),c=[0,0,this.width,this.height],h=getTransformMatrix(this.data.rect,c,[1,0,0,1,0,0]);let u;this.oc&&(u=await e.parseMarkedContentProps(this.oc,null));void 0!==u&&n.addOp(jt,["OC",u]);n.addOp($t,[this.data.id,this.data.rect,h,this.getRotationMatrix(r),o]);const d=new StringStream(i);await e.getOperatorList({stream:d,task:t,resources:this._fieldResources.mergedResources,operatorList:n});n.addOp(Gt,[]);void 0!==u&&n.addOp(_t,[]);return{opList:n,separateForm:!1,separateCanvas:o}}_getMKDict(e){const t=new Dict(null);e&&t.set("R",e);this.borderColor&&t.set("BC",getPdfColorArray(this.borderColor));this.backgroundColor&&t.set("BG",getPdfColorArray(this.backgroundColor));return t.size>0?t:null}amendSavedDict(e,t){}setValue(e,t,a,r){const{dict:i,ref:n}=function getParentToUpdate(e,t,a){const r=new RefSet,i=e,n={dict:null,ref:null};for(;e instanceof Dict&&!r.has(t);){r.put(t);if(e.has("T"))break;if(!((t=e.getRaw("Parent"))instanceof Ref))return n;e=a.fetch(t)}if(e instanceof Dict&&e!==i){n.dict=e;n.ref=t}return n}(e,this.ref,a);if(i){if(!r.has(n)){const e=i.clone();e.set("V",t);r.put(n,{data:e});return e}}else e.set("V",t);return null}async save(e,t,a,r){const i=a?.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.value,o=i?.rotation;if(s===this.data.fieldValue||void 0===s){if(!this._hasValueFromXFA&&void 0===o&&void 0===n)return;s||=this.data.fieldValue}if(void 0===o&&!this._hasValueFromXFA&&Array.isArray(s)&&Array.isArray(this.data.fieldValue)&&isArrayEqual(s,this.data.fieldValue)&&void 0===n)return;void 0===o&&(o=this.rotation);let l=null;if(!this._needAppearances){l=await this._getAppearance(e,t,c,a);if(null===l&&void 0===n)return}let h=!1;if(l?.needAppearances){h=!0;l=null}const{xref:u}=e,d=u.fetchIfRef(this.ref);if(!(d instanceof Dict))return;const f=new Dict(u);for(const e of d.getKeys())"AP"!==e&&f.set(e,d.getRaw(e));if(void 0!==n){f.set("F",n);if(null===l&&!h){const e=d.getRaw("AP");e&&f.set("AP",e)}}const g={path:this.data.fieldName,value:s},p=this.setValue(f,Array.isArray(s)?s.map(stringToAsciiOrUTF16BE):stringToAsciiOrUTF16BE(s),u,r);this.amendSavedDict(a,p||f);const m=this._getMKDict(o);m&&f.set("MK",m);r.put(this.ref,{data:f,xfa:g,needAppearances:h});if(null!==l){const e=u.getNewTemporaryRef(),t=new Dict(u);f.set("AP",t);t.set("N",e);const i=this._getSaveFieldResources(u),n=new StringStream(l),s=n.dict=new Dict(u);s.set("Subtype",Name.get("Form"));s.set("Resources",i);const c=o%180==0?[0,0,this.width,this.height]:[0,0,this.height,this.width];s.set("BBox",c);const h=this.getRotationMatrix(a);h!==Fa&&s.set("Matrix",h);r.put(e,{data:n,xfa:null,needAppearances:!1})}f.set("M",`D:${getModificationDate()}`)}async _getAppearance(e,t,a,r){if(this.data.password)return null;const n=r?.get(this.data.id);let s,o;if(n){s=n.formattedValue||n.value;o=n.rotation}if(void 0===o&&void 0===s&&!this._needAppearances&&(!this._hasValueFromXFA||this.appearance))return null;const l=this.getBorderAndBackgroundAppearances(r);if(void 0===s){s=this.data.fieldValue;if(!s)return`/Tx BMC q ${l}Q EMC`}Array.isArray(s)&&1===s.length&&(s=s[0]);assert("string"==typeof s,"Expected `value` to be a string.");s=s.trimEnd();if(this.data.combo){const e=this.data.options.find((({exportValue:e})=>s===e));s=e?.displayValue||s}if(""===s)return`/Tx BMC q ${l}Q EMC`;void 0===o&&(o=this.rotation);let h,u=-1;if(this.data.multiLine){h=s.split(/\r\n?|\n/).map((e=>e.normalize("NFC")));u=h.length}else h=[s.replace(/\r\n?|\n/,"").normalize("NFC")];let{width:d,height:f}=this;90!==o&&270!==o||([d,f]=[f,d]);this._defaultAppearance||(this.data.defaultAppearanceData=parseDefaultAppearance(this._defaultAppearance="/Helvetica 0 Tf 0 g"));let g,p,m,b=await WidgetAnnotation._getFontData(e,t,this.data.defaultAppearanceData,this._fieldResources.mergedResources);const y=[];let w=!1;for(const e of h){const t=b.encodeString(e);t.length>1&&(w=!0);y.push(t.join(""))}if(w&&a&c)return{needAppearances:!0};if(w&&this._isOffscreenCanvasSupported){const a=this.data.comb?"monospace":"sans-serif",r=new FakeUnicodeFont(e.xref,a),i=r.createFontResources(h.join("")),n=i.getRaw("Font");if(this._fieldResources.mergedResources.has("Font")){const e=this._fieldResources.mergedResources.get("Font");for(const t of n.getKeys())e.set(t,n.getRaw(t))}else this._fieldResources.mergedResources.set("Font",n);const o=r.fontName.name;b=await WidgetAnnotation._getFontData(e,t,{fontName:o,fontSize:0},i);for(let e=0,t=y.length;e2)return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 ${numberToString(2)} ${numberToString(C)} Tm (${escapeString(y[0])}) Tj ET Q EMC`;return`/Tx BMC q ${l}BT `+g+` 1 0 0 1 0 0 Tm ${this._renderText(y[0],b,p,d,k,{shift:0},2,C)} ET Q EMC`}static async _getFontData(e,t,a,r){const i=new OperatorList,n={font:null,clone(){return this}},{fontName:s,fontSize:o}=a;await e.handleSetFont(r,[s&&Name.get(s),o],null,i,t,n,null);return n.font}_getTextWidth(e,t){return Math.sumPrecise(t.charsToGlyphs(e).map((e=>e.width)))/1e3}_computeFontSize(e,t,r,i,n){let{fontSize:s}=this.data.defaultAppearanceData,o=(s||12)*a,c=Math.round(e/o);if(!s){const roundWithTwoDigits=e=>Math.floor(100*e)/100;if(-1===n){const n=this._getTextWidth(r,i);s=roundWithTwoDigits(Math.min(e/a,t/n));c=1}else{const l=r.split(/\r\n?|\n/),h=[];for(const e of l){const t=i.encodeString(e).join(""),a=i.charsToGlyphs(t),r=i.getCharPositions(t);h.push({line:t,glyphs:a,positions:r})}const isTooBig=a=>{let r=0;for(const n of h){r+=this._splitLine(null,i,a,t,n).length*a;if(r>e)return!0}return!1};c=Math.max(c,n);for(;;){o=e/c;s=roundWithTwoDigits(o/a);if(!isTooBig(s))break;c++}}const{fontName:l,fontColor:h}=this.data.defaultAppearanceData;this._defaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:a}){return`/${escapePDFName(t)} ${e} Tf ${getPdfColor(a,!0)}`}({fontSize:s,fontName:l,fontColor:h})}return[this._defaultAppearance,s,e/c]}_renderText(e,t,a,r,i,n,s,o){let c;if(1===i){c=(r-this._getTextWidth(e,t)*a)/2}else if(2===i){c=r-this._getTextWidth(e,t)*a-s}else c=s;const l=numberToString(c-n.shift);n.shift=c;return`${l} ${o=numberToString(o)} Td (${escapeString(e)}) Tj`}_getSaveFieldResources(e){const{localResources:t,appearanceResources:a,acroFormResources:r}=this._fieldResources,i=this.data.defaultAppearanceData?.fontName;if(!i)return t||Dict.empty;for(const e of[t,a])if(e instanceof Dict){const t=e.get("Font");if(t instanceof Dict&&t.has(i))return e}if(r instanceof Dict){const a=r.get("Font");if(a instanceof Dict&&a.has(i)){const r=new Dict(e);r.set(i,a.getRaw(i));const n=new Dict(e);n.set("Font",r);return Dict.merge({xref:e,dictArray:[n,t],mergeSubDicts:!0})}}return t||Dict.empty}getFieldObject(){return null}}class TextWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t}=e;if(t.has("PMD")){this.flags|=J;this.data.hidden=!0;warn("Barcodes are not supported")}this.data.hasOwnCanvas=this.data.readOnly&&!this.data.noHTML;this._hasText=!0;"string"!=typeof this.data.fieldValue&&(this.data.fieldValue="");let a=getInheritableProperty({dict:t,key:"Q"});(!Number.isInteger(a)||a<0||a>2)&&(a=null);this.data.textAlignment=a;let r=getInheritableProperty({dict:t,key:"MaxLen"});(!Number.isInteger(r)||r<0)&&(r=0);this.data.maxLen=r;this.data.multiLine=this.hasFieldFlag(ie);this.data.comb=this.hasFieldFlag(de)&&!this.data.multiLine&&!this.data.password&&!this.hasFieldFlag(le)&&0!==this.data.maxLen;this.data.doNotScroll=this.hasFieldFlag(ue)}get hasTextContent(){return!!this.appearance&&!this._needAppearances}_getCombAppearance(e,t,a,r,i,n,s,o,c,l,h){const u=i/this.data.maxLen,d=this.getBorderAndBackgroundAppearances(h),f=[],g=t.getCharPositions(a);for(const[e,t]of g)f.push(`(${escapeString(a.substring(e,t))}) Tj`);const p=f.join(` ${numberToString(u)} 0 Td `);return`/Tx BMC q ${d}BT `+e+` 1 0 0 1 ${numberToString(s)} ${numberToString(o+c)} Tm ${p} ET Q EMC`}_getMultilineAppearance(e,t,a,r,i,n,s,o,c,l,h,u){const d=[],f=i-2*o,g={shift:0};for(let e=0,n=t.length;er){c.push(e.substring(d,a));d=a;f=p;l=-1;u=-1}else{f+=p;l=a;h=i;u=t}else if(f+p>r)if(-1!==l){c.push(e.substring(d,h));d=h;t=u+1;l=-1;f=0}else{c.push(e.substring(d,a));d=a;f=p}else f+=p}dt?`\\${t}`:"\\s+"));new RegExp(`^\\s*${n}\\s*$`).test(this.data.fieldValue)&&(this.data.textContent=this.data.fieldValue.split("\n"))}getFieldObject(){return{id:this.data.id,value:this.data.fieldValue,defaultValue:this.data.defaultFieldValue||"",multiline:this.data.multiLine,password:this.data.password,charLimit:this.data.maxLen,comb:this.data.comb,editable:!this.data.readOnly,hidden:this.data.hidden,name:this.data.fieldName,rect:this.data.rect,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:"text"}}}class ButtonWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);this.checkedAppearance=null;this.uncheckedAppearance=null;const t=this.hasFieldFlag(se),a=this.hasFieldFlag(oe);this.data.checkBox=!t&&!a;this.data.radioButton=t&&!a;this.data.pushButton=a;this.data.isTooltipOnly=!1;if(this.data.checkBox)this._processCheckBox(e);else if(this.data.radioButton)this._processRadioButton(e);else if(this.data.pushButton){this.data.hasOwnCanvas=!0;this.data.noHTML=!1;this._processPushButton(e)}else warn("Invalid field flags for button widget annotation")}async getOperatorList(e,t,a,r){if(this.data.pushButton)return super.getOperatorList(e,t,a,!1,r);let i=null,n=null;if(r){const e=r.get(this.data.id);i=e?e.value:null;n=e?e.rotation:null}if(null===i&&this.appearance)return super.getOperatorList(e,t,a,r);null==i&&(i=this.data.checkBox?this.data.fieldValue===this.data.exportValue:this.data.fieldValue===this.data.buttonValue);const s=i?this.checkedAppearance:this.uncheckedAppearance;if(s){const i=this.appearance,o=lookupMatrix(s.dict.getArray("Matrix"),Fa);n&&s.dict.set("Matrix",this.getRotationMatrix(r));this.appearance=s;const c=super.getOperatorList(e,t,a,r);this.appearance=i;s.dict.set("Matrix",o);return c}return{opList:new OperatorList,separateForm:!1,separateCanvas:!1}}async save(e,t,a,r){this.data.checkBox?this._saveCheckbox(e,t,a,r):this.data.radioButton&&this._saveRadioButton(e,t,a,r)}async _saveCheckbox(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.exportValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===s&&(s=this.rotation);void 0===o&&(o=this.data.fieldValue===this.data.exportValue);const l={path:this.data.fieldName,value:o?this.data.exportValue:""},h=Name.get(o?this.data.exportValue:"Off");this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}async _saveRadioButton(e,t,a,r){if(!a)return;const i=a.get(this.data.id),n=this._buildFlags(i?.noView,i?.noPrint);let s=i?.rotation,o=i?.value;if(void 0===s&&void 0===n){if(void 0===o)return;if(this.data.fieldValue===this.data.buttonValue===o)return}let c=e.xref.fetchIfRef(this.ref);if(!(c instanceof Dict))return;c=c.clone();void 0===o&&(o=this.data.fieldValue===this.data.buttonValue);void 0===s&&(s=this.rotation);const l={path:this.data.fieldName,value:o?this.data.buttonValue:""},h=Name.get(o?this.data.buttonValue:"Off");o&&this.setValue(c,h,e.xref,r);c.set("AS",h);c.set("M",`D:${getModificationDate()}`);void 0!==n&&c.set("F",n);const u=this._getMKDict(s);u&&c.set("MK",u);r.put(this.ref,{data:c,xfa:l,needAppearances:!1})}_getDefaultCheckedAppearance(e,t){const{width:a,height:r}=this,i=[0,0,a,r],n=.8*Math.min(a,r);let s,o;if("check"===t){s={width:.755*n,height:.705*n};o="3"}else if("disc"===t){s={width:.791*n,height:.705*n};o="l"}else unreachable(`_getDefaultCheckedAppearance - unsupported type: ${t}`);const c=`q BT /PdfJsZaDb ${n} Tf 0 g ${numberToString((a-s.width)/2)} ${numberToString((r-s.height)/2)} Td (${o}) Tj ET Q`,l=new Dict(e.xref);l.set("FormType",1);l.set("Subtype",Name.get("Form"));l.set("Type",Name.get("XObject"));l.set("BBox",i);l.set("Matrix",[1,0,0,1,0,0]);l.set("Length",c.length);const h=new Dict(e.xref),u=new Dict(e.xref);u.set("PdfJsZaDb",this.fallbackFontDict);h.set("Font",u);l.set("Resources",h);this.checkedAppearance=new StringStream(c);this.checkedAppearance.dict=l;this._streams.push(this.checkedAppearance)}_processCheckBox(e){const t=e.dict.get("AP");if(!(t instanceof Dict))return;const a=t.get("N");if(!(a instanceof Dict))return;const r=this._decodeFormValue(e.dict.get("AS"));"string"==typeof r&&(this.data.fieldValue=r);const i=null!==this.data.fieldValue&&"Off"!==this.data.fieldValue?this.data.fieldValue:"Yes",n=this._decodeFormValue(a.getKeys());if(0===n.length)n.push("Off",i);else if(1===n.length)"Off"===n[0]?n.push(i):n.unshift("Off");else if(n.includes(i)){n.length=0;n.push("Off",i)}else{const e=n.find((e=>"Off"!==e));n.length=0;n.push("Off",e)}n.includes(this.data.fieldValue)||(this.data.fieldValue="Off");this.data.exportValue=n[1];const s=a.get(this.data.exportValue);this.checkedAppearance=s instanceof BaseStream?s:null;const o=a.get("Off");this.uncheckedAppearance=o instanceof BaseStream?o:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"check");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processRadioButton(e){this.data.buttonValue=null;const t=e.dict.get("Parent");if(t instanceof Dict){this.parent=e.dict.getRaw("Parent");const a=t.get("V");a instanceof Name&&(this.data.fieldValue=this._decodeFormValue(a))}const a=e.dict.get("AP");if(!(a instanceof Dict))return;const r=a.get("N");if(!(r instanceof Dict))return;for(const e of r.getKeys())if("Off"!==e){this.data.buttonValue=this._decodeFormValue(e);break}const i=r.get(this.data.buttonValue);this.checkedAppearance=i instanceof BaseStream?i:null;const n=r.get("Off");this.uncheckedAppearance=n instanceof BaseStream?n:null;this.checkedAppearance?this._streams.push(this.checkedAppearance):this._getDefaultCheckedAppearance(e,"disc");this.uncheckedAppearance&&this._streams.push(this.uncheckedAppearance);this._fallbackFontDict=this.fallbackFontDict;null===this.data.defaultFieldValue&&(this.data.defaultFieldValue="Off")}_processPushButton(e){const{dict:t,annotationGlobals:a}=e;if(t.has("A")||t.has("AA")||this.data.alternativeText){this.data.isTooltipOnly=!t.has("A")&&!t.has("AA");Catalog.parseDestDictionary({destDict:t,resultObj:this.data,docBaseUrl:a.baseUrl,docAttachments:a.attachments})}else warn("Push buttons without action dictionaries are not supported")}getFieldObject(){let e,t="button";if(this.data.checkBox){t="checkbox";e=this.data.exportValue}else if(this.data.radioButton){t="radiobutton";e=this.data.buttonValue}return{id:this.data.id,value:this.data.fieldValue||"Off",defaultValue:this.data.defaultFieldValue,exportValues:e,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,hidden:this.data.hidden,actions:this.data.actions,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:t}}get fallbackFontDict(){const e=new Dict;e.set("BaseFont",Name.get("ZapfDingbats"));e.set("Type",Name.get("FallbackType"));e.set("Subtype",Name.get("FallbackType"));e.set("Encoding",Name.get("ZapfDingbatsEncoding"));return shadow(this,"fallbackFontDict",e)}}class ChoiceWidgetAnnotation extends WidgetAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.indices=t.getArray("I");this.hasIndices=Array.isArray(this.indices)&&this.indices.length>0;this.data.options=[];const r=getInheritableProperty({dict:t,key:"Opt"});if(Array.isArray(r))for(let e=0,t=r.length;e=0&&t0&&(this.data.options=this.data.fieldValue.map((e=>({exportValue:e,displayValue:e}))));this.data.combo=this.hasFieldFlag(ce);this.data.multiSelect=this.hasFieldFlag(he);this._hasText=!0}getFieldObject(){const e=this.data.combo?"combobox":"listbox",t=this.data.fieldValue.length>0?this.data.fieldValue[0]:null;return{id:this.data.id,value:t,defaultValue:this.data.defaultFieldValue,editable:!this.data.readOnly,name:this.data.fieldName,rect:this.data.rect,numItems:this.data.fieldValue.length,multipleSelection:this.data.multiSelect,hidden:this.data.hidden,actions:this.data.actions,items:this.data.options,page:this.data.pageIndex,strokeColor:this.data.borderColor,fillColor:this.data.backgroundColor,rotation:this.rotation,type:e}}amendSavedDict(e,t){if(!this.hasIndices)return;let a=e?.get(this.data.id)?.value;Array.isArray(a)||(a=[a]);const r=[],{options:i}=this.data;for(let e=0,t=0,n=i.length;ea){a=r;t=e}}[f,g]=this._computeFontSize(e,c-4,t,d,-1)}const p=g*a,m=(p-g)/2,b=Math.floor(l/p);let y=0;if(u.length>0){const e=Math.min(...u),t=Math.max(...u);y=Math.max(0,t-b+1);y>e&&(y=e)}const w=Math.min(y+b+1,h),S=["/Tx BMC q",`1 1 ${c} ${l} re W n`];if(u.length){S.push("0.600006 0.756866 0.854904 rg");for(const e of u)y<=e&&ee.trimEnd()));const{coords:e,bbox:t,matrix:r}=FakeUnicodeFont.getFirstPositionInfo(this.rectangle,this.rotation,a);this.data.textPosition=this._transformPoint(e,t,r)}if(this._isOffscreenCanvasSupported){const i=e.dict.get("CA"),n=new FakeUnicodeFont(r,"sans-serif");this.appearance=n.createAppearance(this._contents.str,this.rectangle,this.rotation,a,t,i);this._streams.push(this.appearance)}else warn("FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly.")}}get hasTextContent(){return this._hasAppearance}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,fontSize:n,oldAnnotation:s,rect:o,rotation:c,user:l,value:h}=e,u=s||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("FreeText"));if(s){u.set("M",`D:${getModificationDate()}`);u.delete("RC")}else u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);const d=`/Helv ${n} Tf ${getPdfColor(i,!0)}`;u.set("DA",d);u.set("Contents",stringToAsciiOrUTF16BE(h));u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);a?e.set("N",a):e.set("N",r)}return u}static async createNewAppearanceStream(e,t,r){const{baseFontRef:i,evaluator:n,task:s}=r,{color:o,fontSize:c,rect:l,rotation:h,value:u}=e,d=new Dict(t),f=new Dict(t);if(i)f.set("Helv",i);else{const e=new Dict(t);e.set("BaseFont",Name.get("Helvetica"));e.set("Type",Name.get("Font"));e.set("Subtype",Name.get("Type1"));e.set("Encoding",Name.get("WinAnsiEncoding"));f.set("Helv",e)}d.set("Font",f);const g=await WidgetAnnotation._getFontData(n,s,{fontName:"Helv",fontSize:c},d),[p,m,b,y]=l;let w=b-p,S=y-m;h%180!=0&&([w,S]=[S,w]);const x=u.split("\n"),k=c/1e3;let C=-1/0;const v=[];for(let e of x){const t=g.encodeString(e);if(t.length>1)return null;e=t.join("");v.push(e);let a=0;const r=g.charsToGlyphs(e);for(const e of r)a+=e.width*k;C=Math.max(C,a)}let F=1;C>w&&(F=w/C);let O=1;const T=a*c,M=1*c,D=T*x.length;D>S&&(O=S/D);const R=c*Math.min(F,O);let N,E,L;switch(h){case 0:L=[1,0,0,1];E=[l[0],l[1],w,S];N=[l[0],l[3]-M];break;case 90:L=[0,1,-1,0];E=[l[1],-l[2],w,S];N=[l[1],-l[0]-M];break;case 180:L=[-1,0,0,-1];E=[-l[2],-l[3],w,S];N=[-l[2],-l[1]-M];break;case 270:L=[0,-1,1,0];E=[-l[3],l[0],w,S];N=[-l[3],l[2]-M]}const j=["q",`${L.join(" ")} 0 0 cm`,`${E.join(" ")} re W n`,"BT",`${getPdfColor(o,!0)}`,`0 Tc /Helv ${numberToString(R)} Tf`];j.push(`${N.join(" ")} Td (${escapeString(v[0])}) Tj`);const _=numberToString(T);for(let e=1,t=v.length;e{e.push(`${r[0]} ${r[1]} m`,`${r[2]} ${r[3]} l`,"S");return[t[0]-c,t[7]-c,t[2]+c,t[3]+c]}})}}}class SquareAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=D;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[4]+this.borderStyle.width/2,r=t[5]+this.borderStyle.width/2,i=t[6]-t[4]-this.borderStyle.width,s=t[3]-t[7]-this.borderStyle.width;e.push(`${a} ${r} ${i} ${s} re`);n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class CircleAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=R;if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),i=getRgbColor(t.getArray("IC"),null),n=i?getPdfColorArray(i):null,s=n?r:null;if(0===this.borderStyle.width&&!n)return;const o=4/3*Math.tan(Math.PI/8);this._setDefaultAppearance({xref:a,extra:`${this.borderStyle.width} w`,strokeColor:e,fillColor:n,strokeAlpha:r,fillAlpha:s,pointsCallback:(e,t)=>{const a=t[0]+this.borderStyle.width/2,r=t[1]-this.borderStyle.width/2,i=t[6]-this.borderStyle.width/2,s=t[7]+this.borderStyle.width/2,c=a+(i-a)/2,l=r+(s-r)/2,h=(i-a)/2*o,u=(s-r)/2*o;e.push(`${c} ${s} m`,`${c+h} ${s} ${i} ${l+u} ${i} ${l} c`,`${i} ${l-u} ${c+h} ${r} ${c} ${r} c`,`${c-h} ${r} ${a} ${l-u} ${a} ${l} c`,`${a} ${l+u} ${c-h} ${s} ${c} ${s} c`,"h");n?e.push("B"):e.push("S");return[t[0],t[7],t[2],t[3]]}})}}}class PolylineAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=E;this.data.hasOwnCanvas=this.data.noRotate;this.data.noHTML=!1;this.data.vertices=null;if(!(this instanceof PolygonAnnotation)){this.setLineEndings(t.getArray("LE"));this.data.lineEndings=this.lineEndings}const r=t.getArray("Vertices");if(!isNumberArray(r,null))return;const i=this.data.vertices=Float32Array.from(r);if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA"),n=this.borderStyle.width||1,s=2*n,o=[1/0,1/0,-1/0,-1/0];for(let e=0,t=i.length;e{for(let t=0,a=i.length;t{for(const t of this.data.inkLists){for(let a=0,r=t.length;a{e.push(`${t[0]} ${t[1]} m`,`${t[2]} ${t[3]} l`,`${t[6]} ${t[7]} l`,`${t[4]} ${t[5]} l`,"f");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}static createNewDict(e,t,{apRef:a,ap:r}){const{color:i,oldAnnotation:n,opacity:s,rect:o,rotation:c,user:l,quadPoints:h}=e,u=n||new Dict(t);u.set("Type",Name.get("Annot"));u.set("Subtype",Name.get("Highlight"));u.set(n?"M":"CreationDate",`D:${getModificationDate()}`);u.set("CreationDate",`D:${getModificationDate()}`);u.set("Rect",o);u.set("F",4);u.set("Border",[0,0,0]);u.set("Rotate",c);u.set("QuadPoints",h);u.set("C",getPdfColorArray(i));u.set("CA",s);l&&u.set("T",stringToAsciiOrUTF16BE(l));if(a||r){const e=new Dict(t);u.set("AP",e);e.set("N",a||r)}return u}static async createNewAppearanceStream(e,t,a){const{color:r,rect:i,outlines:n,opacity:s}=e,o=[`${getPdfColor(r,!0)}`,"/R0 gs"],c=[];for(const e of n){c.length=0;c.push(`${numberToString(e[0])} ${numberToString(e[1])} m`);for(let t=2,a=e.length;t{e.push(`${t[4]} ${t[5]+1.3} m`,`${t[6]} ${t[7]+1.3} l`,"S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class SquigglyAnnotation extends MarkupAnnotation{constructor(e){super(e);const{dict:t,xref:a}=e;this.data.annotationType=_;if(this.data.quadPoints=getQuadPoints(t,null)){if(!this.appearance){const e=this.color?getPdfColorArray(this.color):[0,0,0],r=t.get("CA");this._setDefaultAppearance({xref:a,extra:"[] 0 d 1 w",strokeColor:e,strokeAlpha:r,pointsCallback:(e,t)=>{const a=(t[1]-t[5])/6;let r=a,i=t[4];const n=t[5],s=t[6];e.push(`${i} ${n+r} m`);do{i+=2;r=0===r?a:0;e.push(`${i} ${n+r} l`)}while(i{e.push((t[0]+t[4])/2+" "+(t[1]+t[5])/2+" m",(t[2]+t[6])/2+" "+(t[3]+t[7])/2+" l","S");return[t[0],t[7],t[2],t[3]]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{#pe=null;constructor(e){super(e);this.data.annotationType=X;this.data.hasOwnCanvas=this.data.noRotate;this.data.isEditable=!this.data.noHTML;this.data.noHTML=!1}mustBeViewedWhenEditing(e,t=null){if(e){if(!this.data.isEditable)return!0;this.#pe??=this.data.hasOwnCanvas;this.data.hasOwnCanvas=!0;return!0}if(null!==this.#pe){this.data.hasOwnCanvas=this.#pe;this.#pe=null}return!t?.has(this.data.id)}static async createImage(e,t){const{width:a,height:r}=e,i=new OffscreenCanvas(a,r),n=i.getContext("2d",{alpha:!0});n.drawImage(e,0,0);const s=n.getImageData(0,0,a,r).data,o=new Uint32Array(s.buffer),c=o.some(FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>!!(255&~e));if(c){n.fillStyle="white";n.fillRect(0,0,a,r);n.drawImage(e,0,0)}const l=i.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),h=Name.get("XObject"),u=Name.get("Image"),d=new Dict(t);d.set("Type",h);d.set("Subtype",u);d.set("BitsPerComponent",8);d.set("ColorSpace",Name.get("DeviceRGB"));d.set("Filter",Name.get("DCTDecode"));d.set("BBox",[0,0,a,r]);d.set("Width",a);d.set("Height",r);let f=null;if(c){const e=new Uint8Array(o.length);if(FeatureTest.isLittleEndian)for(let t=0,a=o.length;t>>24;else for(let t=0,a=o.length;t=0&&n<=1?n:null}}const fc={get r(){return shadow(this,"r",new Uint8Array([7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21]))},get k(){return shadow(this,"k",new Int32Array([-680876936,-389564586,606105819,-1044525330,-176418897,1200080426,-1473231341,-45705983,1770035416,-1958414417,-42063,-1990404162,1804603682,-40341101,-1502002290,1236535329,-165796510,-1069501632,643717713,-373897302,-701558691,38016083,-660478335,-405537848,568446438,-1019803690,-187363961,1163531501,-1444681467,-51403784,1735328473,-1926607734,-378558,-2022574463,1839030562,-35309556,-1530992060,1272893353,-155497632,-1094730640,681279174,-358537222,-722521979,76029189,-640364487,-421815835,530742520,-995338651,-198630844,1126891415,-1416354905,-57434055,1700485571,-1894986606,-1051523,-2054922799,1873313359,-30611744,-1560198380,1309151649,-145523070,-1120210379,718787259,-343485551]))}};function calculateMD5(e,t,a){let r=1732584193,i=-271733879,n=-1732584194,s=271733878;const o=a+72&-64,c=new Uint8Array(o);let l,h;for(l=0;l>5&255;c[l++]=a>>13&255;c[l++]=a>>21&255;c[l++]=a>>>29&255;l+=3;const d=new Int32Array(16),{k:f,r:g}=fc;for(l=0;l>>32-n)|0;a=r}r=r+a|0;i=i+o|0;n=n+u|0;s=s+p|0}return new Uint8Array([255&r,r>>8&255,r>>16&255,r>>>24&255,255&i,i>>8&255,i>>16&255,i>>>24&255,255&n,n>>8&255,n>>16&255,n>>>24&255,255&s,s>>8&255,s>>16&255,s>>>24&255])}function decodeString(e){try{return stringToUTF8String(e)}catch(t){warn(`UTF-8 decoding failed: "${t}".`);return e}}class DatasetXMLParser extends SimpleXMLParser{constructor(e){super(e);this.node=null}onEndElement(e){const t=super.onEndElement(e);if(t&&"xfa:datasets"===e){this.node=t;throw new Error("Aborting DatasetXMLParser.")}}}class DatasetReader{constructor(e){if(e.datasets)this.node=new SimpleXMLParser({hasAttributes:!0}).parseFromString(e.datasets).documentElement;else{const t=new DatasetXMLParser({hasAttributes:!0});try{t.parseFromString(e["xdp:xdp"])}catch{}this.node=t.node}}getValue(e){if(!this.node||!e)return"";const t=this.node.searchNode(parseXFAPath(e),0);if(!t)return"";const a=t.firstChild;return"value"===a?.nodeName?t.children.map((e=>decodeString(e.textContent))):decodeString(t.textContent)}}class Word64{constructor(e,t){this.high=0|e;this.low=0|t}and(e){this.high&=e.high;this.low&=e.low}xor(e){this.high^=e.high;this.low^=e.low}shiftRight(e){if(e>=32){this.low=this.high>>>e-32|0;this.high=0}else{this.low=this.low>>>e|this.high<<32-e;this.high=this.high>>>e|0}}rotateRight(e){let t,a;if(32&e){a=this.low;t=this.high}else{t=this.low;a=this.high}e&=31;this.low=t>>>e|a<<32-e;this.high=a>>>e|t<<32-e}not(){this.high=~this.high;this.low=~this.low}add(e){const t=(this.low>>>0)+(e.low>>>0);let a=(this.high>>>0)+(e.high>>>0);t>4294967295&&(a+=1);this.low=0|t;this.high=0|a}copyTo(e,t){e[t]=this.high>>>24&255;e[t+1]=this.high>>16&255;e[t+2]=this.high>>8&255;e[t+3]=255&this.high;e[t+4]=this.low>>>24&255;e[t+5]=this.low>>16&255;e[t+6]=this.low>>8&255;e[t+7]=255&this.low}assign(e){this.high=e.high;this.low=e.low}}const gc={get k(){return shadow(this,"k",[new Word64(1116352408,3609767458),new Word64(1899447441,602891725),new Word64(3049323471,3964484399),new Word64(3921009573,2173295548),new Word64(961987163,4081628472),new Word64(1508970993,3053834265),new Word64(2453635748,2937671579),new Word64(2870763221,3664609560),new Word64(3624381080,2734883394),new Word64(310598401,1164996542),new Word64(607225278,1323610764),new Word64(1426881987,3590304994),new Word64(1925078388,4068182383),new Word64(2162078206,991336113),new Word64(2614888103,633803317),new Word64(3248222580,3479774868),new Word64(3835390401,2666613458),new Word64(4022224774,944711139),new Word64(264347078,2341262773),new Word64(604807628,2007800933),new Word64(770255983,1495990901),new Word64(1249150122,1856431235),new Word64(1555081692,3175218132),new Word64(1996064986,2198950837),new Word64(2554220882,3999719339),new Word64(2821834349,766784016),new Word64(2952996808,2566594879),new Word64(3210313671,3203337956),new Word64(3336571891,1034457026),new Word64(3584528711,2466948901),new Word64(113926993,3758326383),new Word64(338241895,168717936),new Word64(666307205,1188179964),new Word64(773529912,1546045734),new Word64(1294757372,1522805485),new Word64(1396182291,2643833823),new Word64(1695183700,2343527390),new Word64(1986661051,1014477480),new Word64(2177026350,1206759142),new Word64(2456956037,344077627),new Word64(2730485921,1290863460),new Word64(2820302411,3158454273),new Word64(3259730800,3505952657),new Word64(3345764771,106217008),new Word64(3516065817,3606008344),new Word64(3600352804,1432725776),new Word64(4094571909,1467031594),new Word64(275423344,851169720),new Word64(430227734,3100823752),new Word64(506948616,1363258195),new Word64(659060556,3750685593),new Word64(883997877,3785050280),new Word64(958139571,3318307427),new Word64(1322822218,3812723403),new Word64(1537002063,2003034995),new Word64(1747873779,3602036899),new Word64(1955562222,1575990012),new Word64(2024104815,1125592928),new Word64(2227730452,2716904306),new Word64(2361852424,442776044),new Word64(2428436474,593698344),new Word64(2756734187,3733110249),new Word64(3204031479,2999351573),new Word64(3329325298,3815920427),new Word64(3391569614,3928383900),new Word64(3515267271,566280711),new Word64(3940187606,3454069534),new Word64(4118630271,4000239992),new Word64(116418474,1914138554),new Word64(174292421,2731055270),new Word64(289380356,3203993006),new Word64(460393269,320620315),new Word64(685471733,587496836),new Word64(852142971,1086792851),new Word64(1017036298,365543100),new Word64(1126000580,2618297676),new Word64(1288033470,3409855158),new Word64(1501505948,4234509866),new Word64(1607167915,987167468),new Word64(1816402316,1246189591)])}};function ch(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.not();i.and(r);e.xor(i)}function maj(e,t,a,r,i){e.assign(t);e.and(a);i.assign(t);i.and(r);e.xor(i);i.assign(a);i.and(r);e.xor(i)}function sigma(e,t,a){e.assign(t);e.rotateRight(28);a.assign(t);a.rotateRight(34);e.xor(a);a.assign(t);a.rotateRight(39);e.xor(a)}function sigmaPrime(e,t,a){e.assign(t);e.rotateRight(14);a.assign(t);a.rotateRight(18);e.xor(a);a.assign(t);a.rotateRight(41);e.xor(a)}function littleSigma(e,t,a){e.assign(t);e.rotateRight(1);a.assign(t);a.rotateRight(8);e.xor(a);a.assign(t);a.shiftRight(7);e.xor(a)}function littleSigmaPrime(e,t,a){e.assign(t);e.rotateRight(19);a.assign(t);a.rotateRight(61);e.xor(a);a.assign(t);a.shiftRight(6);e.xor(a)}function calculateSHA512(e,t,a,r=!1){let i,n,s,o,c,l,h,u;if(r){i=new Word64(3418070365,3238371032);n=new Word64(1654270250,914150663);s=new Word64(2438529370,812702999);o=new Word64(355462360,4144912697);c=new Word64(1731405415,4290775857);l=new Word64(2394180231,1750603025);h=new Word64(3675008525,1694076839);u=new Word64(1203062813,3204075428)}else{i=new Word64(1779033703,4089235720);n=new Word64(3144134277,2227873595);s=new Word64(1013904242,4271175723);o=new Word64(2773480762,1595750129);c=new Word64(1359893119,2917565137);l=new Word64(2600822924,725511199);h=new Word64(528734635,4215389547);u=new Word64(1541459225,327033209)}const d=128*Math.ceil((a+17)/128),f=new Uint8Array(d);let g,p;for(g=0;g>>29&255;f[g++]=a>>21&255;f[g++]=a>>13&255;f[g++]=a>>5&255;f[g++]=a<<3&255;const b=new Array(80);for(g=0;g<80;g++)b[g]=new Word64(0,0);const{k:y}=gc;let w=new Word64(0,0),S=new Word64(0,0),x=new Word64(0,0),k=new Word64(0,0),C=new Word64(0,0),v=new Word64(0,0),F=new Word64(0,0),O=new Word64(0,0);const T=new Word64(0,0),M=new Word64(0,0),D=new Word64(0,0),R=new Word64(0,0);let N,E;for(g=0;g>>t|e<<32-t}function calculate_sha256_ch(e,t,a){return e&t^~e&a}function calculate_sha256_maj(e,t,a){return e&t^e&a^t&a}function calculate_sha256_sigma(e){return rotr(e,2)^rotr(e,13)^rotr(e,22)}function calculate_sha256_sigmaPrime(e){return rotr(e,6)^rotr(e,11)^rotr(e,25)}function calculate_sha256_littleSigma(e){return rotr(e,7)^rotr(e,18)^e>>>3}function calculateSHA256(e,t,a){let r=1779033703,i=3144134277,n=1013904242,s=2773480762,o=1359893119,c=2600822924,l=528734635,h=1541459225;const u=64*Math.ceil((a+9)/64),d=new Uint8Array(u);let f,g;for(f=0;f>>29&255;d[f++]=a>>21&255;d[f++]=a>>13&255;d[f++]=a>>5&255;d[f++]=a<<3&255;const m=new Uint32Array(64),{k:b}=pc;for(f=0;f>>10)+m[g-7]+calculate_sha256_littleSigma(m[g-15])+m[g-16]|0;let e,t,a=r,u=i,p=n,w=s,S=o,x=c,k=l,C=h;for(g=0;g<64;++g){e=C+calculate_sha256_sigmaPrime(S)+calculate_sha256_ch(S,x,k)+b[g]+m[g];t=calculate_sha256_sigma(a)+calculate_sha256_maj(a,u,p);C=k;k=x;x=S;S=w+e|0;w=p;p=u;u=a;a=e+t|0}r=r+a|0;i=i+u|0;n=n+p|0;s=s+w|0;o=o+S|0;c=c+x|0;l=l+k|0;h=h+C|0}var y;return new Uint8Array([r>>24&255,r>>16&255,r>>8&255,255&r,i>>24&255,i>>16&255,i>>8&255,255&i,n>>24&255,n>>16&255,n>>8&255,255&n,s>>24&255,s>>16&255,s>>8&255,255&s,o>>24&255,o>>16&255,o>>8&255,255&o,c>>24&255,c>>16&255,c>>8&255,255&c,l>>24&255,l>>16&255,l>>8&255,255&l,h>>24&255,h>>16&255,h>>8&255,255&h])}class DecryptStream extends DecodeStream{constructor(e,t,a){super(t);this.str=e;this.dict=e.dict;this.decrypt=a;this.nextChunk=null;this.initialized=!1}readBlock(){let e;if(this.initialized)e=this.nextChunk;else{e=this.str.getBytes(512);this.initialized=!0}if(!e?.length){this.eof=!0;return}this.nextChunk=this.str.getBytes(512);const t=this.nextChunk?.length>0;e=(0,this.decrypt)(e,!t);const a=this.bufferLength,r=a+e.length;this.ensureBuffer(r).set(e,a);this.bufferLength=r}}class ARCFourCipher{constructor(e){this.a=0;this.b=0;const t=new Uint8Array(256),a=e.length;for(let e=0;e<256;++e)t[e]=e;for(let r=0,i=0;r<256;++r){const n=t[r];i=i+n+e[r%a]&255;t[r]=t[i];t[i]=n}this.s=t}encryptBlock(e){let t=this.a,a=this.b;const r=this.s,i=e.length,n=new Uint8Array(i);for(let s=0;st<128?t<<1:t<<1^27));constructor(){this.buffer=new Uint8Array(16);this.bufferPosition=0}_expandKey(e){unreachable("Cannot call `_expandKey` on the base class")}_decrypt(e,t){let a,r,i;const n=new Uint8Array(16);n.set(e);for(let e=0,a=this._keySize;e<16;++e,++a)n[e]^=t[a];for(let e=this._cyclesOfRepetition-1;e>=1;--e){a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e)n[e]=this._inv_s[n[e]];for(let a=0,r=16*e;a<16;++a,++r)n[a]^=t[r];for(let e=0;e<16;e+=4){const t=this._mix[n[e]],r=this._mix[n[e+1]],i=this._mix[n[e+2]],s=this._mix[n[e+3]];a=t^r>>>8^r<<24^i>>>16^i<<16^s>>>24^s<<8;n[e]=a>>>24&255;n[e+1]=a>>16&255;n[e+2]=a>>8&255;n[e+3]=255&a}}a=n[13];n[13]=n[9];n[9]=n[5];n[5]=n[1];n[1]=a;a=n[14];r=n[10];n[14]=n[6];n[10]=n[2];n[6]=a;n[2]=r;a=n[15];r=n[11];i=n[7];n[15]=n[3];n[11]=a;n[7]=r;n[3]=i;for(let e=0;e<16;++e){n[e]=this._inv_s[n[e]];n[e]^=t[e]}return n}_encrypt(e,t){const a=this._s;let r,i,n;const s=new Uint8Array(16);s.set(e);for(let e=0;e<16;++e)s[e]^=t[e];for(let e=1;e=r;--a)if(e[a]!==t){t=0;break}o-=t;n[n.length-1]=e.subarray(0,16-t)}}const c=new Uint8Array(o);for(let e=0,t=0,a=n.length;e=256&&(o=255&(27^o))}for(let t=0;t<4;++t){a[e]=r^=a[e-32];e++;a[e]=i^=a[e-32];e++;a[e]=n^=a[e-32];e++;a[e]=s^=a[e-32];e++}}return a}}class PDFBase{_hash(e,t,a){unreachable("Abstract method `_hash` called")}checkOwnerPassword(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);return isArrayEqual(this._hash(e,i,a),r)}checkUserPassword(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);return isArrayEqual(this._hash(e,r,[]),a)}getOwnerKey(e,t,a,r){const i=new Uint8Array(e.length+56);i.set(e,0);i.set(t,e.length);i.set(a,e.length+t.length);const n=this._hash(e,i,a);return new AES256Cipher(n).decryptBlock(r,!1,new Uint8Array(16))}getUserKey(e,t,a){const r=new Uint8Array(e.length+8);r.set(e,0);r.set(t,e.length);const i=this._hash(e,r,[]);return new AES256Cipher(i).decryptBlock(a,!1,new Uint8Array(16))}}class PDF17 extends PDFBase{_hash(e,t,a){return calculateSHA256(t,0,t.length)}}class PDF20 extends PDFBase{_hash(e,t,a){let r=calculateSHA256(t,0,t.length).subarray(0,32),i=[0],n=0;for(;n<64||i.at(-1)>n-32;){const t=e.length+r.length+a.length,l=new Uint8Array(t);let h=0;l.set(e,h);h+=e.length;l.set(r,h);h+=r.length;l.set(a,h);const u=new Uint8Array(64*t);for(let e=0,a=0;e<64;e++,a+=t)u.set(l,a);i=new AES128Cipher(r.subarray(0,16)).encrypt(u,r.subarray(16,32));const d=Math.sumPrecise(i.slice(0,16))%3;0===d?r=calculateSHA256(i,0,i.length):1===d?r=(s=i,o=0,c=i.length,calculateSHA512(s,o,c,!0)):2===d&&(r=calculateSHA512(i,0,i.length));n++}var s,o,c;return r.subarray(0,32)}}class CipherTransform{constructor(e,t){this.StringCipherConstructor=e;this.StreamCipherConstructor=t}createStream(e,t){const a=new this.StreamCipherConstructor;return new DecryptStream(e,t,(function cipherTransformDecryptStream(e,t){return a.decryptBlock(e,t)}))}decryptString(e){const t=new this.StringCipherConstructor;let a=stringToBytes(e);a=t.decryptBlock(a,!0);return bytesToString(a)}encryptString(e){const t=new this.StringCipherConstructor;if(t instanceof AESBaseCipher){const a=16-e.length%16;e+=String.fromCharCode(a).repeat(a);const r=new Uint8Array(16);crypto.getRandomValues(r);let i=stringToBytes(e);i=t.encrypt(i,r);const n=new Uint8Array(16+i.length);n.set(r);n.set(i,16);return bytesToString(n)}let a=stringToBytes(e);a=t.encrypt(a);return bytesToString(a)}}class CipherTransformFactory{static get _defaultPasswordBytes(){return shadow(this,"_defaultPasswordBytes",new Uint8Array([40,191,78,94,78,117,138,65,100,0,78,86,255,250,1,8,46,46,0,182,208,104,62,128,47,12,169,254,100,83,105,122]))}#be(e,t,a,r,i,n,s,o,c,l,h,u){if(t){const e=Math.min(127,t.length);t=t.subarray(0,e)}else t=[];const d=6===e?new PDF20:new PDF17;return d.checkUserPassword(t,o,s)?d.getUserKey(t,c,h):t.length&&d.checkOwnerPassword(t,r,n,a)?d.getOwnerKey(t,i,n,l):null}#ye(e,t,a,r,i,n,s,o){const c=40+a.length+e.length,l=new Uint8Array(c);let h,u,d=0;if(t){u=Math.min(32,t.length);for(;d>8&255;l[d++]=i>>16&255;l[d++]=i>>>24&255;l.set(e,d);d+=e.length;if(n>=4&&!o){l.fill(255,d,d+4);d+=4}let f=calculateMD5(l,0,d);const g=s>>3;if(n>=3)for(h=0;h<50;++h)f=calculateMD5(f,0,g);const p=f.subarray(0,g);let m,b;if(n>=3){d=0;l.set(CipherTransformFactory._defaultPasswordBytes,d);d+=32;l.set(e,d);d+=e.length;m=new ARCFourCipher(p);b=m.encryptBlock(calculateMD5(l,0,d));u=p.length;const t=new Uint8Array(u);for(h=1;h<=19;++h){for(let e=0;er[t]===e))?p:null}#we(e,t,a,r){const i=new Uint8Array(32);let n=0;const s=Math.min(32,e.length);for(;n>3;if(a>=3)for(o=0;o<50;++o)c=calculateMD5(c,0,c.length);let h,u;if(a>=3){u=t;const e=new Uint8Array(l);for(o=19;o>=0;o--){for(let t=0;t>8&255;n[s++]=e>>16&255;n[s++]=255&t;n[s++]=t>>8&255;if(r){n[s++]=115;n[s++]=65;n[s++]=108;n[s++]=84}return calculateMD5(n,0,s).subarray(0,Math.min(i+5,16))}#xe(e,t,a,r,i){if(!(t instanceof Name))throw new FormatError("Invalid crypt filter name.");const n=this,s=e.get(t.name),o=s?.get("CFM");if(!o||"None"===o.name)return function(){return new NullCipher};if("V2"===o.name)return function(){return new ARCFourCipher(n.#Se(a,r,i,!1))};if("AESV2"===o.name)return function(){return new AES128Cipher(n.#Se(a,r,i,!0))};if("AESV3"===o.name)return function(){return new AES256Cipher(i)};throw new FormatError("Unknown crypto method")}constructor(e,t,a){const r=e.get("Filter");if(!isName(r,"Standard"))throw new FormatError("unknown encryption method");this.filterName=r.name;this.dict=e;const i=e.get("V");if(!Number.isInteger(i)||1!==i&&2!==i&&4!==i&&5!==i)throw new FormatError("unsupported encryption algorithm");this.algorithm=i;let n=e.get("Length");if(!n)if(i<=3)n=40;else{const t=e.get("CF"),a=e.get("StmF");if(t instanceof Dict&&a instanceof Name){t.suppressEncryption=!0;const e=t.get(a.name);n=e?.get("Length")||128;n<40&&(n<<=3)}}if(!Number.isInteger(n)||n<40||n%8!=0)throw new FormatError("invalid key length");const s=stringToBytes(e.get("O")),o=stringToBytes(e.get("U")),c=s.subarray(0,32),l=o.subarray(0,32),h=e.get("P"),u=e.get("R"),d=(4===i||5===i)&&!1!==e.get("EncryptMetadata");this.encryptMetadata=d;const f=stringToBytes(t);let g,p;if(a){if(6===u)try{a=utf8StringToString(a)}catch{warn("CipherTransformFactory: Unable to convert UTF8 encoded password.")}g=stringToBytes(a)}if(5!==i)p=this.#ye(f,g,c,l,h,u,n,d);else{const t=s.subarray(32,40),a=s.subarray(40,48),r=o.subarray(0,48),i=o.subarray(32,40),n=o.subarray(40,48),h=stringToBytes(e.get("OE")),d=stringToBytes(e.get("UE")),f=stringToBytes(e.get("Perms"));p=this.#be(u,g,c,t,a,r,l,i,n,h,d,f)}if(!p){if(!a)throw new PasswordException("No password given",ha);const e=this.#we(g,c,u,n);p=this.#ye(f,e,c,l,h,u,n,d)}if(!p)throw new PasswordException("Incorrect Password",ua);if(4===i&&p.length<16){this.encryptionKey=new Uint8Array(16);this.encryptionKey.set(p)}else this.encryptionKey=p;if(i>=4){const t=e.get("CF");t instanceof Dict&&(t.suppressEncryption=!0);this.cf=t;this.stmf=e.get("StmF")||Name.get("Identity");this.strf=e.get("StrF")||Name.get("Identity");this.eff=e.get("EFF")||this.stmf}}createCipherTransform(e,t){if(4===this.algorithm||5===this.algorithm)return new CipherTransform(this.#xe(this.cf,this.strf,e,t,this.encryptionKey),this.#xe(this.cf,this.stmf,e,t,this.encryptionKey));const a=this.#Se(e,t,this.encryptionKey,!1),cipherConstructor=function(){return new ARCFourCipher(a)};return new CipherTransform(cipherConstructor,cipherConstructor)}}class XRef{#Ae=null;constructor(e,t){this.stream=e;this.pdfManager=t;this.entries=[];this._xrefStms=new Set;this._cacheMap=new Map;this._pendingRefs=new RefSet;this._newPersistentRefNum=null;this._newTemporaryRefNum=null;this._persistentRefsCache=null}getNewPersistentRef(e){null===this._newPersistentRefNum&&(this._newPersistentRefNum=this.entries.length||1);const t=this._newPersistentRefNum++;this._cacheMap.set(t,e);return Ref.get(t,0)}getNewTemporaryRef(){if(null===this._newTemporaryRefNum){this._newTemporaryRefNum=this.entries.length||1;if(this._newPersistentRefNum){this._persistentRefsCache=new Map;for(let e=this._newTemporaryRefNum;e0;){const[s,o]=n;if(!Number.isInteger(s)||!Number.isInteger(o))throw new FormatError(`Invalid XRef range fields: ${s}, ${o}`);if(!Number.isInteger(a)||!Number.isInteger(r)||!Number.isInteger(i))throw new FormatError(`Invalid XRef entry fields length: ${s}, ${o}`);for(let n=t.entryNum;n=e.length);){a+=String.fromCharCode(r);r=e[t]}return a}function skipUntil(e,t,a){const r=a.length,i=e.length;let n=0;for(;t=r)break;t++;n++}return n}const e=/\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/g,t=/\b(startxref|\d+\s+\d+\s+obj)\b/g,a=/^(\d+)\s+(\d+)\s+obj\b/,r=new Uint8Array([116,114,97,105,108,101,114]),i=new Uint8Array([115,116,97,114,116,120,114,101,102]),n=new Uint8Array([47,88,82,101,102]);this.entries.length=0;this._cacheMap.clear();const s=this.stream;s.pos=0;const o=s.getBytes(),c=bytesToString(o),l=o.length;let h=s.start;const u=[],d=[];for(;h=l)break;f=o[h]}while(10!==f&&13!==f);continue}const g=readToken(o,h);let p;if(g.startsWith("xref")&&(4===g.length||/\s/.test(g[4]))){h+=skipUntil(o,h,r);u.push(h);h+=skipUntil(o,h,i)}else if(p=a.exec(g)){const t=0|p[1],a=0|p[2],r=h+g.length;let i,u=!1;if(this.entries[t]){if(this.entries[t].gen===a)try{new Parser({lexer:new Lexer(s.makeSubStream(r))}).getObj();u=!0}catch(e){e instanceof ParserEOFException?warn(`indexObjects -- checking object (${g}): "${e}".`):u=!0}}else u=!0;u&&(this.entries[t]={offset:h-s.start,gen:a,uncompressed:!0});e.lastIndex=r;const f=e.exec(c);if(f){i=e.lastIndex+1-h;if("endobj"!==f[1]){warn(`indexObjects: Found "${f[1]}" inside of another "obj", caused by missing "endobj" -- trying to recover.`);i-=f[1].length+1}}else i=l-h;const m=o.subarray(h,h+i),b=skipUntil(m,0,n);if(b0?Math.max(...this._xrefStms):null)}getEntry(e){const t=this.entries[e];return t&&!t.free&&t.offset?t:null}fetchIfRef(e,t=!1){return e instanceof Ref?this.fetch(e,t):e}fetch(e,t=!1){if(!(e instanceof Ref))throw new Error("ref object is not a reference");const a=e.num,r=this._cacheMap.get(a);if(void 0!==r){r instanceof Dict&&!r.objId&&(r.objId=e.toString());return r}let i=this.getEntry(a);if(null===i)return i;if(this._pendingRefs.has(e)){this._pendingRefs.remove(e);warn(`Ignoring circular reference: ${e}.`);return ya}this._pendingRefs.put(e);try{i=i.uncompressed?this.fetchUncompressed(e,i,t):this.fetchCompressed(e,i,t);this._pendingRefs.remove(e)}catch(t){this._pendingRefs.remove(e);throw t}i instanceof Dict?i.objId=e.toString():i instanceof BaseStream&&(i.dict.objId=e.toString());return i}fetchUncompressed(e,t,a=!1){const r=e.gen;let i=e.num;if(t.gen!==r){const n=`Inconsistent generation in XRef: ${e}`;if(this._generationFallback&&t.gen0&&t[3]-t[1]>0)return t;warn(`Empty, or invalid, /${e} entry.`)}return null}get mediaBox(){return shadow(this,"mediaBox",this.#Fe("MediaBox")||mc)}get cropBox(){return shadow(this,"cropBox",this.#Fe("CropBox")||this.mediaBox)}get userUnit(){const e=this.pageDict.get("UserUnit");return shadow(this,"userUnit","number"==typeof e&&e>0?e:1)}get view(){const{cropBox:e,mediaBox:t}=this;if(e!==t&&!isArrayEqual(e,t)){const a=Util.intersect(e,t);if(a&&a[2]-a[0]>0&&a[3]-a[1]>0)return shadow(this,"view",a);warn("Empty /CropBox and /MediaBox intersection.")}return shadow(this,"view",t)}get rotate(){let e=this.#ve("Rotate")||0;e%90!=0?e=0:e>=360?e%=360:e<0&&(e=(e%360+360)%360);return shadow(this,"rotate",e)}#Ie(e,t){if(!this.evaluatorOptions.ignoreErrors)throw e;warn(`getContentStream - ignoring sub-stream (${t}): "${e}".`)}async getContentStream(){const e=await this.pdfManager.ensure(this,"content");return e instanceof BaseStream?e:Array.isArray(e)?new StreamsSequenceStream(e,this.#Ie.bind(this)):new NullStream}get xfaData(){return shadow(this,"xfaData",this.xfaFactory?{bbox:this.xfaFactory.getBoundingBox(this.pageIndex)}:null)}async#Oe(e,t,a){const r=[];for(const i of e)if(i.id){const e=Ref.fromString(i.id);if(!e){warn(`A non-linked annotation cannot be modified: ${i.id}`);continue}if(i.deleted){t.put(e,e);if(i.popupRef){const e=Ref.fromString(i.popupRef);e&&t.put(e,e)}continue}a?.put(e);i.ref=e;r.push(this.xref.fetchAsync(e).then((e=>{e instanceof Dict&&(i.oldAnnotation=e.clone())}),(()=>{warn(`Cannot fetch \`oldAnnotation\` for: ${e}.`)})));delete i.id}await Promise.all(r)}async saveNewAnnotations(e,t,a,r,i){if(this.xfaFactory)throw new Error("XFA: Cannot save new annotations.");const n=this.#Ce(e),s=new RefSetCache,o=new RefSet;await this.#Oe(a,s,o);const c=this.pageDict,l=this.annotations.filter((e=>!(e instanceof Ref&&s.has(e)))),h=await AnnotationFactory.saveNewAnnotations(n,t,a,r,i);for(const{ref:e}of h.annotations)e instanceof Ref&&!o.has(e)&&l.push(e);const u=c.clone();u.set("Annots",l);i.put(this.ref,{data:u});for(const e of s)i.put(e,{data:null})}async save(e,t,a,r){const i=this.#Ce(e),n=await this._parsedAnnotations,s=[];for(const e of n)s.push(e.save(i,t,a,r).catch((function(e){warn(`save - ignoring annotation data during "${t.name}" task: "${e}".`);return null})));return Promise.all(s)}async loadResources(e){await(this.#ke??=this.pdfManager.ensure(this,"resources"));await ObjectLoader.load(this.resources,e,this.xref)}async#Te(e,t){const a=e?.get("Resources");if(!(a instanceof Dict&&a.size))return this.resources;await ObjectLoader.load(a,t,this.xref);return Dict.merge({xref:this.xref,dictArray:[a,this.resources],mergeSubDicts:!0})}async getOperatorList({handler:e,sink:t,task:a,intent:r,cacheKey:i,annotationStorage:c=null,modifiedIds:d=null}){const g=this.getContentStream(),p=this.loadResources(Ia),m=this.#Ce(e),b=this.xfaFactory?null:getNewAnnotationsMap(c),y=b?.get(this.pageIndex);let w=Promise.resolve(null),S=null;if(y){const e=this.pdfManager.ensureDoc("annotationGlobals");let t;const r=new Set;for(const{bitmapId:e,bitmap:t}of y)!e||t||r.has(e)||r.add(e);const{isOffscreenCanvasSupported:i}=this.evaluatorOptions;if(r.size>0){const e=y.slice();for(const[t,a]of c)t.startsWith(f)&&a.bitmap&&r.has(a.bitmapId)&&e.push(a);t=AnnotationFactory.generateImages(e,this.xref,i)}else t=AnnotationFactory.generateImages(y,this.xref,i);S=new RefSet;w=Promise.all([e,this.#Oe(y,S,null)]).then((([e])=>e?AnnotationFactory.printNewAnnotations(e,m,a,y,t):null))}const x=Promise.all([g,p]).then((async([n])=>{const s=await this.#Te(n.dict,Ia),o=new OperatorList(r,t);e.send("StartRenderPage",{transparency:m.hasBlendModes(s,this.nonBlendModesSet),pageIndex:this.pageIndex,cacheKey:i});await m.getOperatorList({stream:n,task:a,resources:s,operatorList:o});return o}));let[k,C,v]=await Promise.all([x,this._parsedAnnotations,w]);if(v){C=C.filter((e=>!(e.ref&&S.has(e.ref))));for(let e=0,t=v.length;ee.ref&&isRefsEqual(e.ref,a.refToReplace)));if(r>=0){C.splice(r,1,a);v.splice(e--,1);t--}}}C=C.concat(v)}if(0===C.length||r&h){k.flush(!0);return{length:k.totalLength}}const F=!!(r&l),O=!!(r&u),T=!!(r&n),M=!!(r&s),D=!!(r&o),R=[];for(const e of C)(T||M&&e.mustBeViewed(c,F)&&e.mustBeViewedWhenEditing(O,d)||D&&e.mustBePrinted(c))&&R.push(e.getOperatorList(m,a,r,c).catch((function(e){warn(`getOperatorList - ignoring annotation data during "${a.name}" task: "${e}".`);return{opList:null,separateForm:!1,separateCanvas:!1}})));const N=await Promise.all(R);let E=!1,L=!1;for(const{opList:e,separateForm:t,separateCanvas:a}of N){k.addOpList(e);E||=t;L||=a}k.flush(!0,{form:E,canvas:L});return{length:k.totalLength}}async extractTextContent({handler:e,task:t,includeMarkedContent:a,disableNormalization:r,sink:i}){const n=this.getContentStream(),s=this.loadResources(Oa),o=this.pdfManager.ensureCatalog("lang"),[c,,l]=await Promise.all([n,s,o]),h=await this.#Te(c.dict,Oa);return this.#Ce(e).getTextContent({stream:c,task:t,resources:h,includeMarkedContent:a,disableNormalization:r,sink:i,viewBox:this.view,lang:l})}async getStructTree(){const e=await this.pdfManager.ensureCatalog("structTreeRoot");if(!e)return null;await this._parsedAnnotations;try{const t=await this.pdfManager.ensure(this,"_parseStructTree",[e]);return await this.pdfManager.ensure(t,"serializable")}catch(e){warn(`getStructTree: "${e}".`);return null}}_parseStructTree(e){const t=new StructTreePage(e,this.pageDict);t.parse(this.ref);return t}async getAnnotationsData(e,t,a){const r=await this._parsedAnnotations;if(0===r.length)return r;const i=[],c=[];let l;const h=!!(a&n),u=!!(a&s),d=!!(a&o);for(const a of r){const r=h||u&&a.viewable;(r||d&&a.printable)&&i.push(a.data);if(a.hasTextContent&&r){l??=this.#Ce(e);c.push(a.extractTextContent(l,t,[-1/0,-1/0,1/0,1/0]).catch((function(e){warn(`getAnnotationsData - ignoring textContent during "${t.name}" task: "${e}".`)})))}}await Promise.all(c);return i}get annotations(){const e=this.#ve("Annots");return shadow(this,"annotations",Array.isArray(e)?e:[])}get _parsedAnnotations(){return shadow(this,"_parsedAnnotations",this.pdfManager.ensure(this,"annotations").then((async e=>{if(0===e.length)return e;const[t,a]=await Promise.all([this.pdfManager.ensureDoc("annotationGlobals"),this.pdfManager.ensureDoc("fieldObjects")]);if(!t)return[];const r=a?.orphanFields,i=[];for(const a of e)i.push(AnnotationFactory.create(this.xref,a,t,this._localIdFactory,!1,r,this.ref).catch((function(e){warn(`_parsedAnnotations: "${e}".`);return null})));const n=[];let s,o;for(const e of await Promise.all(i))e&&(e instanceof WidgetAnnotation?(o||=[]).push(e):e instanceof PopupAnnotation?(s||=[]).push(e):n.push(e));o&&n.push(...o);s&&n.push(...s);return n})))}get jsActions(){return shadow(this,"jsActions",collectActions(this.xref,this.pageDict,Se))}}const bc=new Uint8Array([37,80,68,70,45]),yc=new Uint8Array([115,116,97,114,116,120,114,101,102]),wc=new Uint8Array([101,110,100,111,98,106]);function find(e,t,a=1024,r=!1){const i=t.length,n=e.peekBytes(a),s=n.length-i;if(s<=0)return!1;if(r){const a=i-1;let r=n.length-1;for(;r>=a;){let s=0;for(;s=i){e.pos+=r-a;return!0}r--}}else{let a=0;for(;a<=s;){let r=0;for(;r=i){e.pos+=a;return!0}a++}}return!1}class PDFDocument{#Me=new Map;#De=null;constructor(e,t){if(t.length<=0)throw new InvalidPDFException("The PDF file is empty, i.e. its size is zero bytes.");this.pdfManager=e;this.stream=t;this.xref=new XRef(t,e);const a={font:0};this._globalIdFactory=class{static getDocId(){return`g_${e.docId}`}static createFontId(){return"f"+ ++a.font}static createObjId(){unreachable("Abstract method `createObjId` called.")}static getPageObjId(){unreachable("Abstract method `getPageObjId` called.")}}}parse(e){this.xref.parse(e);this.catalog=new Catalog(this.pdfManager,this.xref)}get linearization(){let e=null;try{e=Linearization.create(this.stream)}catch(e){if(e instanceof MissingDataException)throw e;info(e)}return shadow(this,"linearization",e)}get startXRef(){const e=this.stream;let t=0;if(this.linearization){e.reset();if(find(e,wc)){e.skip(6);let a=e.peekByte();for(;isWhiteSpace(a);){e.pos++;a=e.peekByte()}t=e.pos-e.start}}else{const a=1024,r=yc.length;let i=!1,n=e.end;for(;!i&&n>0;){n-=a-r;n<0&&(n=0);e.pos=n;i=find(e,yc,a,!0)}if(i){e.skip(9);let a;do{a=e.getByte()}while(isWhiteSpace(a));let r="";for(;a>=32&&a<=57;){r+=String.fromCharCode(a);a=e.getByte()}t=parseInt(r,10);isNaN(t)&&(t=0)}}return shadow(this,"startXRef",t)}checkHeader(){const e=this.stream;e.reset();if(!find(e,bc))return;e.moveStart();e.skip(bc.length);let t,a="";for(;(t=e.getByte())>32&&a.length<7;)a+=String.fromCharCode(t);Ca.test(a)?this.#De=a:warn(`Invalid PDF header version: ${a}`)}parseStartXRef(){this.xref.setStartXRef(this.startXRef)}get numPages(){let e=0;e=this.catalog.hasActualNumPages?this.catalog.numPages:this.xfaFactory?this.xfaFactory.getNumPages():this.linearization?this.linearization.numPages:this.catalog.numPages;return shadow(this,"numPages",e)}#Be(e,t=0){return!!Array.isArray(e)&&e.every((e=>{if(!((e=this.xref.fetchIfRef(e))instanceof Dict))return!1;if(e.has("Kids")){if(++t>10){warn("#hasOnlyDocumentSignatures: maximum recursion depth reached");return!1}return this.#Be(e.get("Kids"),t)}const a=isName(e.get("FT"),"Sig"),r=e.get("Rect"),i=Array.isArray(r)&&r.every((e=>0===e));return a&&i}))}get _xfaStreams(){const{acroForm:e}=this.catalog;if(!e)return null;const t=e.get("XFA"),a=new Map(["xdp:xdp","template","datasets","config","connectionSet","localeSet","stylesheet","/xdp:xdp"].map((e=>[e,null])));if(t instanceof BaseStream&&!t.isEmpty){a.set("xdp:xdp",t);return a}if(!Array.isArray(t)||0===t.length)return null;for(let e=0,r=t.length;el.handleSetFont(r,[Name.get(e),1],null,h,t,d,a,i).catch((e=>{warn(`loadXfaFonts: "${e}".`);return null})),f=[];for(const[e,t]of i){const a=t.get("FontDescriptor");if(!(a instanceof Dict))continue;let r=a.get("FontFamily");r=r.replaceAll(/[ ]+(\d)/g,"$1");const i={fontFamily:r,fontWeight:a.get("FontWeight"),italicAngle:-a.get("ItalicAngle")};validateCSSFont(i)&&f.push(parseFont(e,null,i))}await Promise.all(f);const g=this.xfaFactory.setFonts(u);if(!g)return;n.ignoreErrors=!0;f.length=0;u.length=0;const p=new Set;for(const e of g)getXfaFontName(`${e}-Regular`)||p.add(e);p.size&&g.push("PdfJS-Fallback");for(const e of g)if(!p.has(e))for(const t of[{name:"Regular",fontWeight:400,italicAngle:0},{name:"Bold",fontWeight:700,italicAngle:0},{name:"Italic",fontWeight:400,italicAngle:12},{name:"BoldItalic",fontWeight:700,italicAngle:12}]){const a=`${e}-${t.name}`;f.push(parseFont(a,getXfaFontDict(a),{fontFamily:e,fontWeight:t.fontWeight,italicAngle:t.italicAngle}))}await Promise.all(f);this.xfaFactory.appendFonts(u,p)}loadXfaResources(e,t){return Promise.all([this.#Ne(e,t).catch((()=>{})),this.#Re()])}serializeXfaData(e){return this.xfaFactory?this.xfaFactory.serializeData(e):null}get version(){return this.catalog.version||this.#De}get formInfo(){const e={hasFields:!1,hasAcroForm:!1,hasXfa:!1,hasSignatures:!1},{acroForm:t}=this.catalog;if(!t)return shadow(this,"formInfo",e);try{const a=t.get("Fields"),r=Array.isArray(a)&&a.length>0;e.hasFields=r;const i=t.get("XFA");e.hasXfa=Array.isArray(i)&&i.length>0||i instanceof BaseStream&&!i.isEmpty;const n=!!(1&t.get("SigFlags")),s=n&&this.#Be(a);e.hasAcroForm=r&&!s;e.hasSignatures=n}catch(e){if(e instanceof MissingDataException)throw e;warn(`Cannot fetch form information: "${e}".`)}return shadow(this,"formInfo",e)}get documentInfo(){const{catalog:e,formInfo:t,xref:a}=this,r={PDFFormatVersion:this.version,Language:e.lang,EncryptFilterName:a.encrypt?.filterName??null,IsLinearized:!!this.linearization,IsAcroFormPresent:t.hasAcroForm,IsXFAPresent:t.hasXfa,IsCollectionPresent:!!e.collection,IsSignaturesPresent:t.hasSignatures};let i;try{i=a.trailer.get("Info")}catch(e){if(e instanceof MissingDataException)throw e;info("The document information dictionary is invalid.")}if(!(i instanceof Dict))return shadow(this,"documentInfo",r);for(const[e,t]of i){switch(e){case"Title":case"Author":case"Subject":case"Keywords":case"Creator":case"Producer":case"CreationDate":case"ModDate":if("string"==typeof t){r[e]=stringToPDFString(t);continue}break;case"Trapped":if(t instanceof Name){r[e]=t;continue}break;default:let a;switch(typeof t){case"string":a=stringToPDFString(t);break;case"number":case"boolean":a=t;break;default:t instanceof Name&&(a=t)}if(void 0===a){warn(`Bad value, for custom key "${e}", in Info: ${t}.`);continue}r.Custom??=Object.create(null);r.Custom[e]=a;continue}warn(`Bad value, for key "${e}", in Info: ${t}.`)}return shadow(this,"documentInfo",r)}get fingerprints(){const e="\0".repeat(16);function validate(t){return"string"==typeof t&&16===t.length&&t!==e}const t=this.xref.trailer.get("ID");let a,r;if(Array.isArray(t)&&validate(t[0])){a=stringToBytes(t[0]);t[1]!==t[0]&&validate(t[1])&&(r=stringToBytes(t[1]))}else a=calculateMD5(this.stream.getByteRange(0,1024),0,1024);return shadow(this,"fingerprints",[toHexUtil(a),r?toHexUtil(r):null])}async#Ee(e){const{catalog:t,linearization:a,xref:r}=this,i=Ref.get(a.objectNumberFirst,0);try{const e=await r.fetchAsync(i);if(e instanceof Dict){let a=e.getRaw("Type");a instanceof Ref&&(a=await r.fetchAsync(a));if(isName(a,"Page")||!e.has("Type")&&!e.has("Kids")&&e.has("Contents")){t.pageKidsCountCache.has(i)||t.pageKidsCountCache.put(i,1);t.pageIndexCache.has(i)||t.pageIndexCache.put(i,0);return[e,i]}}throw new FormatError("The Linearization dictionary doesn't point to a valid Page dictionary.")}catch(a){warn(`_getLinearizationPage: "${a.message}".`);return t.getPageDict(e)}}getPage(e){const t=this.#Me.get(e);if(t)return t;const{catalog:a,linearization:r,xfaFactory:i}=this;let n;n=i?Promise.resolve([Dict.empty,null]):r?.pageFirst===e?this.#Ee(e):a.getPageDict(e);n=n.then((([t,r])=>new Page({pdfManager:this.pdfManager,xref:this.xref,pageIndex:e,pageDict:t,ref:r,globalIdFactory:this._globalIdFactory,fontCache:a.fontCache,builtInCMapCache:a.builtInCMapCache,standardFontDataCache:a.standardFontDataCache,globalColorSpaceCache:a.globalColorSpaceCache,globalImageCache:a.globalImageCache,systemFontCache:a.systemFontCache,nonBlendModesSet:a.nonBlendModesSet,xfaFactory:i})));this.#Me.set(e,n);return n}async checkFirstPage(e=!1){if(!e)try{await this.getPage(0)}catch(e){if(e instanceof XRefEntryException){this.#Me.delete(0);await this.cleanup();throw new XRefParseException}}}async checkLastPage(e=!1){const{catalog:t,pdfManager:a}=this;t.setActualNumPages();let r;try{await Promise.all([a.ensureDoc("xfaFactory"),a.ensureDoc("linearization"),a.ensureCatalog("numPages")]);if(this.xfaFactory)return;r=this.linearization?this.linearization.numPages:t.numPages;if(!Number.isInteger(r))throw new FormatError("Page count is not an integer.");if(r<=1)return;await this.getPage(r-1)}catch(i){this.#Me.delete(r-1);await this.cleanup();if(i instanceof XRefEntryException&&!e)throw new XRefParseException;warn(`checkLastPage - invalid /Pages tree /Count: ${r}.`);let n;try{n=await t.getAllPageDicts(e)}catch(a){if(a instanceof XRefEntryException&&!e)throw new XRefParseException;t.setActualNumPages(1);return}for(const[e,[r,i]]of n){let n;if(r instanceof Error){n=Promise.reject(r);n.catch((()=>{}))}else n=Promise.resolve(new Page({pdfManager:a,xref:this.xref,pageIndex:e,pageDict:r,ref:i,globalIdFactory:this._globalIdFactory,fontCache:t.fontCache,builtInCMapCache:t.builtInCMapCache,standardFontDataCache:t.standardFontDataCache,globalColorSpaceCache:this.globalColorSpaceCache,globalImageCache:t.globalImageCache,systemFontCache:t.systemFontCache,nonBlendModesSet:t.nonBlendModesSet,xfaFactory:null}));this.#Me.set(e,n)}t.setActualNumPages(n.size)}}async fontFallback(e,t){const{catalog:a,pdfManager:r}=this;for(const i of await Promise.all(a.fontCache))if(i.loadedName===e){i.fallback(t,r.evaluatorOptions);return}}async cleanup(e=!1){return this.catalog?this.catalog.cleanup(e):clearGlobalCaches()}async#Pe(e,t,a,r,i,n,s){const{xref:o}=this;if(!(a instanceof Ref)||n.has(a))return;n.put(a);const c=await o.fetchAsync(a);if(!(c instanceof Dict))return;let l=await c.getAsync("Subtype");l=l instanceof Name?l.name:null;if("Link"===l)return;if(c.has("T")){const t=stringToPDFString(await c.getAsync("T"));e=""===e?t:`${e}.${t}`}else{let a=c;for(;;){a=a.getRaw("Parent")||t;if(a instanceof Ref){if(n.has(a))break;a=await o.fetchAsync(a)}if(!(a instanceof Dict))break;if(a.has("T")){const t=stringToPDFString(await a.getAsync("T"));e=""===e?t:`${e}.${t}`;break}}}t&&!c.has("Parent")&&isName(c.get("Subtype"),"Widget")&&s.put(a,t);r.has(e)||r.set(e,[]);r.get(e).push(AnnotationFactory.create(o,a,i,null,!0,s,null).then((e=>e?.getFieldObject())).catch((function(e){warn(`#collectFieldObjects: "${e}".`);return null})));if(!c.has("Kids"))return;const h=await c.getAsync("Kids");if(Array.isArray(h))for(const t of h)await this.#Pe(e,a,t,r,i,n,s)}get fieldObjects(){return shadow(this,"fieldObjects",this.pdfManager.ensureDoc("formInfo").then((async e=>{if(!e.hasFields)return null;const t=await this.annotationGlobals;if(!t)return null;const{acroForm:a}=t,r=new RefSet,i=Object.create(null),n=new Map,s=new RefSetCache;for(const e of a.get("Fields"))await this.#Pe("",null,e,n,t,r,s);const o=[];for(const[e,t]of n)o.push(Promise.all(t).then((t=>{(t=t.filter((e=>!!e))).length>0&&(i[e]=t)})));await Promise.all(o);return{allFields:objectSize(i)>0?i:null,orphanFields:s}})))}get hasJSActions(){return shadow(this,"hasJSActions",this.pdfManager.ensureDoc("_parseHasJSActions"))}async _parseHasJSActions(){const[e,t]=await Promise.all([this.pdfManager.ensureCatalog("jsActions"),this.pdfManager.ensureDoc("fieldObjects")]);return!!e||!!t?.allFields&&Object.values(t.allFields).some((e=>e.some((e=>null!==e.actions))))}get calculationOrderIds(){const e=this.catalog.acroForm?.get("CO");if(!Array.isArray(e)||0===e.length)return shadow(this,"calculationOrderIds",null);const t=[];for(const a of e)a instanceof Ref&&t.push(a.toString());return shadow(this,"calculationOrderIds",t.length?t:null)}get annotationGlobals(){return shadow(this,"annotationGlobals",AnnotationFactory.createGlobals(this.pdfManager))}}class BasePdfManager{constructor({docBaseUrl:e,docId:t,enableXfa:a,evaluatorOptions:r,handler:i,password:n}){this._docBaseUrl=function parseDocBaseUrl(e){if(e){const t=createValidAbsoluteUrl(e);if(t)return t.href;warn(`Invalid absolute docBaseUrl: "${e}".`)}return null}(e);this._docId=t;this._password=n;this.enableXfa=a;r.isOffscreenCanvasSupported&&=FeatureTest.isOffscreenCanvasSupported;r.isImageDecoderSupported&&=FeatureTest.isImageDecoderSupported;this.evaluatorOptions=Object.freeze(r);ImageResizer.setOptions(r);JpegStream.setOptions(r);OperatorList.setOptions(r);const s={...r,handler:i};JpxImage.setOptions(s);IccColorSpace.setOptions(s);CmykICCBasedCS.setOptions(s)}get docId(){return this._docId}get password(){return this._password}get docBaseUrl(){return this._docBaseUrl}ensureDoc(e,t){return this.ensure(this.pdfDocument,e,t)}ensureXRef(e,t){return this.ensure(this.pdfDocument.xref,e,t)}ensureCatalog(e,t){return this.ensure(this.pdfDocument.catalog,e,t)}getPage(e){return this.pdfDocument.getPage(e)}fontFallback(e,t){return this.pdfDocument.fontFallback(e,t)}cleanup(e=!1){return this.pdfDocument.cleanup(e)}async ensure(e,t,a){unreachable("Abstract method `ensure` called")}requestRange(e,t){unreachable("Abstract method `requestRange` called")}requestLoadedStream(e=!1){unreachable("Abstract method `requestLoadedStream` called")}sendProgressiveData(e){unreachable("Abstract method `sendProgressiveData` called")}updatePassword(e){this._password=e}terminate(e){unreachable("Abstract method `terminate` called")}}class LocalPdfManager extends BasePdfManager{constructor(e){super(e);const t=new Stream(e.source);this.pdfDocument=new PDFDocument(this,t);this._loadedStreamPromise=Promise.resolve(t)}async ensure(e,t,a){const r=e[t];return"function"==typeof r?r.apply(e,a):r}requestRange(e,t){return Promise.resolve()}requestLoadedStream(e=!1){return this._loadedStreamPromise}terminate(e){}}class NetworkPdfManager extends BasePdfManager{constructor(e){super(e);this.streamManager=new ChunkedStreamManager(e.source,{msgHandler:e.handler,length:e.length,disableAutoFetch:e.disableAutoFetch,rangeChunkSize:e.rangeChunkSize});this.pdfDocument=new PDFDocument(this,this.streamManager.getStream())}async ensure(e,t,a){try{const r=e[t];return"function"==typeof r?r.apply(e,a):r}catch(r){if(!(r instanceof MissingDataException))throw r;await this.requestRange(r.begin,r.end);return this.ensure(e,t,a)}}requestRange(e,t){return this.streamManager.requestRange(e,t)}requestLoadedStream(e=!1){return this.streamManager.requestAllChunks(e)}sendProgressiveData(e){this.streamManager.onReceiveData({chunk:e})}terminate(e){this.streamManager.abort(e)}}const Sc=1,xc=2,Ac=1,kc=2,Cc=3,vc=4,Fc=5,Ic=6,Oc=7,Tc=8;function onFn(){}function wrapReason(e){if(e instanceof AbortException||e instanceof InvalidPDFException||e instanceof PasswordException||e instanceof ResponseException||e instanceof UnknownErrorException)return e;e instanceof Error||"object"==typeof e&&null!==e||unreachable('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(e.name){case"AbortException":return new AbortException(e.message);case"InvalidPDFException":return new InvalidPDFException(e.message);case"PasswordException":return new PasswordException(e.message,e.code);case"ResponseException":return new ResponseException(e.message,e.status,e.missing);case"UnknownErrorException":return new UnknownErrorException(e.message,e.details)}return new UnknownErrorException(e.message,e.toString())}class MessageHandler{#Le=new AbortController;constructor(e,t,a){this.sourceName=e;this.targetName=t;this.comObj=a;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);a.addEventListener("message",this.#je.bind(this),{signal:this.#Le.signal})}#je({data:e}){if(e.targetName!==this.sourceName)return;if(e.stream){this.#_e(e);return}if(e.callback){const t=e.callbackId,a=this.callbackCapabilities[t];if(!a)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===Sc)a.resolve(e.data);else{if(e.callback!==xc)throw new Error("Unexpected callback case");a.reject(wrapReason(e.reason))}return}const t=this.actionHandler[e.action];if(!t)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const a=this.sourceName,r=e.sourceName,i=this.comObj;Promise.try(t,e.data).then((function(t){i.postMessage({sourceName:a,targetName:r,callback:Sc,callbackId:e.callbackId,data:t})}),(function(t){i.postMessage({sourceName:a,targetName:r,callback:xc,callbackId:e.callbackId,reason:wrapReason(t)})}))}else e.streamId?this.#Ue(e):t(e.data)}on(e,t){const a=this.actionHandler;if(a[e])throw new Error(`There is already an actionName called "${e}"`);a[e]=t}send(e,t,a){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,data:t},a)}sendWithPromise(e,t,a){const r=this.callbackId++,i=Promise.withResolvers();this.callbackCapabilities[r]=i;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:e,callbackId:r,data:t},a)}catch(e){i.reject(e)}return i.promise}sendWithStream(e,t,a,r){const i=this.streamId++,n=this.sourceName,s=this.targetName,o=this.comObj;return new ReadableStream({start:a=>{const c=Promise.withResolvers();this.streamControllers[i]={controller:a,startCall:c,pullCall:null,cancelCall:null,isClosed:!1};o.postMessage({sourceName:n,targetName:s,action:e,streamId:i,data:t,desiredSize:a.desiredSize},r);return c.promise},pull:e=>{const t=Promise.withResolvers();this.streamControllers[i].pullCall=t;o.postMessage({sourceName:n,targetName:s,stream:Ic,streamId:i,desiredSize:e.desiredSize});return t.promise},cancel:e=>{assert(e instanceof Error,"cancel must have a valid reason");const t=Promise.withResolvers();this.streamControllers[i].cancelCall=t;this.streamControllers[i].isClosed=!0;o.postMessage({sourceName:n,targetName:s,stream:Ac,streamId:i,reason:wrapReason(e)});return t.promise}},a)}#Ue(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this,s=this.actionHandler[e.action],o={enqueue(e,n=1,s){if(this.isCancelled)return;const o=this.desiredSize;this.desiredSize-=n;if(o>0&&this.desiredSize<=0){this.sinkCapability=Promise.withResolvers();this.ready=this.sinkCapability.promise}i.postMessage({sourceName:a,targetName:r,stream:vc,streamId:t,chunk:e},s)},close(){if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Cc,streamId:t});delete n.streamSinks[t]}},error(e){assert(e instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;i.postMessage({sourceName:a,targetName:r,stream:Fc,streamId:t,reason:wrapReason(e)})}},sinkCapability:Promise.withResolvers(),onPull:null,onCancel:null,isCancelled:!1,desiredSize:e.desiredSize,ready:null};o.sinkCapability.resolve();o.ready=o.sinkCapability.promise;this.streamSinks[t]=o;Promise.try(s,e.data,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Tc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Tc,streamId:t,reason:wrapReason(e)})}))}#_e(e){const t=e.streamId,a=this.sourceName,r=e.sourceName,i=this.comObj,n=this.streamControllers[t],s=this.streamSinks[t];switch(e.stream){case Tc:e.success?n.startCall.resolve():n.startCall.reject(wrapReason(e.reason));break;case Oc:e.success?n.pullCall.resolve():n.pullCall.reject(wrapReason(e.reason));break;case Ic:if(!s){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,success:!0});break}s.desiredSize<=0&&e.desiredSize>0&&s.sinkCapability.resolve();s.desiredSize=e.desiredSize;Promise.try(s.onPull||onFn).then((function(){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:Oc,streamId:t,reason:wrapReason(e)})}));break;case vc:assert(n,"enqueue should have stream controller");if(n.isClosed)break;n.controller.enqueue(e.chunk);break;case Cc:assert(n,"close should have stream controller");if(n.isClosed)break;n.isClosed=!0;n.controller.close();this.#Xe(n,t);break;case Fc:assert(n,"error should have stream controller");n.controller.error(wrapReason(e.reason));this.#Xe(n,t);break;case kc:e.success?n.cancelCall.resolve():n.cancelCall.reject(wrapReason(e.reason));this.#Xe(n,t);break;case Ac:if(!s)break;const o=wrapReason(e.reason);Promise.try(s.onCancel||onFn,o).then((function(){i.postMessage({sourceName:a,targetName:r,stream:kc,streamId:t,success:!0})}),(function(e){i.postMessage({sourceName:a,targetName:r,stream:kc,streamId:t,reason:wrapReason(e)})}));s.sinkCapability.reject(o);s.isCancelled=!0;delete this.streamSinks[t];break;default:throw new Error("Unexpected stream case")}}async#Xe(e,t){await Promise.allSettled([e.startCall?.promise,e.pullCall?.promise,e.cancelCall?.promise]);delete this.streamControllers[t]}destroy(){this.#Le?.abort();this.#Le=null}}async function writeObject(e,t,a,{encrypt:r=null}){const i=r?.createCipherTransform(e.num,e.gen);a.push(`${e.num} ${e.gen} obj\n`);t instanceof Dict?await writeDict(t,a,i):t instanceof BaseStream?await writeStream(t,a,i):(Array.isArray(t)||ArrayBuffer.isView(t))&&await writeArray(t,a,i);a.push("\nendobj\n")}async function writeDict(e,t,a){t.push("<<");for(const r of e.getKeys()){t.push(` /${escapePDFName(r)} `);await writeValue(e.getRaw(r),t,a)}t.push(">>")}async function writeStream(e,t,a){let r=e.getBytes();const{dict:i}=e,[n,s]=await Promise.all([i.getAsync("Filter"),i.getAsync("DecodeParms")]),o=isName(Array.isArray(n)?await i.xref.fetchIfRefAsync(n[0]):n,"FlateDecode");if(r.length>=256||o)try{const e=new CompressionStream("deflate"),t=e.writable.getWriter();await t.ready;t.write(r).then((async()=>{await t.ready;await t.close()})).catch((()=>{}));const a=await new Response(e.readable).arrayBuffer();r=new Uint8Array(a);let c,l;if(n){if(!o){c=Array.isArray(n)?[Name.get("FlateDecode"),...n]:[Name.get("FlateDecode"),n];s&&(l=Array.isArray(s)?[null,...s]:[null,s])}}else c=Name.get("FlateDecode");c&&i.set("Filter",c);l&&i.set("DecodeParms",l)}catch(e){info(`writeStream - cannot compress data: "${e}".`)}let c=bytesToString(r);a&&(c=a.encryptString(c));i.set("Length",c.length);await writeDict(i,t,a);t.push(" stream\n",c,"\nendstream")}async function writeArray(e,t,a){t.push("[");let r=!0;for(const i of e){r?r=!1:t.push(" ");await writeValue(i,t,a)}t.push("]")}async function writeValue(e,t,a){if(e instanceof Name)t.push(`/${escapePDFName(e.name)}`);else if(e instanceof Ref)t.push(`${e.num} ${e.gen} R`);else if(Array.isArray(e)||ArrayBuffer.isView(e))await writeArray(e,t,a);else if("string"==typeof e){a&&(e=a.encryptString(e));t.push(`(${escapeString(e)})`)}else"number"==typeof e?t.push(numberToString(e)):"boolean"==typeof e?t.push(e.toString()):e instanceof Dict?await writeDict(e,t,a):e instanceof BaseStream?await writeStream(e,t,a):null===e?t.push("null"):warn(`Unhandled value in writer: ${typeof e}, please file a bug.`)}function writeInt(e,t,a,r){for(let i=t+a-1;i>a-1;i--){r[i]=255&e;e>>=8}return a+t}function writeString(e,t,a){const r=e.length;for(let i=0;i1&&(n=a.documentElement.searchNode([i.at(-1)],0));n?n.childNodes=Array.isArray(r)?r.map((e=>new SimpleDOMNode("value",e))):[new SimpleDOMNode("#text",r)]:warn(`Node not found for path: ${t}`)}const r=[];a.documentElement.dump(r);return r.join("")}(r.fetchIfRef(t).getString(),a)}const i=new StringStream(e);i.dict=new Dict(r);i.dict.set("Type",Name.get("EmbeddedFile"));a.put(t,{data:i})}function getIndexes(e){const t=[];for(const{ref:a}of e)a.num===t.at(-2)+t.at(-1)?t[t.length-1]+=1:t.push(a.num,1);return t}function computeIDs(e,t,a){if(Array.isArray(t.fileIds)&&t.fileIds.length>0){const r=function computeMD5(e,t){const a=Math.floor(Date.now()/1e3),r=t.filename||"",i=[a.toString(),r,e.toString(),...t.infoMap.values()],n=Math.sumPrecise(i.map((e=>e.length))),s=new Uint8Array(n);let o=0;for(const e of i)o=writeString(e,o,s);return bytesToString(calculateMD5(s,0,s.length))}(e,t);a.set("ID",[t.fileIds[0],r])}}async function incrementalUpdate({originalData:e,xrefInfo:t,changes:a,xref:r=null,hasXfa:i=!1,xfaDatasetsRef:n=null,hasXfaDatasetsEntry:s=!1,needAppearances:o,acroFormRef:c=null,acroForm:l=null,xfaData:h=null,useXrefStream:u=!1}){await async function updateAcroform({xref:e,acroForm:t,acroFormRef:a,hasXfa:r,hasXfaDatasetsEntry:i,xfaDatasetsRef:n,needAppearances:s,changes:o}){!r||i||n||warn("XFA - Cannot save it");if(!s&&(!r||!n||i))return;const c=t.clone();if(r&&!i){const e=t.get("XFA").slice();e.splice(2,0,"datasets");e.splice(3,0,n);c.set("XFA",e)}s&&c.set("NeedAppearances",!0);o.put(a,{data:c})}({xref:r,acroForm:l,acroFormRef:c,hasXfa:i,hasXfaDatasetsEntry:s,xfaDatasetsRef:n,needAppearances:o,changes:a});i&&updateXFA({xfaData:h,xfaDatasetsRef:n,changes:a,xref:r});const d=function getTrailerDict(e,t,a){const r=new Dict(null);r.set("Prev",e.startXRef);const i=e.newRef;if(a){t.put(i,{data:""});r.set("Size",i.num+1);r.set("Type",Name.get("XRef"))}else r.set("Size",i.num);null!==e.rootRef&&r.set("Root",e.rootRef);null!==e.infoRef&&r.set("Info",e.infoRef);null!==e.encryptRef&&r.set("Encrypt",e.encryptRef);return r}(t,a,u),f=[],g=await async function writeChanges(e,t,a=[]){const r=[];for(const[i,{data:n}]of e.items())if(null!==n&&"string"!=typeof n){await writeObject(i,n,a,t);r.push({ref:i,data:a.join("")});a.length=0}else r.push({ref:i,data:n});return r.sort(((e,t)=>e.ref.num-t.ref.num))}(a,r,f);let p=e.length;const m=e.at(-1);if(10!==m&&13!==m){f.push("\n");p+=1}for(const{data:e}of g)null!==e&&f.push(e);await(u?async function getXRefStreamTable(e,t,a,r,i){const n=[];let s=0,o=0;for(const{ref:e,data:r}of a){let a;s=Math.max(s,t);if(null!==r){a=Math.min(e.gen,65535);n.push([1,t,a]);t+=r.length}else{a=Math.min(e.gen+1,65535);n.push([0,0,a])}o=Math.max(o,a)}r.set("Index",getIndexes(a));const c=[1,getSizeInBytes(s),getSizeInBytes(o)];r.set("W",c);computeIDs(t,e,r);const l=Math.sumPrecise(c),h=new Uint8Array(l*n.length),u=new Stream(h);u.dict=r;let d=0;for(const[e,t,a]of n){d=writeInt(e,c[0],d,h);d=writeInt(t,c[1],d,h);d=writeInt(a,c[2],d,h)}await writeObject(e.newRef,u,i,{});i.push("startxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f):async function getXRefTable(e,t,a,r,i){i.push("xref\n");const n=getIndexes(a);let s=0;for(const{ref:e,data:r}of a){if(e.num===n[s]){i.push(`${n[s]} ${n[s+1]}\n`);s+=2}if(null!==r){i.push(`${t.toString().padStart(10,"0")} ${Math.min(e.gen,65535).toString().padStart(5,"0")} n\r\n`);t+=r.length}else i.push(`0000000000 ${Math.min(e.gen+1,65535).toString().padStart(5,"0")} f\r\n`)}computeIDs(t,e,r);i.push("trailer\n");await writeDict(r,i);i.push("\nstartxref\n",t.toString(),"\n%%EOF\n")}(t,p,g,d,f));const b=e.length+Math.sumPrecise(f.map((e=>e.length))),y=new Uint8Array(b);y.set(e);let w=e.length;for(const e of f)w=writeString(e,w,y);return y}class PDFWorkerStream{constructor(e){this._msgHandler=e;this._contentLength=null;this._fullRequestReader=null;this._rangeRequestReaders=[]}getFullReader(){assert(!this._fullRequestReader,"PDFWorkerStream.getFullReader can only be called once.");this._fullRequestReader=new PDFWorkerStreamReader(this._msgHandler);return this._fullRequestReader}getRangeReader(e,t){const a=new PDFWorkerStreamRangeReader(e,t,this._msgHandler);this._rangeRequestReaders.push(a);return a}cancelAllRequests(e){this._fullRequestReader?.cancel(e);for(const t of this._rangeRequestReaders.slice(0))t.cancel(e)}}class PDFWorkerStreamReader{constructor(e){this._msgHandler=e;this.onProgress=null;this._contentLength=null;this._isRangeSupported=!1;this._isStreamingSupported=!1;const t=this._msgHandler.sendWithStream("GetReader");this._reader=t.getReader();this._headersReady=this._msgHandler.sendWithPromise("ReaderHeadersReady").then((e=>{this._isStreamingSupported=e.isStreamingSupported;this._isRangeSupported=e.isRangeSupported;this._contentLength=e.contentLength}))}get headersReady(){return this._headersReady}get contentLength(){return this._contentLength}get isStreamingSupported(){return this._isStreamingSupported}get isRangeSupported(){return this._isRangeSupported}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class PDFWorkerStreamRangeReader{constructor(e,t,a){this._msgHandler=a;this.onProgress=null;const r=this._msgHandler.sendWithStream("GetRangeReader",{begin:e,end:t});this._reader=r.getReader()}get isStreamingSupported(){return!1}async read(){const{value:e,done:t}=await this._reader.read();return t?{value:void 0,done:!0}:{value:e.buffer,done:!1}}cancel(e){this._reader.cancel(e)}}class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=Promise.withResolvers()}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}class WorkerMessageHandler{static{"undefined"==typeof window&&!e&&"undefined"!=typeof self&&"function"==typeof self.postMessage&&"onmessage"in self&&this.initializeFromPort(self)}static setup(e,t){let a=!1;e.on("test",(t=>{if(!a){a=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(e=>{!function setVerbosityLevel(e){Number.isInteger(e)&&(da=e)}(e.verbosity)}));e.on("GetDocRequest",(e=>this.createDocumentHandler(e,t)))}static createDocumentHandler(e,t){let a,r=!1,i=null;const n=new Set,s=getVerbosityLevel(),{docId:o,apiVersion:c}=e,l="5.3.31";if(c!==l)throw new Error(`The API version "${c}" does not match the Worker version "${l}".`);const buildMsg=(e,t)=>`The \`${e}.prototype\` contains unexpected enumerable property "${t}", thus breaking e.g. \`for...in\` iteration of ${e}s.`;for(const e in{})throw new Error(buildMsg("Object",e));for(const e in[])throw new Error(buildMsg("Array",e));const h=o+"_worker";let u=new MessageHandler(h,o,t);function ensureNotTerminated(){if(r)throw new Error("Worker was terminated")}function startWorkerTask(e){n.add(e)}function finishWorkerTask(e){e.finish();n.delete(e)}async function loadDocument(e){await a.ensureDoc("checkHeader");await a.ensureDoc("parseStartXRef");await a.ensureDoc("parse",[e]);await a.ensureDoc("checkFirstPage",[e]);await a.ensureDoc("checkLastPage",[e]);const t=await a.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaResources");startWorkerTask(e);await a.ensureDoc("loadXfaResources",[u,e]);finishWorkerTask(e)}const[r,i]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("fingerprints")]);return{numPages:r,fingerprints:i,htmlForXfa:t?await a.ensureDoc("htmlForXfa"):null}}function setupDoc(e){function onSuccess(e){ensureNotTerminated();u.send("GetDoc",{pdfInfo:e})}function onFailure(e){ensureNotTerminated();if(e instanceof PasswordException){const t=new WorkerTask(`PasswordException: response ${e.code}`);startWorkerTask(t);u.sendWithPromise("PasswordRequest",e).then((function({password:e}){finishWorkerTask(t);a.updatePassword(e);pdfManagerReady()})).catch((function(){finishWorkerTask(t);u.send("DocException",e)}))}else u.send("DocException",wrapReason(e))}function pdfManagerReady(){ensureNotTerminated();loadDocument(!1).then(onSuccess,(function(e){ensureNotTerminated();e instanceof XRefParseException?a.requestLoadedStream().then((function(){ensureNotTerminated();loadDocument(!0).then(onSuccess,onFailure)})):onFailure(e)}))}ensureNotTerminated();(async function getPdfManager({data:e,password:t,disableAutoFetch:a,rangeChunkSize:r,length:n,docBaseUrl:s,enableXfa:c,evaluatorOptions:l}){const h={source:null,disableAutoFetch:a,docBaseUrl:s,docId:o,enableXfa:c,evaluatorOptions:l,handler:u,length:n,password:t,rangeChunkSize:r};if(e){h.source=e;return new LocalPdfManager(h)}const d=new PDFWorkerStream(u),f=d.getFullReader(),g=Promise.withResolvers();let p,m=[],b=0;f.headersReady.then((function(){if(f.isRangeSupported){h.source=d;h.length=f.contentLength;h.disableAutoFetch||=f.isStreamingSupported;p=new NetworkPdfManager(h);for(const e of m)p.sendProgressiveData(e);m=[];g.resolve(p);i=null}})).catch((function(e){g.reject(e);i=null}));new Promise((function(e,t){const readChunk=function({value:e,done:a}){try{ensureNotTerminated();if(a){if(!p){const e=arrayBuffersToBytes(m);m=[];n&&e.length!==n&&warn("reported HTTP length is different from actual");h.source=e;p=new LocalPdfManager(h);g.resolve(p)}i=null;return}b+=e.byteLength;f.isStreamingSupported||u.send("DocProgress",{loaded:b,total:Math.max(b,f.contentLength||0)});p?p.sendProgressiveData(e):m.push(e);f.read().then(readChunk,t)}catch(e){t(e)}};f.read().then(readChunk,t)})).catch((function(e){g.reject(e);i=null}));i=e=>{d.cancelAllRequests(e)};return g.promise})(e).then((function(e){if(r){e.terminate(new AbortException("Worker was terminated."));throw new Error("Worker was terminated")}a=e;a.requestLoadedStream(!0).then((e=>{u.send("DataLoaded",{length:e.bytes.byteLength})}))})).then(pdfManagerReady,onFailure)}u.on("GetPage",(function(e){return a.getPage(e.pageIndex).then((function(e){return Promise.all([a.ensure(e,"rotate"),a.ensure(e,"ref"),a.ensure(e,"userUnit"),a.ensure(e,"view")]).then((function([e,t,a,r]){return{rotate:e,ref:t,refStr:t?.toString()??null,userUnit:a,view:r}}))}))}));u.on("GetPageIndex",(function(e){const t=Ref.get(e.num,e.gen);return a.ensureCatalog("getPageIndex",[t])}));u.on("GetDestinations",(function(e){return a.ensureCatalog("destinations")}));u.on("GetDestination",(function(e){return a.ensureCatalog("getDestination",[e.id])}));u.on("GetPageLabels",(function(e){return a.ensureCatalog("pageLabels")}));u.on("GetPageLayout",(function(e){return a.ensureCatalog("pageLayout")}));u.on("GetPageMode",(function(e){return a.ensureCatalog("pageMode")}));u.on("GetViewerPreferences",(function(e){return a.ensureCatalog("viewerPreferences")}));u.on("GetOpenAction",(function(e){return a.ensureCatalog("openAction")}));u.on("GetAttachments",(function(e){return a.ensureCatalog("attachments")}));u.on("GetDocJSActions",(function(e){return a.ensureCatalog("jsActions")}));u.on("GetPageJSActions",(function({pageIndex:e}){return a.getPage(e).then((e=>a.ensure(e,"jsActions")))}));u.on("GetOutline",(function(e){return a.ensureCatalog("documentOutline")}));u.on("GetOptionalContentConfig",(function(e){return a.ensureCatalog("optionalContentConfig")}));u.on("GetPermissions",(function(e){return a.ensureCatalog("permissions")}));u.on("GetMetadata",(function(e){return Promise.all([a.ensureDoc("documentInfo"),a.ensureCatalog("metadata")])}));u.on("GetMarkInfo",(function(e){return a.ensureCatalog("markInfo")}));u.on("GetData",(function(e){return a.requestLoadedStream().then((e=>e.bytes))}));u.on("GetAnnotations",(function({pageIndex:e,intent:t}){return a.getPage(e).then((function(a){const r=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(r);return a.getAnnotationsData(u,r,t).then((e=>{finishWorkerTask(r);return e}),(e=>{finishWorkerTask(r);throw e}))}))}));u.on("GetFieldObjects",(function(e){return a.ensureDoc("fieldObjects").then((e=>e?.allFields||null))}));u.on("HasJSActions",(function(e){return a.ensureDoc("hasJSActions")}));u.on("GetCalculationOrderIds",(function(e){return a.ensureDoc("calculationOrderIds")}));u.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:r,filename:i}){const n=[a.requestLoadedStream(),a.ensureCatalog("acroForm"),a.ensureCatalog("acroFormRef"),a.ensureDoc("startXRef"),a.ensureDoc("xref"),a.ensureDoc("linearization"),a.ensureCatalog("structTreeRoot")],s=new RefSetCache,o=[],c=e?null:getNewAnnotationsMap(r),[l,h,d,f,g,p,m]=await Promise.all(n),b=g.trailer.getRaw("Root")||null;let y;if(c){m?await m.canUpdateStructTree({pdfManager:a,newAnnotationsByPage:c})&&(y=m):await StructTreeRoot.canCreateStructureTree({catalogRef:b,pdfManager:a,newAnnotationsByPage:c})&&(y=null);const e=AnnotationFactory.generateImages(r.values(),g,a.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===y?o:[];for(const[r,i]of c)t.push(a.getPage(r).then((t=>{const a=new WorkerTask(`Save (editor): page ${r}`);startWorkerTask(a);return t.saveNewAnnotations(u,a,i,e,s).finally((function(){finishWorkerTask(a)}))})));null===y?o.push(Promise.all(t).then((async()=>{await StructTreeRoot.createStructureTree({newAnnotationsByPage:c,xref:g,catalogRef:b,pdfManager:a,changes:s})}))):y&&o.push(Promise.all(t).then((async()=>{await y.updateStructureTree({newAnnotationsByPage:c,pdfManager:a,changes:s})})))}if(e)o.push(a.ensureDoc("serializeXfaData",[r]));else for(let e=0;ee.needAppearances)),k=h instanceof Dict&&h.get("XFA")||null;let C=null,v=!1;if(Array.isArray(k)){for(let e=0,t=k.length;e{g.resetNewTemporaryRef()}))}));u.on("GetOperatorList",(function(e,t){const r=e.pageIndex;a.getPage(r).then((function(a){const i=new WorkerTask(`GetOperatorList: page ${r}`);startWorkerTask(i);const n=s>=Ae?Date.now():0;a.getOperatorList({handler:u,sink:t,task:i,intent:e.intent,cacheKey:e.cacheKey,annotationStorage:e.annotationStorage,modifiedIds:e.modifiedIds}).then((function(e){finishWorkerTask(i);n&&info(`page=${r+1} - getOperatorList: time=${Date.now()-n}ms, len=${e.length}`);t.close()}),(function(e){finishWorkerTask(i);i.terminated||t.error(e)}))}))}));u.on("GetTextContent",(function(e,t){const{pageIndex:r,includeMarkedContent:i,disableNormalization:n}=e;a.getPage(r).then((function(e){const a=new WorkerTask("GetTextContent: page "+r);startWorkerTask(a);const o=s>=Ae?Date.now():0;e.extractTextContent({handler:u,task:a,sink:t,includeMarkedContent:i,disableNormalization:n}).then((function(){finishWorkerTask(a);o&&info(`page=${r+1} - getTextContent: time=`+(Date.now()-o)+"ms");t.close()}),(function(e){finishWorkerTask(a);a.terminated||t.error(e)}))}))}));u.on("GetStructTree",(function(e){return a.getPage(e.pageIndex).then((e=>a.ensure(e,"getStructTree")))}));u.on("FontFallback",(function(e){return a.fontFallback(e.id,u)}));u.on("Cleanup",(function(e){return a.cleanup(!0)}));u.on("Terminate",(function(e){r=!0;const t=[];if(a){a.terminate(new AbortException("Worker was terminated."));const e=a.cleanup();t.push(e);a=null}else clearGlobalCaches();i?.(new AbortException("Worker was terminated."));for(const e of n){t.push(e.finished);e.terminate()}return Promise.all(t).then((function(){u.destroy();u=null}))}));u.on("Ready",(function(t){setupDoc(e);e=null}));return h}static initializeFromPort(e){const t=new MessageHandler("worker","main",e);this.setup(t,e);t.send("ready",null)}}globalThis.pdfjsWorker={WorkerMessageHandler};export{WorkerMessageHandler}; \ No newline at end of file diff --git a/src/configs/config-manager.js b/src/configs/config-manager.js index 0861991..16116fb 100644 --- a/src/configs/config-manager.js +++ b/src/configs/config-manager.js @@ -25,6 +25,13 @@ const writeStateFile = (file, data) => { }; let configCache = { at: 0, cfg: null }; const CONFIG_TTL_MS = 2000; +const CONNECTIONS_MIN = 5; +const CONNECTIONS_MAX = 30; +const CONNECTIONS_DEFAULT = 10; +const connectionsOf = (value) => { + const n = Math.round(Number(value)); + return value === undefined || value === null || value === '' || !Number.isFinite(n) ? CONNECTIONS_DEFAULT : Math.min(CONNECTIONS_MAX, Math.max(CONNECTIONS_MIN, n)); +}; const writeConfigFile = (data) => { configCache = { at: 0, cfg: null }; return writeStateFile(configFilePath, data); }; const readJsonFile = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } }; const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v); @@ -160,6 +167,7 @@ const readConfig = () => { const configData = fs.readFileSync(configFilePath); const cfg = JSON.parse(configData); if (!['whole', 'mutuals', 'only-lan', 'local'].includes(cfg.wish)) cfg.wish = 'whole'; + cfg.connections = connectionsOf(cfg.connections); if (!['whole', 'mutuals', 'following'].includes(cfg.pmVisibility)) cfg.pmVisibility = 'whole'; if (!['whole', 'mutuals', 'following'].includes(cfg.mentionsVisibility)) cfg.mentionsVisibility = 'whole'; if (!cfg.phone || typeof cfg.phone !== 'object') cfg.phone = {}; @@ -207,4 +215,8 @@ module.exports = { saveServerConfig, readInviteCodes, defaultInvite, + CONNECTIONS_MIN, + CONNECTIONS_MAX, + CONNECTIONS_DEFAULT, + connectionsOf, }; diff --git a/src/models/activity_model.js b/src/models/activity_model.js index b4770b6..7997b3e 100644 --- a/src/models/activity_model.js +++ b/src/models/activity_model.js @@ -89,7 +89,7 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'courts-key' ]); -module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, larpModel }) => { +module.exports = ({ cooler, cryptoModel, tribesModel, padsModel, industryModel, larpModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }; @@ -121,26 +121,25 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, }; const tryDecryptPublicInviteKey = (invites) => { - if (!tribeCrypto || !Array.isArray(invites)) return null; + if (!cryptoModel || !Array.isArray(invites)) return null; for (const inv of invites) { if (!inv || typeof inv !== 'object' || inv.public !== true) continue; if (typeof inv.code !== 'string') continue; if (typeof inv.ek === 'string') { try { - const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt); + const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k; } catch (_) {} } if (typeof inv.ekChain === 'string') { try { - const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); + const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key; } catch (_) {} } } return null; }; - const hasBlob = async (ssbClient, url) => new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has))); const getMsg = async (ssbClient, key) => new Promise(resolve => ssbClient.get(key, (err, msg) => resolve(err ? null : msg))); const normNL = (s) => String(s || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const stripHtml = (s) => normNL(s) @@ -302,7 +301,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, for (const choice of v.choices) entry.counts[choice] = (entry.counts[choice] || 0) + 1; } } - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); let larpWall = null; if (larpModel && results.some(m => m && m.value && m.value.content && m.value.content.type === 'larpHousePost')) { @@ -330,8 +329,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, if (c.type === 'larpHousePost' && !larpPostVisible(v.author, String(c.house || '').toLowerCase())) continue; if (typeof c.type === 'string' && /e2ee/i.test(c.type)) continue; if (c.type === 'contact') continue; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (cryptoModel && cryptoModel.isSealedMsg(c)) { + const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null; if (!r || !r.body) continue; const inner = r.body; if (inner.k !== 'tribe' || inner.op !== 'create') continue; @@ -341,11 +340,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, } if (!c.type) continue; if (c.type === 'tombstone') continue; - if (c.encryptedPayload && tribeCrypto && !c.tribeId) { + if (c.encryptedPayload && cryptoModel && !c.tribeId) { const pubKey = tryDecryptPublicInviteKey(c.invites); if (pubKey) { try { - const dec = tribeCrypto.decryptContent(c, [[pubKey]]); + const dec = cryptoModel.decryptContent(c, [[pubKey]]); if (dec && !dec._undecryptable) c = dec; } catch (_) {} } @@ -618,6 +617,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, feedOpinionsByRoot.get(r).push({ author: v.author, category: String(c.category || '') }); } + const reportConfirmsByRoot = new Map(); + for (const msg of results) { + const c = msg && msg.value && msg.value.content; + if (!c || c.type !== 'reportConfirm' || typeof c.target !== 'string') continue; + const r = rootOf(c.target); + if (!reportConfirmsByRoot.has(r)) reportConfirmsByRoot.set(r, new Set()); + reportConfirmsByRoot.get(r).add(msg.value.author); + } + const forgedFeedAggByRoot = new Map(); for (const [child, fc] of forgedFeedContent.entries()) { let cur = child; @@ -632,8 +640,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, } const latest = []; - const docUrls = [...new Set([...idToAction.values()].filter(a => a.type === 'document').map(a => (a.content || {}).url))]; - const docBlobOk = new Map(await Promise.all(docUrls.map(async url => [url, await hasBlob(ssbClient, url)]))); for (const a of idToAction.values()) { if (tombstoned.has(a.id)) continue; if (a.type === 'tribe' && parentOf.has(a.id)) continue; @@ -644,7 +650,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, if (c.key && tombstoned.has(c.key)) continue; if (c.branch && tombstoned.has(c.branch)) continue; if (c.target && tombstoned.has(c.target)) continue; - if (a.type === 'document' && !docBlobOk.get(c.url)) continue; if (a.type === 'forum' && c.root) { const rootId = typeof c.root === 'string' ? c.root : (c.root?.key || c.root?.id || ''); const rootAction = idToAction.get(rootId); @@ -696,6 +701,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, } content = { ...content, opinions, opinions_inhabitants: voters }; } + if (a.type === 'report' && reportConfirmsByRoot.has(actionRoot)) { + content = { ...content, confirmations: [...new Set([...(Array.isArray(content.confirmations) ? content.confirmations : []), ...reportConfirmsByRoot.get(actionRoot)])] }; + } latest.push({ ...a, content, ...extra, tipId: idToTipId.get(a.id) || a.id, rootId: actionRoot !== a.id ? actionRoot : null }); } @@ -745,7 +753,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel, padsModel, industryModel, } } else if (textTypes.has(a.type)) { const t = norm(c.text); - const key = t ? `${a.type}:${a.author}:${t}` : null; + const key = t ? `${a.type}:${a.author}:${typeof c.root === 'string' ? c.root : ''}:${t}` : null; const prev = key ? byKey.get(key) : null; if (!key) { byKey.set(`id:${a.id}`, { ...a, __effTs: effTs }); diff --git a/src/models/audios_model.js b/src/models/audios_model.js index cf65107..4f95794 100644 --- a/src/models/audios_model.js +++ b/src/models/audios_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); const { readTyped, memoIndex } = require('./typed_log'); @@ -230,7 +231,7 @@ module.exports = ({ cooler }) => { if (!oldMsg || oldMsg.content?.type !== "audio") throw new Error("Audio not found"); if (oldMsg.content.author !== userId) throw new Error("Not the author"); const aggAudio = await this.getAudioById(id, userId); - if (aggAudio && Object.keys(aggAudio.opinions || {}).some(k => (aggAudio.opinions[k] || 0) > 0)) throw new Error("Cannot edit audio after it has received opinions."); + opinionRules.assertMediaKept(aggAudio, !!blobMarkdown); const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags); const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; diff --git a/src/models/blockchain_model.js b/src/models/blockchain_model.js index b305269..c935b03 100644 --- a/src/models/blockchain_model.js +++ b/src/models/blockchain_model.js @@ -6,7 +6,7 @@ const { readContentWindow, requestScope, logTip } = require('./typed_log'); const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; -module.exports = ({ cooler, tribeCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { @@ -41,9 +41,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return set; }; - const hasBlob = async (ssbClient, url) => - new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has))); - const isReplacedBlock = (vg, tombstoned, block) => { const c = block.content; const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root); @@ -142,7 +139,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const idToBlock = new Map(); const versions = []; - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); for (const msg of results) { const k = msg.key; @@ -157,8 +154,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { } if (!c?.type) continue; if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (cryptoModel && cryptoModel.isSealedMsg(c)) { + const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null; if (!r || !r.body) continue; const inner = r.body; const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); @@ -264,7 +261,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const tombstoned = buildValidatedTombstoneSet(results); const idToBlock = new Map(); const versions = []; - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); for (const msg of results) { @@ -280,8 +277,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { } if (!c?.type) continue; if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (cryptoModel && cryptoModel.isSealedMsg(c)) { + const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null; if (!r || !r.body) continue; const inner = r.body; const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg'); @@ -300,11 +297,6 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const block = idToBlock.get(id); if (!block) return null; - if (block.type === 'document') { - const valid = await hasBlob(ssbClient, block.content.url); - if (!valid) return null; - } - const isTombstoned = tombstoned.has(block.id); const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block); diff --git a/src/models/blog_model.js b/src/models/blog_model.js index c0fa690..568ca7c 100644 --- a/src/models/blog_model.js +++ b/src/models/blog_model.js @@ -3,6 +3,7 @@ const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { readTyped, memoIndex } = require('./typed_log'); const longText = require('../backend/long_text'); +const { isComment, isBlogPost } = require('./comments_model'); const logLimit = getConfig().ssbLogStream?.limit || 1000; const OPINION_TYPE = 'blogOpinion'; @@ -15,9 +16,6 @@ module.exports = ({ cooler, isPublic = false }) => { const getAllMessages = async (ssbClient) => readTyped(ssbClient, BLOG_TYPES, { limit: logLimit }); - const isRootPost = (c) => - !!c && c.type === 'post' && typeof c.text === 'string' && - !c.root && !c.fork && !c.branch && !c.about && !c.target; const buildIndex = (messages) => { const tomb = buildValidatedTombstoneSet(messages); @@ -33,9 +31,9 @@ module.exports = ({ cooler, isPublic = false }) => { if (tomb.has(m.key)) continue; if (c.type === 'post' && typeof c.text === 'string') { - if (isRootPost(c)) { + if (isBlogPost(c)) { posts.set(m.key, { key: m.key, author: v.author, ts: v.timestamp || m.timestamp || 0, c }); - } else if (typeof c.root === 'string') { + } else if (isComment(c)) { commentsByRoot.set(c.root, (commentsByRoot.get(c.root) || 0) + 1); } continue; @@ -142,7 +140,7 @@ module.exports = ({ cooler, isPublic = false }) => { const ssbClient = await openSsb(); const userId = ssbClient.id; const msg = await new Promise((res, rej) => ssbClient.get(id, (err, m) => (err || !m || !m.content) ? rej(new Error('Blog not found')) : res(m))); - if (!isRootPost(msg.content)) throw new Error('Blog not found'); + if (!isBlogPost(msg.content)) throw new Error('Blog not found'); if (msg.author !== userId) throw new Error('Not the author'); const content = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId }; return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); @@ -157,27 +155,6 @@ module.exports = ({ cooler, isPublic = false }) => { if (blog.opinions_inhabitants.includes(userId)) throw new Error('Already opined'); const content = { type: OPINION_TYPE, target: blog.id, category, createdAt: new Date().toISOString() }; return new Promise((res, rej) => ssbClient.publish(content, (err, result) => err ? rej(err) : res(result))); - }, - - async resolveRootId(id) { - const ssbClient = await openSsb(); - const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); - const c = msg && (msg.content || (msg.value && msg.value.content)); - if (c && c.type === 'post' && typeof c.root === 'string') return c.root; - return id; - }, - - async blogHrefFor(id) { - const ssbClient = await openSsb(); - const msg = await new Promise((res) => ssbClient.get(id, (err, m) => res(err ? null : m))); - const c = msg && (msg.content || (msg.value && msg.value.content)); - if (!c || c.type !== 'post' || typeof c.text !== 'string') return null; - if (c.private === true || Array.isArray(c.recps)) return null; - if (typeof c.root === 'string') { - return `/blogs/${encodeURIComponent(c.root)}#${encodeURIComponent(id)}`; - } - if (c.fork || c.branch || c.about || c.target) return null; - return `/blogs/${encodeURIComponent(id)}`; } }; }; diff --git a/src/models/bookmarking_model.js b/src/models/bookmarking_model.js index df6c3ac..bff396b 100644 --- a/src/models/bookmarking_model.js +++ b/src/models/bookmarking_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const moment = require("../server/node_modules/moment"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); @@ -29,6 +30,8 @@ const coerceLastVisit = (lastVisit) => { const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); +const BOOKMARK_TITLE_MAX = 100; + module.exports = ({ cooler }) => { let ssb; @@ -103,6 +106,7 @@ module.exports = ({ cooler }) => { id: node.key, rootId, url: c.url || "", + title: c.title || "", createdAt: c.createdAt || new Date(node.ts).toISOString(), updatedAt: c.updatedAt || null, lastVisit: c.lastVisit || null, @@ -139,7 +143,7 @@ module.exports = ({ cooler }) => { return root; }, - async createBookmark(url, tagsRaw, description, lastVisit) { + async createBookmark(url, tagsRaw, description, lastVisit, title = "") { const ssbClient = await openSsb(); const now = new Date().toISOString(); @@ -150,6 +154,7 @@ module.exports = ({ cooler }) => { type: "bookmark", author: ssbClient.id, url: u, + ...(safeText(title) ? { title: safeText(title).slice(0, BOOKMARK_TITLE_MAX) } : {}), tags: normalizeTags(tagsRaw), description: description || "", createdAt: now, @@ -174,7 +179,7 @@ module.exports = ({ cooler }) => { if (!oldMsg || oldMsg.content?.type !== "bookmark") throw new Error("Bookmark not found"); if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author"); const aggBm = await this.getBookmarkById(id, userId); - if (aggBm && Object.keys(aggBm.opinions || {}).some(k => (aggBm.opinions[k] || 0) > 0)) throw new Error("Cannot edit bookmark after it has received opinions."); + opinionRules.assertMediaKept(aggBm, updatedData.url !== undefined && safeText(updatedData.url) !== safeText(oldMsg.content.url)); const url = safeText(updatedData.url || oldMsg.content.url); if (!url) throw new Error("URL is required"); @@ -185,6 +190,7 @@ module.exports = ({ cooler }) => { ...oldMsg.content, replaces: tipId, url, + title: updatedData.title !== undefined ? safeText(updatedData.title).slice(0, BOOKMARK_TITLE_MAX) : (oldMsg.content.title || ""), tags: updatedData.tags !== undefined ? normalizeTags(updatedData.tags) : safeArr(oldMsg.content.tags), description: updatedData.description !== undefined ? updatedData.description || "" : oldMsg.content.description || "", lastVisit: coerceLastVisit(updatedData.lastVisit || oldMsg.content.lastVisit), diff --git a/src/models/calendars_model.js b/src/models/calendars_model.js index e21f286..83c9204 100644 --- a/src/models/calendars_model.js +++ b/src/models/calendars_model.js @@ -51,31 +51,31 @@ const assertCalendarDates = ({ deadline, date, until }) => { if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline") } -module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => { +module.exports = ({ cooler, pmModel, cryptoModel, calendarCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - const ownCrypto = calendarCrypto || tribeCrypto - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const ownCrypto = calendarCrypto || cryptoModel + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a - return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + return (cryptoModel && cryptoModel.getKeys(rid)) || [] } - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0) const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } const rotateCalendarKey = async (rootId, remainingMembers) => { - if (!ownCrypto || !tribeCrypto || !rootId) return + if (!ownCrypto || !cryptoModel || !rootId) return const existing = lookupKey(rootId) if (!existing) return - const newKey = ownCrypto.generateTribeKey() + const newKey = ownCrypto.generateKey() const newGen = ownCrypto.addNewKey(rootId, newKey) const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -115,7 +115,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const readAll = async (ssbClient) => unwrapForIndex(await readTyped(ssbClient, CALENDAR_TYPES, { limit: logLimit, withWindow: true })) const ensureMemberKeys = async (ssbClient, messages, cals) => { - if (!tribeCrypto || !ownCrypto) return + if (!cryptoModel || !ownCrypto) return const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content @@ -138,7 +138,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const key = lookupKey(rootId) const memberKeys = {} for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue await new Promise((resolve) => { @@ -147,50 +147,50 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) } } - const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null + const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, CALENDAR_TYPES) : msgs - const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c - const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c + const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c + const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {} const encryptStandalone = (content, rootId) => { - if (!tribeCrypto || !rootId) return content + if (!cryptoModel || !rootId) return content const key = lookupKey(rootId) if (!key) return content - return tribeCrypto.encryptContent(content, [key], true) + return cryptoModel.encryptContent(content, [key], true) } const decryptScoped = async (content, rootId) => { if (!content) return content - if (content.tribeId || (tribeCrypto && tribeCrypto.isTribeMsg(content))) return await decryptIfTribe(content) + if (content.tribeId || (cryptoModel && cryptoModel.isSealedMsg(content))) return await decryptIfGroup(content) return decryptCalendarRoot(content, rootId) } const decryptCalendarRoot = (content, rootId) => { if (!content || !content.encryptedPayload) return content - if (!tribeCrypto) return content + if (!cryptoModel) return content const keys = lookupKeys(rootId) if (!keys || !keys.length) return { ...content, _undecryptable: true } - return tribeCrypto.decryptContent(content, keys.map(k => [k])) + return cryptoModel.decryptContent(content, keys.map(k => [k])) } const tryDecryptPublicInviteKey = (invites) => { - if (!tribeCrypto || !Array.isArray(invites)) return null + if (!cryptoModel || !Array.isArray(invites)) return null for (const inv of invites) { if (!inv || typeof inv !== "object" || inv.public !== true) continue if (typeof inv.code !== "string") continue if (typeof inv.ek === "string") { - try { const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k } catch (_) {} + try { const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k } catch (_) {} } if (typeof inv.ekChain === "string") { - try { const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key } catch (_) {} + try { const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key } catch (_) {} } } return null } const decryptIndexNodes = async (idx) => { - if (!tribeCrypto) return + if (!cryptoModel) return for (const [k, n] of idx.nodes.entries()) { if (!n.c || !n.c.encryptedPayload) continue let root = k @@ -198,7 +198,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) let dec = null if (n.c.tribeId && tribesModel) { try { - const r = await tribeCrypto.decryptFromTribe(n.c, tribesModel) + const r = await cryptoModel.decryptFromGroup(n.c, tribesModel) if (r && !r._undecryptable) dec = r } catch (_) {} } @@ -209,7 +209,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (!dec) { const pubKey = tryDecryptPublicInviteKey(n.c.invites) if (pubKey) { - try { const r = tribeCrypto.decryptContent(n.c, [[pubKey]]); if (r && !r._undecryptable) dec = r } catch (_) {} + try { const r = cryptoModel.decryptContent(n.c, [[pubKey]]); if (r && !r._undecryptable) dec = r } catch (_) {} } } if (dec) { @@ -438,10 +438,10 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) let calKey = null let content = plainContent if (tribeId) { - content = await encryptIfTribe(plainContent) - } else if (tribeCrypto) { - calKey = ownCrypto.generateTribeKey() - content = tribeCrypto.encryptContent(plainContent, [calKey], true) + content = await encryptIfGroup(plainContent) + } else if (cryptoModel) { + calKey = ownCrypto.generateKey() + content = cryptoModel.encryptContent(plainContent, [calKey], true) } const calMsg = await new Promise((resolve, reject) => { @@ -450,11 +450,11 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const calendarId = calMsg.key - if (calKey && tribeCrypto) { + if (calKey && cryptoModel) { ownCrypto.setKey(calendarId, calKey, 1) try { const ssbKeys = require("../server/node_modules/ssb-keys") - const boxedKey = tribeCrypto.boxKeyForMember(calKey, userId, ssbKeys) + const boxedKey = cryptoModel.boxKeyForMember(calKey, userId, ssbKeys) await new Promise((resolve) => { ssbClient.publish({ type: "tribe-keys", tribeId: calendarId, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve()) }) @@ -462,8 +462,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (validStatus === "OPEN") { try { const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex") - const inviteSalt = tribeCrypto.generateInviteSalt() - const ek = tribeCrypto.encryptForInvite(calKey, pubCode, inviteSalt) + const inviteSalt = cryptoModel.generateInviteSalt() + const ek = cryptoModel.encryptForInvite(calKey, pubCode, inviteSalt) const tipId = await this.resolveCurrentId(calendarId) const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it))) const dec = decryptCalendarRoot(item.content, calendarId) @@ -501,8 +501,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}), ...(tribeId ? { tribeId } : {}) } - if (tribeId) dateContent = await encryptIfTribe(dateContent) - else if (calKey) dateContent = tribeCrypto.encryptContent(dateContent, [calKey], true) + if (tribeId) dateContent = await encryptIfGroup(dateContent) + else if (calKey) dateContent = cryptoModel.encryptContent(dateContent, [calKey], true) const dateMsg = await new Promise((resolve, reject) => { ssbClient.publish(dateContent, (err, msg) => err ? reject(err) : resolve(msg)) }) @@ -517,8 +517,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) createdAt: new Date().toISOString(), ...(tribeId ? { tribeId } : {}) } - if (tribeId) noteContent = await encryptIfTribe(noteContent) - else if (calKey) noteContent = tribeCrypto.encryptContent(noteContent, [calKey], true) + if (tribeId) noteContent = await encryptIfGroup(noteContent) + else if (calKey) noteContent = cryptoModel.encryptContent(noteContent, [calKey], true) await new Promise((resolve, reject) => { ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg)) }) @@ -561,7 +561,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) updated.invites = updated.invites.filter(inv => !(inv && typeof inv === "object" && inv.public === true)) try { await rotateCalendarKey(rootId, (updated.participants || []).filter(p => p !== userId)) } catch (_) {} } - if (updated.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfGroup(updated) else updated = encryptStandalone(updated, rootId) const result = await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -591,7 +591,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (!cal) throw new Error("Calendar not found") const participants = Array.isArray(cal.participants) ? cal.participants : [] if (participants.includes(userId)) return - if (tribeCrypto && Array.isArray(cal.invites)) { + if (cryptoModel && Array.isArray(cal.invites)) { const pub = cal.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain)) if (pub) return await this.joinByInvite(pub.code) } @@ -693,7 +693,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) ...(ruleDeadline ? { intervalDeadline: ruleDeadline } : {}), ...(cal.tribeId ? { tribeId: cal.tribeId } : {}) } - if (cal.tribeId) dateContent = await encryptIfTribe(dateContent) + if (cal.tribeId) dateContent = await encryptIfGroup(dateContent) else dateContent = encryptStandalone(dateContent, rootId) const msg = await new Promise((resolve, reject) => { ssbClient.publish(dateContent, (err, m) => err ? reject(err) : resolve(m)) @@ -728,15 +728,15 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (c.calendarId !== rootId) continue if (!members.has(v.author)) continue let dec = c - if (c.encryptedPayload && tribeCrypto) { + if (c.encryptedPayload && cryptoModel) { if (c.tribeId && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + const r = await cryptoModel.decryptFromGroup(c, tribesModel) dec = r && !r._undecryptable ? r : c if (r && r._undecryptable) continue } else { const keys = [...(lookupKeys(c.calendarId) || []), ...(pubKey ? [pubKey] : [])] if (!keys.length) continue - const r = tribeCrypto.decryptContent(c, keys.map(k => [k])) + const r = cryptoModel.decryptContent(c, keys.map(k => [k])) if (!r || r._undecryptable) continue dec = r } @@ -789,8 +789,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (!c || c.type !== "calendarDate") continue if (tombstoned.has(m.key)) break let dec = c - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + if (c.encryptedPayload && cryptoModel && tribesModel) { + const r = await cryptoModel.decryptFromGroup(c, tribesModel) if (r && !r._undecryptable) dec = r } dateAuthor = dec.author || (m.value || {}).author @@ -828,7 +828,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) createdAt: new Date().toISOString(), ...(cal.tribeId ? { tribeId: cal.tribeId } : {}) } - if (cal.tribeId) noteContent = await encryptIfTribe(noteContent) + if (cal.tribeId) noteContent = await encryptIfGroup(noteContent) else noteContent = encryptStandalone(noteContent, rootId) return new Promise((resolve, reject) => { ssbClient.publish(noteContent, (err, msg) => err ? reject(err) : resolve(msg)) @@ -871,12 +871,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) let dec = c if (c.encryptedPayload) { let r = await decryptScoped(c, rootId) - if ((!r || r._undecryptable) && !c.tribeId && tribeCrypto) { + if ((!r || r._undecryptable) && !c.tribeId && cryptoModel) { if (pubKey === undefined) { const cal = await this.getCalendarById(rootId).catch(() => null) pubKey = cal ? tryDecryptPublicInviteKey(cal.invites) : null } - if (pubKey) r = tribeCrypto.decryptContent(c, [[pubKey]]) + if (pubKey) r = cryptoModel.decryptContent(c, [[pubKey]]) } if (r && !r._undecryptable) dec = r else continue @@ -924,8 +924,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const dated = messages.filter(m => !tombstoned.has(m.key) && ((m.value || {}).content || {}).type === "calendarDate") const decoded = await Promise.all(dated.map(async (m) => { const c = m.value.content - if (!(c.encryptedPayload && tribeCrypto && tribesModel)) return c - const r = await tribeCrypto.decryptFromTribe(c, tribesModel) + if (!(c.encryptedPayload && cryptoModel && tribesModel)) return c + const r = await cryptoModel.decryptFromGroup(c, tribesModel) return !r || r._undecryptable ? null : r })) for (let i = 0; i < dated.length; i++) { @@ -1013,12 +1013,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex") let invite = code const calKey = !cal.tribeId ? lookupKey(cal.rootId) : null - if (tribeCrypto && calKey) { - const inviteSalt = tribeCrypto.generateInviteSalt() - const ek = tribeCrypto.encryptForInvite(calKey, code, inviteSalt) + if (cryptoModel && calKey) { + const inviteSalt = cryptoModel.generateInviteSalt() + const ek = cryptoModel.encryptForInvite(calKey, code, inviteSalt) invite = opts.public ? { code, ek, salt: inviteSalt, gen: lookupGen(cal.rootId), public: true } - : { ch: tribeCrypto.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) } + : { ch: cryptoModel.hashInviteCode(code, inviteSalt), ek, salt: inviteSalt, gen: lookupGen(cal.rootId) } } if (opts.public && typeof invite !== "object") invite = { code, public: true } const tipId = await this.resolveCurrentId(calendarId) @@ -1041,7 +1041,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) updatedAt: new Date().toISOString(), replaces: tipId } - if (updated.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfGroup(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -1087,7 +1087,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) updatedAt: new Date().toISOString(), replaces: tipId } - if (updated.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfGroup(updated) else updated = encryptStandalone(updated, cal.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } @@ -1105,15 +1105,15 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) const invs = Array.isArray(cal.invites) ? cal.invites : [] for (const inv of invs) { if (typeof inv === "string" && inv === code) { matched = cal; matchedInvite = inv; break } - if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && tribeCrypto && inv.ch === tribeCrypto.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break } + if (typeof inv === "object" && inv && (inv.code === code || (inv.ch && cryptoModel && inv.ch === cryptoModel.hashInviteCode(code, inv.salt)))) { matched = cal; matchedInvite = inv; break } } if (matched) break } if (!matched) throw new Error("Invalid or expired invite code") if (matched.participants.includes(userId)) throw new Error("Already a participant") - if (typeof matchedInvite === "object" && matchedInvite.ch && tribeCrypto && matchedInvite.ek) { + if (typeof matchedInvite === "object" && matchedInvite.ch && cryptoModel && matchedInvite.ek) { let probeKey = null - try { probeKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {} + try { probeKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } catch (_) {} const messages = await readAll(await openSsb()) const spent = messages.some(m => { const c = m.value && m.value.content @@ -1122,21 +1122,21 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) if (spent) throw new Error("Invite already used") } let calKey = null - if (tribeCrypto && typeof matchedInvite === "object") { + if (cryptoModel && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + const chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) if (Array.isArray(chain) && chain.length) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) + cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) + cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1) } } calKey = chain[0].key } } else if (matchedInvite.ek) { - calKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) + calKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) ownCrypto.setKey(matched.rootId, calKey, matchedInvite.gen || 1) } } @@ -1166,18 +1166,18 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) updatedAt: new Date().toISOString(), replaces: tipId } - if (updated.tribeId) updated = await encryptIfTribe(updated) + if (updated.tribeId) updated = await encryptIfGroup(updated) else updated = encryptStandalone(updated, matched.rootId) await new Promise((resolve, reject) => ssbClient.publish(updated, (e, res) => e ? reject(e) : resolve(res))) const tombstone = { type: "tombstone", target: tipId, deletedAt: new Date().toISOString(), author: userId } await new Promise((resolve, reject) => ssbClient.publish(tombstone, e => e ? reject(e) : resolve())) - if (tribeCrypto && calKey) { + if (cryptoModel && calKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(calKey, userId, ssbKeys) } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(calKey, userId, ssbKeys) } catch (_) {} if (matched.author && matched.author !== userId) { - try { memberKeys[matched.author] = tribeCrypto.boxKeyForMember(calKey, matched.author, ssbKeys) } catch (_) {} + try { memberKeys[matched.author] = cryptoModel.boxKeyForMember(calKey, matched.author, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { diff --git a/src/models/chats_model.js b/src/models/chats_model.js index 5f60594..61fba40 100644 --- a/src/models/chats_model.js +++ b/src/models/chats_model.js @@ -21,7 +21,7 @@ const REACTION_EMOJIS = ["up", "heart", "laugh", "down"] const DEFAULT_MESSAGES_PER_HOUR = 60 -module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, chatCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } @@ -35,9 +35,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } })() - const ownCrypto = chatCrypto || tribeCrypto - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null - const hashCode = (code, salt) => tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null + const ownCrypto = chatCrypto || cryptoModel + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null + const hashCode = (code, salt) => cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null const inviteMatches = (inv, token) => { if (!token) return false if (typeof inv === "string") return inv === token @@ -51,21 +51,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a - return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + return (cryptoModel && cryptoModel.getKeys(rid)) || [] } - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0) const rotateChatKey = async (rootId, remainingMembers) => { - if (!ownCrypto || !tribeCrypto || !rootId) return + if (!ownCrypto || !cryptoModel || !rootId) return const existing = lookupKey(rootId) if (!existing) return - const newKey = ownCrypto.generateTribeKey() + const newKey = ownCrypto.generateKey() const newGen = ownCrypto.addNewKey(rootId, newKey) const ssbClient = await openSsb() const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -104,10 +104,10 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } const getTribeKeysFor = async (tribeId) => { - if (!tribeCrypto || !tribesModel || !tribeId) return [] + if (!cryptoModel || !tribesModel || !tribeId) return [] try { const rootId = await tribesModel.getRootId(tribeId) - return tribeCrypto.getKeys(rootId) || [] + return cryptoModel.getKeys(rootId) || [] } catch (_) { return [] } } @@ -258,25 +258,25 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } const resolveKeyChainSets = (chatRootId) => { - if (!tribeCrypto) return [] + if (!cryptoModel) return [] const keys = lookupKeys(chatRootId) return keys.map(k => [k]) } const tryDecryptPublicInviteKey = (invites) => { - if (!tribeCrypto || !Array.isArray(invites)) return null + if (!cryptoModel || !Array.isArray(invites)) return null for (const inv of invites) { if (!inv || typeof inv !== "object" || inv.public !== true) continue if (typeof inv.code !== "string") continue if (typeof inv.ek === "string") { try { - const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt) + const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt) if (k) return k } catch (_) {} } if (typeof inv.ekChain === "string") { try { - const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt) + const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt) if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key } catch (_) {} } @@ -287,14 +287,14 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const decryptChatContent = (rawC, rootId) => { let c = rawC let undecryptable = false - if (tribeCrypto && c.encryptedPayload) { + if (cryptoModel && c.encryptedPayload) { const keyChainSets = resolveKeyChainSets(rootId) - c = tribeCrypto.decryptContent(c, keyChainSets) + c = cryptoModel.decryptContent(c, keyChainSets) undecryptable = !!c._undecryptable if (undecryptable) { const pubKey = tryDecryptPublicInviteKey(rawC.invites) if (pubKey) { - const retry = tribeCrypto.decryptContent(rawC, [[pubKey]]) + const retry = cryptoModel.decryptContent(rawC, [[pubKey]]) if (retry && !retry._undecryptable) { c = retry undecryptable = false @@ -369,13 +369,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let text = c.text || "" let image = c.image || null let mimeType = typeof c.mimeType === "string" ? c.mimeType : null - if (tribeCrypto && c.encryptedText) { + if (cryptoModel && c.encryptedText) { const candidateKeys = [...tribeKeys, ...lookupKeys(chatRootId)] for (const keyHex of candidateKeys) { try { - text = tribeCrypto.decryptWithKey(c.encryptedText, keyHex) - if (c.encryptedImage) image = tribeCrypto.decryptWithKey(c.encryptedImage, keyHex) - if (c.encryptedMime) mimeType = tribeCrypto.decryptWithKey(c.encryptedMime, keyHex) + text = cryptoModel.decryptWithKey(c.encryptedText, keyHex) + if (c.encryptedImage) image = cryptoModel.decryptWithKey(c.encryptedImage, keyHex) + if (c.encryptedMime) mimeType = cryptoModel.decryptWithKey(c.encryptedMime, keyHex) break } catch (_) {} } @@ -400,7 +400,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { }) const ensureMemberKeys = async (ssbClient, messages, chats) => { - if (!tribeCrypto || !ownCrypto) return + if (!cryptoModel || !ownCrypto) return const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content @@ -421,7 +421,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const key = lookupKey(rootId) const memberKeys = {} for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue await new Promise((resolve) => { @@ -440,11 +440,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { }) const decryptTribeChats = async (idx) => { - if (!tribeCrypto || !tribesModel || typeof tribeCrypto.decryptFromTribe !== "function") return idx + if (!cryptoModel || !tribesModel || typeof cryptoModel.decryptFromGroup !== "function") return idx for (const [k, n] of idx.nodes) { const c = n.c || {} if (!c.encryptedPayload || !c.tribeId) continue - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel).catch(() => null) + const dec = await cryptoModel.decryptFromGroup(c, tribesModel).catch(() => null) if (dec && !dec._undecryptable) idx.nodes.set(k, { ...n, c: dec }) } return idx @@ -466,7 +466,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { type: "chat", async encryptionKeyFor(chatRootId, tribeId = null) { - if (!tribeCrypto) return null + if (!cryptoModel) return null if (tribeId) { const k = await getTribeFirstKeyFor(tribeId) if (k) return k @@ -517,7 +517,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { ...(extra && typeof extra === "object" && typeof extra.courseId === "string" && extra.courseId ? { courseId: extra.courseId } : {}) } - if (!tribeCrypto) { + if (!cryptoModel) { return new Promise((resolve, reject) => { ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg)) }) @@ -528,11 +528,11 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { const ancestryIds = await tribesModel.getAncestryChain(tribeId) const chain = [] for (const rid of ancestryIds || []) { - const k = tribeCrypto.getKey(rid) + const k = cryptoModel.getKey(rid) if (!k) { chain.length = 0; break } chain.push(k) } - if (chain.length) content = tribeCrypto.encryptContent(content, chain, true) + if (chain.length) content = cryptoModel.encryptContent(content, chain, true) } catch (_) {} return new Promise((resolve, reject) => { ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg)) @@ -545,15 +545,15 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { }) } - const chatKey = ownCrypto.generateTribeKey() - content = tribeCrypto.encryptContent(content, [chatKey], true) + const chatKey = ownCrypto.generateKey() + content = cryptoModel.encryptContent(content, [chatKey], true) const result = await new Promise((resolve, reject) => { ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg)) }) ownCrypto.setKey(result.key, chatKey, 1) try { const ssbKeys = require("../server/node_modules/ssb-keys") - const boxedKey = tribeCrypto.boxKeyForMember(chatKey, userId, ssbKeys) + const boxedKey = cryptoModel.boxKeyForMember(chatKey, userId, ssbKeys) await new Promise((resolve) => { ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve()) }) @@ -600,21 +600,21 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { ...(chat.tribeId ? { tribeId: chat.tribeId } : {}) } - if (tribeCrypto) { + if (cryptoModel) { if (chat.tribeId) { try { const ancestryIds = await tribesModel.getAncestryChain(chat.tribeId) const chain = [] for (const rid of ancestryIds || []) { - const k = tribeCrypto.getKey(rid) + const k = cryptoModel.getKey(rid) if (!k) { chain.length = 0; break } chain.push(k) } - if (chain.length) updated = tribeCrypto.encryptContent(updated, chain, true) + if (chain.length) updated = cryptoModel.encryptContent(updated, chain, true) } catch (_) {} } else { const chatKey = lookupKey(rootId) - if (chatKey) updated = tribeCrypto.encryptContent(updated, [chatKey], true) + if (chatKey) updated = cryptoModel.encryptContent(updated, [chatKey], true) } } @@ -747,16 +747,16 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let invite = code const pubFlag = opts.public ? { public: true } : {} - if (tribeCrypto) { - const inviteSalt = tribeCrypto.generateInviteSalt() - const ekChain = tribeCrypto.encryptChainForInvite([chat.rootId], code, inviteSalt) + if (cryptoModel) { + const inviteSalt = cryptoModel.generateInviteSalt() + const ekChain = cryptoModel.encryptChainForInvite([chat.rootId], code, inviteSalt) const ident = opts.public ? { code, ...pubFlag } : { ch: hashCode(code, inviteSalt) } if (ekChain) { invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(chat.rootId) } } else { const chatKey = lookupKey(chat.rootId) if (chatKey) { - const ek = tribeCrypto.encryptForInvite(chatKey, code, inviteSalt) + const ek = cryptoModel.encryptForInvite(chatKey, code, inviteSalt) invite = { ...ident, ek, salt: inviteSalt, gen: lookupGen(chat.rootId) } } else { invite = { ...ident, salt: inviteSalt } @@ -823,13 +823,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { let chatKey = null let chain = null - if (tribeCrypto && typeof matchedInvite === "object") { + if (cryptoModel && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) + chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3) if (Array.isArray(chain) && chain.length) chatKey = chain[0].key else chain = null } else if (matchedInvite.ek) { - chatKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) + chatKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt) } } @@ -843,9 +843,9 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (chain) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) + cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length) } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1) + cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1) } } } else if (chatKey) { @@ -854,13 +854,13 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { await publishMemberToggle(ssbClient, matchedChat.rootId, userId, true, isPublic ? undefined : token) - if (tribeCrypto && chatKey) { + if (cryptoModel && chatKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(chatKey, userId, ssbKeys) } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(chatKey, userId, ssbKeys) } catch (_) {} if (matchedChat.author && matchedChat.author !== userId) { - try { memberKeys[matchedChat.author] = tribeCrypto.boxKeyForMember(chatKey, matchedChat.author, ssbKeys) } catch (_) {} + try { memberKeys[matchedChat.author] = cryptoModel.boxKeyForMember(chatKey, matchedChat.author, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -881,7 +881,7 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { if (chat.status === "CLOSED") throw new Error("Chat is closed") if (chat.members.includes(userId)) return chat.key - if (tribeCrypto && Array.isArray(chat.invites)) { + if (cryptoModel && Array.isArray(chat.invites)) { const pub = chat.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain)) if (pub) return await this.joinByInvite(pub.code) } @@ -960,17 +960,17 @@ module.exports = ({ cooler, tribeCrypto, chatCrypto, tribesModel }) => { } const chatIsEncrypted = !!(chat.tribeId) || !!lookupKey(chat.rootId) - if (chatIsEncrypted && tribeCrypto) { + if (chatIsEncrypted && cryptoModel) { let encKey = null if (chat.tribeId) encKey = await getTribeFirstKeyFor(chat.tribeId) if (!encKey) encKey = lookupKey(chat.rootId) if (!encKey) throw new Error(`Missing chat key for ${chat.rootId} โ€” cannot send message`) - content.encryptedText = tribeCrypto.encryptWithKey(safeText(text), encKey) + content.encryptedText = cryptoModel.encryptWithKey(safeText(text), encKey) if (content.image) { - content.encryptedImage = tribeCrypto.encryptWithKey(String(content.image), encKey) + content.encryptedImage = cryptoModel.encryptWithKey(String(content.image), encKey) delete content.image if (content.mimeType) { - content.encryptedMime = tribeCrypto.encryptWithKey(String(content.mimeType), encKey) + content.encryptedMime = cryptoModel.encryptWithKey(String(content.mimeType), encKey) delete content.mimeType } } diff --git a/src/models/comments_model.js b/src/models/comments_model.js new file mode 100644 index 0000000..5fe2184 --- /dev/null +++ b/src/models/comments_model.js @@ -0,0 +1,111 @@ +const { readTyped } = require('./typed_log'); +const { buildValidatedTombstoneSet } = require('./tombstone_validator'); +const { getConfig } = require('../configs/config-manager.js'); + +const MSG_ID_RE = /^%[A-Za-z0-9+/]{43}=?\.sha256$/; +const ROOT_CACHE_MAX = 2000; + +const isMsgId = (v) => typeof v === 'string' && MSG_ID_RE.test(v); +const isComment = (c) => !!c && typeof c === 'object' && c.type === 'post' && isMsgId(c.root) && !c.fork; +const isBlogPost = (c) => !!c && typeof c === 'object' && c.type === 'post' && typeof c.text === 'string' && !c.root && !c.fork && !c.branch && !c.about && !c.target; +const contentOf = (m) => (m && m.value && m.value.content) || (m && m.content) || null; + +const commentsModel = ({ cooler, post }) => { + let ssb; + const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; + const roots = new Map(); + + const getMsg = async (id) => { + const client = await openSsb(); + return new Promise((res) => client.get(id, (err, m) => res(err ? null : m))); + }; + + const remember = (id, type) => { + if (roots.size >= ROOT_CACHE_MAX) roots.delete(roots.keys().next().value); + roots.set(id, type); + }; + + const rootTypeOf = async (rootId) => { + if (!isMsgId(rootId)) return null; + if (roots.has(rootId)) return roots.get(rootId); + const c = contentOf(await getMsg(rootId).catch(() => null)); + const type = !c || typeof c !== 'object' || typeof c.type !== 'string' ? null : (c.type === 'post' && !isBlogPost(c) ? null : c.type); + if (type) remember(rootId, type); + return type; + }; + + const annotate = async (items) => { + const list = Array.isArray(items) ? items : []; + const wanted = new Set(); + for (const it of list) { const c = contentOf(it); if (isComment(c)) wanted.add(c.root); } + const types = new Map(); + await Promise.all([...wanted].map(async (id) => types.set(id, await rootTypeOf(id).catch(() => null)))); + for (const it of list) { + const c = contentOf(it); + if (!isComment(c)) continue; + it.comment = { rootId: c.root, rootType: types.get(c.root) || null }; + } + return list; + }; + + const locate = async (id) => { + if (!isMsgId(id)) return null; + const msg = await getMsg(id).catch(() => null); + const c = contentOf(msg); + if (!c || typeof c !== 'object' || typeof c.type !== 'string') return null; + if (isComment(c)) return { commentId: id, rootId: c.root, rootType: await rootTypeOf(c.root).catch(() => null) }; + if (c.type === 'post') return isBlogPost(c) ? { rootId: id, rootType: 'post' } : null; + remember(id, c.type); + return { rootId: id, rootType: c.type, author: msg.author, content: c }; + }; + + const listFor = async (rootId) => { + if (!isMsgId(rootId)) return []; + const raw = await post.topicComments(rootId); + return (raw || []).filter(m => { const c = contentOf(m); return isComment(c) && c.root === rootId; }) + .sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0)); + }; + + const listForMany = async (ids) => { + const want = new Set((ids || []).filter(isMsgId)); + const out = new Map(); + if (!want.size) return out; + const client = await openSsb(); + const msgs = await readTyped(client, ['post', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); + const gone = buildValidatedTombstoneSet(msgs || []); + for (const m of msgs || []) { + const c = contentOf(m); + if (!isComment(c) || gone.has(m.key) || !want.has(c.root)) continue; + if (!out.has(c.root)) out.set(c.root, []); + out.get(c.root).push(m); + } + for (const list of out.values()) list.sort((a, b) => (a?.value?.timestamp || 0) - (b?.value?.timestamp || 0)); + return out; + }; + + const countsFor = async (ids) => { + const want = new Set((ids || []).filter(isMsgId)); + const counts = new Map(); + if (!want.size) return counts; + const client = await openSsb(); + const msgs = await readTyped(client, ['post', 'tombstone'], { limit: getConfig().ssbLogStream?.limit || 1000 }); + const gone = buildValidatedTombstoneSet(msgs || []); + for (const m of msgs || []) { + const c = contentOf(m); + if (!isComment(c) || gone.has(m.key) || !want.has(c.root)) continue; + counts.set(c.root, (counts.get(c.root) || 0) + 1); + } + return counts; + }; + + const publish = async ({ rootId, text }) => { + if (!isMsgId(rootId)) throw new Error('Invalid root'); + return post.publish({ text, root: rootId, dest: rootId }); + }; + + return { isComment, isBlogPost, rootTypeOf, annotate, locate, listFor, listForMany, countsFor, publish }; +}; + +module.exports = commentsModel; +module.exports.isComment = isComment; +module.exports.isBlogPost = isBlogPost; diff --git a/src/models/courts_model.js b/src/models/courts_model.js index 91f946c..6a542f6 100644 --- a/src/models/courts_model.js +++ b/src/models/courts_model.js @@ -27,7 +27,7 @@ const COURTS_TYPES = [ ]; const VOTE_LOG_TYPES = ['votes', 'votesVote', 'tombstone']; -module.exports = ({ cooler, services = {}, tribeCrypto }) => { +module.exports = ({ cooler, services = {}, cryptoModel }) => { let ssb; let userId; @@ -47,7 +47,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { for (const f of fields) { if (content[f] !== undefined) payload[f] = content[f]; } - const enc = tribeCrypto.encryptWithKey(JSON.stringify(payload), keyHex); + const enc = cryptoModel.encryptWithKey(JSON.stringify(payload), keyHex); const result = { ...content }; for (const f of fields) delete result[f]; result.encryptedPayload = enc; @@ -57,7 +57,7 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { const decryptFields = (content, keyHex) => { if (!content || !content.encryptedPayload) return content; try { - const plain = tribeCrypto.decryptWithKey(content.encryptedPayload, keyHex); + const plain = cryptoModel.decryptWithKey(content.encryptedPayload, keyHex); const payload = JSON.parse(plain); const result = { ...content }; delete result.encryptedPayload; @@ -68,15 +68,15 @@ module.exports = ({ cooler, services = {}, tribeCrypto }) => { }; const getCaseKey = (obj) => { - if (!tribeCrypto || !obj) return null; - return tribeCrypto.getKey(obj.rootCaseId || obj.id); + if (!cryptoModel || !obj) return null; + return cryptoModel.getKey(obj.rootCaseId || obj.id); }; const distributeKey = async (caseKey, caseRootId, recipientId) => { - if (!tribeCrypto || !recipientId) return; + if (!cryptoModel || !recipientId) return; const ssbClient = await openSsb(); const ssbKeys = require('../server/node_modules/ssb-keys'); - const boxed = tribeCrypto.boxKeyForMember(caseKey, recipientId, ssbKeys); + const boxed = cryptoModel.boxKeyForMember(caseKey, recipientId, ssbKeys); const content = { type: 'courts-key', caseRootId, for: recipientId, memberKey: boxed }; await new Promise((res, rej) => ssbClient.publish(content, (e) => e ? rej(e) : res())); }; @@ -182,15 +182,15 @@ if (c.type === type) { createdAt: openedAt }; - if (tribeCrypto) { + if (cryptoModel) { const stub = { ...content }; for (const f of CASE_FIELDS) delete stub[f]; const initialMsg = await new Promise((res, rej) => ssbClient.publish(stub, (err, msg) => (err ? rej(err) : res(msg))) ); const caseRootId = initialMsg.key; - const caseKey = tribeCrypto.generateTribeKey(); - tribeCrypto.setKey(caseRootId, caseKey, 1); + const caseKey = cryptoModel.generateKey(); + cryptoModel.setKey(caseRootId, caseKey, 1); const encrypted = encryptFields({ ...content, rootCaseId: caseRootId }, caseKey, CASE_FIELDS); const update = { ...encrypted, replaces: caseRootId, updatedAt: openedAt }; const finalMsg = await new Promise((res, rej) => @@ -317,7 +317,7 @@ if (c.type === type) { const node = nodes.get(tip); if (!node) return null; let c = { id: tip, ...node.c }; - if (tribeCrypto) { const key = getCaseKey(c); if (key) c = decryptFields(c, key); } + if (cryptoModel) { const key = getCaseKey(c); if (key) c = decryptFields(c, key); } if (!c.rootCaseId) c.rootCaseId = root; return c; } @@ -344,7 +344,7 @@ if (c.type === type) { const author = String(m.value?.author || ''); const ts = m.value?.timestamp || m.timestamp || 0; if (c.type === 'courtsMediators' && matchC(c)) { - let dec = c; if (tribeCrypto && caseKey) dec = decryptFields(c, caseKey); + let dec = c; if (cryptoModel && caseKey) dec = decryptFields(c, caseKey); const list = ensureArray(dec.mediators || []).map(x => String(x || '').trim()).filter(Boolean).filter(x => x !== accuser && x !== respondentId); if (c.side === 'accuser' && author === accuser && ts >= medATs) { medA = list; medATs = ts; } else if (c.side === 'respondent' && author === respondentId && ts >= medRTs) { medR = list; medRTs = ts; } @@ -454,7 +454,7 @@ if (c.type === type) { ); const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId); const caseRootId = c.rootCaseId || c.id; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) { for (const mediatorId of clean) { @@ -464,7 +464,7 @@ if (c.type === type) { } const ssbClient = await openSsb(); let content = { type: 'courtsMediators', caseId: caseRootId, side, mediators: clean, author: userId, createdAt: nowISO() }; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) content = encryptFields(content, caseKey, MEDIATORS_FIELDS); } @@ -488,7 +488,7 @@ if (c.type === type) { throw new Error('Judge cannot be a party of the case.'); } const caseRootId = c.rootCaseId || c.id; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) { await distributeKey(caseKey, caseRootId, id); @@ -525,7 +525,7 @@ if (c.type === type) { imageUrl, createdAt: nowISO() }; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) content = encryptFields(content, caseKey, EVIDENCE_FIELDS); } @@ -552,7 +552,7 @@ if (c.type === type) { text: t, createdAt: nowISO() }; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) content = encryptFields(content, caseKey, ANSWER_FIELDS); } @@ -592,7 +592,7 @@ if (c.type === type) { orders: o, createdAt: nowISO() }; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) content = encryptFields(content, caseKey, VERDICT_FIELDS); } @@ -620,7 +620,7 @@ if (c.type === type) { terms: t, createdAt: nowISO() }; - if (tribeCrypto) { + if (cryptoModel) { const caseKey = getCaseKey(c); if (caseKey) content = encryptFields(content, caseKey, SETTLEMENT_FIELDS); } @@ -972,7 +972,7 @@ if (c.type === type) { } async function processIncomingCourtsKeys() { - if (!tribeCrypto) return; + if (!cryptoModel) return; const ssbKeys = require('../server/node_modules/ssb-keys'); const ssbConfig = require('../server/ssb_config'); const ssbClient = await openSsb(); @@ -982,10 +982,10 @@ if (c.type === type) { if (!c || c.type !== 'courts-key') continue; if (c.for !== ssbClient.id) continue; if (!c.memberKey || !c.caseRootId) continue; - if (tribeCrypto.getKey(c.caseRootId)) continue; + if (cryptoModel.getKey(c.caseRootId)) continue; try { - const key = tribeCrypto.unboxKeyFromMember(c.memberKey, ssbConfig.keys, ssbKeys); - if (key) tribeCrypto.setKey(c.caseRootId, key, 1); + const key = cryptoModel.unboxKeyFromMember(c.memberKey, ssbConfig.keys, ssbKeys); + if (key) cryptoModel.setKey(c.caseRootId, key, 1); } catch (e) {} } } diff --git a/src/models/crypto.js b/src/models/crypto_model.js similarity index 93% rename from src/models/crypto.js rename to src/models/crypto_model.js index 65b3dec..fd92fa8 100644 --- a/src/models/crypto.js +++ b/src/models/crypto_model.js @@ -30,12 +30,14 @@ const ENVELOPE_VERSION = 1; const KEY_DISTRIB_TYPE = 'tribe-keys-distrib'; const KEY_DISTRIB_BATCH = 7; -module.exports = (configPath, namespace = 'tribes') => { +const SHARED_SCOPE = 'tribes'; + +const createScope = (configPath, namespace) => { const keysDir = require('../configs/state-manager').keysDir(configPath); try { fs.mkdirSync(keysDir, { recursive: true, mode: 0o700 }); } catch (_) {} const keyringPath = path.join(keysDir, `${namespace}-keys.json`); - if (namespace === 'tribes') { + if (namespace === SHARED_SCOPE) { const legacyPath = path.join(configPath, 'tribe-keys.json'); try { if (fs.existsSync(legacyPath) && !fs.existsSync(keyringPath)) { @@ -65,7 +67,7 @@ module.exports = (configPath, namespace = 'tribes') => { try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} }; - const generateTribeKey = () => crypto.randomBytes(32).toString('hex'); + const generateKey = () => crypto.randomBytes(32).toString('hex'); const HEX_KEY = /^[0-9a-f]{64}$/i; const validKey = (k) => typeof k === 'string' && HEX_KEY.test(k); @@ -198,7 +200,7 @@ module.exports = (configPath, namespace = 'tribes') => { return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8'); }; - const isTribeMsg = (c) => + const isSealedMsg = (c) => !!c && c.type === ENVELOPE_TYPE && c.v === ENVELOPE_VERSION && typeof c.fp === 'string' && typeof c.p === 'string'; const wrapMsg = (body, kHex) => { @@ -210,7 +212,7 @@ module.exports = (configPath, namespace = 'tribes') => { }; const unwrapMsg = (envelope, fpIdx) => { - if (!isTribeMsg(envelope)) return null; + if (!isSealedMsg(envelope)) return null; const aad = buildAad(envelope.fp); const tryKey = (rootId, keyHex) => { try { @@ -495,15 +497,15 @@ module.exports = (configPath, namespace = 'tribes') => { return buildKeyChainSets(ancestryIds); }; - const encryptForTribe = async (content, tribeId, tribesModel) => { + const encryptForGroup = async (content, tribeId, tribesModel) => { const chain = await resolveKeyChain(tribeId, tribesModel); - if (!chain) throw new Error('Missing tribe key chain โ€” cannot encrypt content for this tribe'); + if (!chain) throw new Error('Missing key chain โ€” cannot encrypt this content'); return encryptContent(content, chain, true); }; - const decryptFromTribe = async (content, tribesModel) => { + const decryptFromGroup = async (content, tribesModel) => { if (!content) return content; - if (isTribeMsg(content)) { + if (isSealedMsg(content)) { const fpIdx = buildFingerprintIndex(); const r = unwrapMsg(content, fpIdx); if (!r) return { ...content, _undecryptable: true }; @@ -539,17 +541,17 @@ module.exports = (configPath, namespace = 'tribes') => { }; const createHelpers = (tribesModel) => ({ - async encryptIfTribe(content) { + async encryptIfGroup(content) { if (!content || !content.tribeId || !tribesModel) return content; const rootId = await tribesModel.getRootId(content.tribeId); const key = getKey(rootId); - if (!key) throw new Error(`Missing tribe key for ${rootId} โ€” cannot publish encrypted content`); + if (!key) throw new Error(`Missing key for ${rootId} โ€” cannot publish encrypted content`); const body = { k: content.type, ...content }; return wrapMsg(body, key); }, - async decryptIfTribe(content) { + async decryptIfGroup(content) { if (!content || !tribesModel) return content; - if (isTribeMsg(content)) { + if (isSealedMsg(content)) { const fpIdx = buildFingerprintIndex(); const r = unwrapMsg(content, fpIdx); if (!r) return { ...content, _undecryptable: true }; @@ -560,17 +562,17 @@ module.exports = (configPath, namespace = 'tribes') => { return flat; } if (!content.encryptedPayload) return content; - return await decryptFromTribe(content, tribesModel); + return await decryptFromGroup(content, tribesModel); }, assertReadable(decrypted, what) { - if (decrypted && decrypted._undecryptable) throw new Error(`${what} is tribe-encrypted and cannot be decrypted with available keys`); + if (decrypted && decrypted._undecryptable) throw new Error(`${what} is encrypted and cannot be decrypted with available keys`); }, async decryptIndexNodes(idx) { if (!tribesModel) return; for (const [k, n] of idx.nodes.entries()) { if (!n.c) continue; - if (!n.c.encryptedPayload && !isTribeMsg(n.c)) continue; - const dec = await decryptFromTribe(n.c, tribesModel); + if (!n.c.encryptedPayload && !isSealedMsg(n.c)) continue; + const dec = await decryptFromGroup(n.c, tribesModel); if (dec && !dec._undecryptable) { idx.nodes.set(k, { ...n, c: { ...dec, _decrypted: true } }); } else { @@ -586,7 +588,7 @@ module.exports = (configPath, namespace = 'tribes') => { for (const m of messages) { const c = m && m.value && m.value.content; if (!c) continue; - if (!isTribeMsg(c)) { out.push(m); continue; } + if (!isSealedMsg(c)) { out.push(m); continue; } const r = unwrapMsg(c, fpIdx); if (!r || !r.body) continue; const inner = r.body; @@ -606,7 +608,7 @@ module.exports = (configPath, namespace = 'tribes') => { if (!tribeId || !tribesModel) return { type: 'tombstone', target, deletedAt, author }; const rootId = await tribesModel.getRootId(tribeId); const key = getKey(rootId); - if (!key) throw new Error(`Missing tribe key for ${rootId} โ€” cannot publish encrypted tombstone`); + if (!key) throw new Error(`Missing key for ${rootId} โ€” cannot publish encrypted tombstone`); return wrapMsg({ k: 'tombstone', target, deletedAt, author }, key); } }); @@ -617,10 +619,10 @@ module.exports = (configPath, namespace = 'tribes') => { SENSITIVE_FIELDS, ENVELOPE_PRESERVE, ENVELOPE_TYPE, ENVELOPE_VERSION, KEY_DISTRIB_TYPE, KEY_DISTRIB_BATCH, loadKeyring, saveKeyring, - generateTribeKey, getKey, getKeys, getGen, getAllRootIds, + generateKey, getKey, getKeys, getGen, getAllRootIds, setKey, setKeys, mergeKeys, addNewKey, dropKey, keyPlan, fingerprint, buildFingerprintIndex, - isTribeMsg, wrapMsg, unwrapMsg, + isSealedMsg, wrapMsg, unwrapMsg, encryptWithKey, decryptWithKey, generateInviteSalt, hashInviteCode, deriveInviteKey, encryptForInvite, decryptFromInvite, @@ -630,7 +632,16 @@ module.exports = (configPath, namespace = 'tribes') => { canonicalAad, encryptChain, decryptChain, encryptContent, decryptContent, buildKeyChainSets, resolveKeyChain, resolveKeyChainSets, - encryptForTribe, decryptFromTribe, + encryptForGroup, decryptFromGroup, createHelpers }; }; + +module.exports = (configPath) => { + const scopes = new Map(); + const scope = (namespace = SHARED_SCOPE) => { + if (!scopes.has(namespace)) scopes.set(namespace, createScope(configPath, namespace)); + return scopes.get(namespace); + }; + return { ...scope(SHARED_SCOPE), scope }; +}; diff --git a/src/models/data_model.js b/src/models/data_model.js index f51ca0b..6079ea9 100644 --- a/src/models/data_model.js +++ b/src/models/data_model.js @@ -165,6 +165,7 @@ module.exports = ({ cooler, favoriteIdsFor = null }) => { const vg = versionGroups(versions); const latestByRoot = new Map(); for (const root of vg.roots) { + if (tomb.has(root) || vg.isForeignCopy(root)) continue; const tip = candidates.get(vg.tipOf(root)); if (!tip) continue; if (tip.kind !== 'tribes') { latestByRoot.set(root, { ...tip, key: root }); continue; } diff --git a/src/models/documents_model.js b/src/models/documents_model.js index b044b9d..8f9536e 100644 --- a/src/models/documents_model.js +++ b/src/models/documents_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); @@ -107,12 +108,6 @@ module.exports = ({ cooler }) => { }; }; - const hasBlob = (ssbClient, blobId) => - new Promise((resolve) => { - if (!blobId) return resolve(false); - ssbClient.blobs.has(blobId, (err, has) => resolve(!err && !!has)); - }); - const favoritesSetForDocuments = async () => { try { return await contentFavorites.getFavoriteSet("documents"); @@ -180,7 +175,7 @@ module.exports = ({ cooler }) => { if (oldMsg.content?.type !== "document") throw new Error("Document not found"); if (String(oldMsg.content.author) !== String(userId)) throw new Error("Not the author"); const aggDoc = await this.getDocumentById(id); - if (aggDoc && Object.keys(aggDoc.opinions || {}).some(k => (aggDoc.opinions[k] || 0) > 0)) throw new Error("Cannot edit document after it has received opinions."); + opinionRules.assertMediaKept(aggDoc, !!blobMarkdown); const parsedTags = parseCSV(tagsRaw); const tags = parsedTags !== undefined ? parsedTags : safeArr(oldMsg.content.tags); @@ -277,10 +272,7 @@ module.exports = ({ cooler }) => { out = out.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } - const checked = await Promise.all(out.map(async (d) => ((await hasBlob(ssbClient, d.url)) ? d : null))); - return checked - .filter(Boolean) - .map((d) => ({ ...d, isFavorite: favorites.has(d.rootId || d.key) })); + return out.map((d) => ({ ...d, isFavorite: favorites.has(d.rootId || d.key) })); }, async getDocumentById(id) { diff --git a/src/models/events_model.js b/src/models/events_model.js index fb95fb2..def8b04 100644 --- a/src/models/events_model.js +++ b/src/models/events_model.js @@ -16,28 +16,28 @@ const EVENT_TYPES = [ 'tribe-keys', 'tombstone' ]; -module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, eventCrypto, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; const me = async () => (await openSsb()).id; - const ownCrypto = eventCrypto || tribeCrypto; - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null; + const ownCrypto = eventCrypto || cryptoModel; + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null; - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0); const readAll = async (ssbClient) => readTyped(ssbClient, EVENT_TYPES, { limit: logLimit }); const rotateEventKey = async (rid, attendees) => { - if (!ownCrypto || !tribeCrypto || !rid || !lookupKey(rid)) return; + if (!ownCrypto || !cryptoModel || !rid || !lookupKey(rid)) return; const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); - const newKey = ownCrypto.generateTribeKey(); + const newKey = ownCrypto.generateKey(); const newGen = ownCrypto.addNewKey(rid, newKey); const memberKeys = {}; for (const m of new Set([ssbClient.id, ...(Array.isArray(attendees) ? attendees : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} } await new Promise((resolve) => { ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: newGen, memberKeys }, () => resolve()); @@ -45,7 +45,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { }; const ensureAttendeeKeys = async (ssbClient, messages, events) => { - if (!tribeCrypto || !ownCrypto) return; + if (!cryptoModel || !ownCrypto) return; const leavesByRoot = new Map(); for (const m of messages) { const c = m.value && m.value.content; @@ -63,7 +63,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const key = lookupKey(ev.rid); const memberKeys = {}; for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys); } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys); } catch (_) {} } if (!Object.keys(memberKeys).length) continue; await new Promise((resolve) => { @@ -100,10 +100,10 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const decryptEventContent = (rawContent, eventId) => { if (!rawContent) return null; if (!rawContent.encryptedPayload) return rawContent; - if (!ownCrypto || !tribeCrypto) return { ...rawContent, _undecryptable: true }; + if (!ownCrypto || !cryptoModel) return { ...rawContent, _undecryptable: true }; const keys = (ownCrypto.getKeys && ownCrypto.getKeys(eventId)) || []; if (!keys.length) return { ...rawContent, _undecryptable: true }; - const dec = tribeCrypto.decryptContent(rawContent, keys.map(k => [k])); + const dec = cryptoModel.decryptContent(rawContent, keys.map(k => [k])); if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true }; return { ...dec, _decrypted: true }; }; @@ -256,12 +256,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { opinions_inhabitants: [] }; - const shouldEncrypt = visibility === 'private' && ownCrypto && tribeCrypto; + const shouldEncrypt = visibility === 'private' && ownCrypto && cryptoModel; let eventKey = null; let content = plainContent; if (shouldEncrypt) { - eventKey = ownCrypto.generateTribeKey(); - content = tribeCrypto.encryptContent(plainContent, [eventKey], true); + eventKey = ownCrypto.generateKey(); + content = cryptoModel.encryptContent(plainContent, [eventKey], true); } const result = await new Promise((resolve, reject) => { @@ -274,7 +274,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; for (const m of attendeeList) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(eventKey, m, ssbKeys); } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(eventKey, m, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -287,7 +287,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { }, async generateInvite(eventId) { - if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable"); + if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable"); const ssbClient = await openSsb(); const userId = await me(); const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m))); @@ -299,11 +299,11 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const key = lookupKey(rid); if (!key) throw new Error("Missing event key โ€” cannot generate invite"); const code = crypto.randomBytes(16).toString('hex'); - const inviteSalt = tribeCrypto.generateInviteSalt(); - const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt); + const inviteSalt = cryptoModel.generateInviteSalt(); + const ek = cryptoModel.encryptForInvite(key, code, inviteSalt); const inviteRef = { code, ek, salt: inviteSalt, gen: 1, rootId: rid }; await new Promise((resolve, reject) => { - ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve()); + ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve()); }); return { code, eventId: rid }; }, @@ -339,7 +339,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { }, async generateOpenInvite(eventId) { - if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable"); + if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable"); const ssbClient = await openSsb(); const userId = await me(); const ev = await new Promise((res, rej) => ssbClient.get(eventId, (err, m) => err || !m || !m.content ? rej(new Error("Error retrieving event")) : res(m))); @@ -352,10 +352,10 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const key = lookupKey(rid); if (!key) throw new Error("Missing event key โ€” cannot generate invite"); const code = crypto.randomBytes(16).toString('hex'); - const inviteSalt = tribeCrypto.generateInviteSalt(); - const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt); + const inviteSalt = cryptoModel.generateInviteSalt(); + const ek = cryptoModel.encryptForInvite(key, code, inviteSalt); const invitePub = await new Promise((resolve, reject) => { - ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r)); + ssbClient.publish({ type: 'event-invite', target: rid, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r)); }); await new Promise((resolve, reject) => { ssbClient.publish({ type: 'event-open-invite', v: 1, target: rid, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve()); @@ -391,7 +391,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { }, async joinByInvite(code) { - if (!ownCrypto || !tribeCrypto) throw new Error("Event crypto unavailable"); + if (!ownCrypto || !cryptoModel) throw new Error("Event crypto unavailable"); const ssbClient = await openSsb(); const userId = await me(); const messages = await readAll(ssbClient); @@ -406,12 +406,12 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { if (!c || c.type !== 'event-invite') continue; if (invTomb.has(m.key)) continue; try { - const hash = tribeCrypto.hashInviteCode(code, c.salt); + const hash = cryptoModel.hashInviteCode(code, c.salt); if (hash === c.codeHash) { matched = c; break; } } catch (_) {} } if (!matched) throw new Error("Invalid or expired invite code"); - const eventKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt); + const eventKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt); if (!eventKey) throw new Error("Could not decrypt invite"); ownCrypto.addNewKey(matched.target, eventKey); await this.toggleAttendee(matched.target); @@ -444,13 +444,13 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { ssbClient.publish({ type: 'eventAttend', target: rid, on, createdAt: new Date().toISOString() }, (err2, res2) => err2 ? reject(err2) : resolve(res2)); }); - if (isPrivate && !isLeaving && ownCrypto && tribeCrypto) { + if (isPrivate && !isLeaving && ownCrypto && cryptoModel) { try { const key = lookupKey(rid); if (key) { const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(key, userId, ssbKeys); } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(key, userId, ssbKeys); } catch (_) {} if (memberKeys[userId]) { await new Promise((resolve) => { ssbClient.publish({ type: "tribe-keys", tribeId: rid, generation: 1, memberKeys }, () => resolve()); @@ -498,6 +498,7 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { return { id: contentTip, + rootId: rid, title: c.title || '', description: c.description || '', date: c.date || '', @@ -578,26 +579,26 @@ module.exports = ({ cooler, tribeCrypto, eventCrypto, tribesModel }) => { const wasPrivate = normalizePrivacy(c.isPublic) === 'private'; const isPrivate = updated.isPublic === 'private'; let newKey = null; - if (isPrivate && ownCrypto && tribeCrypto) { + if (isPrivate && ownCrypto && cryptoModel) { let key = lookupKey(rid); if (!key) { - newKey = ownCrypto.generateTribeKey(); + newKey = ownCrypto.generateKey(); ownCrypto.setKey(rid, newKey, 1); key = newKey; } - updated = tribeCrypto.encryptContent(updated, [key], true); + updated = cryptoModel.encryptContent(updated, [key], true); } const result = await new Promise((resolve, reject) => { ssbClient.publish(updated, (err2, res2) => err2 ? reject(err2) : resolve(res2)); }); - if (newKey && tribeCrypto) { + if (newKey && cryptoModel) { try { const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; for (const m of (Array.isArray(c.attendees) ? c.attendees : [userId])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { diff --git a/src/models/feed_model.js b/src/models/feed_model.js index e37d5c9..c5c0d15 100644 --- a/src/models/feed_model.js +++ b/src/models/feed_model.js @@ -18,9 +18,12 @@ module.exports = ({ cooler }) => { const cleanText = (t) => (typeof t === "string" ? t.replace(/\r\n?/g, "\n").trim() : ""); + const FEED_MEDIA_SRC = String.raw`!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)`; const isValidFeedText = (t) => { - const s = cleanText(t).replace(/!?\[[^\]\n]*\]\(\s*&[^)\s]+\.sha256\s*\)/g, "").trim(); - return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX; + const all = cleanText(t); + const s = all.replace(new RegExp(FEED_MEDIA_SRC, "g"), "").trim(); + if (s.length > FEED_TEXT_MAX) return false; + return s.length >= FEED_TEXT_MIN || (!s && new RegExp(FEED_MEDIA_SRC).test(all)); }; const getMsg = (ssbClient, id) => @@ -100,16 +103,18 @@ module.exports = ({ cooler }) => { if (typeof text !== "string") throw new Error("Invalid text"); const cleaned = cleanText(text); + const attached = typeof media === "string" ? media.trim() : ""; + const full = [cleaned, attached].filter(Boolean).join("\n"); - if (!isValidFeedText(cleaned)) { - if (cleaned.length < FEED_TEXT_MIN) throw new Error("Text too short"); + if (!full) throw new Error("Text required"); + if (!isValidFeedText(full)) { if (cleaned.length > FEED_TEXT_MAX) throw new Error("Text too long"); - throw new Error("Text required"); + throw new Error("Text too short"); } const content = { type: "feed", - text: typeof media === "string" && media.trim() ? `${cleaned}\n${media.trim()}` : cleaned, + text: full, author: userId, createdAt: new Date().toISOString(), tags: extractTags(cleaned), diff --git a/src/models/files_model.js b/src/models/files_model.js index 94acd6a..6874c5e 100644 --- a/src/models/files_model.js +++ b/src/models/files_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { readTyped, memoIndex } = require("./typed_log"); @@ -19,6 +20,7 @@ const fileExtra = (extra) => { const out = {}; if (extra.mime) out.mime = String(extra.mime); if (extra.fileName) out.fileName = String(extra.fileName); + if (typeof extra.license === "string" && extra.license) out.license = extra.license; if (typeof extra.torrentKey === "string" && extra.torrentKey.startsWith("%")) out.torrentKey = extra.torrentKey; if (typeof extra.torrentUrl === "string" && extra.torrentUrl.startsWith("&")) out.torrentUrl = extra.torrentUrl; if (extra.cipher && typeof extra.cipher === "object" && extra.cipher.key && extra.cipher.manifestBlobId) out.cipher = { v: Number(extra.cipher.v) || 1, key: String(extra.cipher.key), manifestBlobId: String(extra.cipher.manifestBlobId), chunkCount: Number(extra.cipher.chunkCount) || 0 }; @@ -36,7 +38,7 @@ const parseBlobId = (blobMarkdown) => { const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); -module.exports = ({ cooler, tribeCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { @@ -44,9 +46,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return ssb; }; - const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null; - const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c; - const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c; + const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null; + const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c; + const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c; const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}; const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {}; const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'file') : msgs; @@ -141,6 +143,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { size: c.size || 0, mime: undec ? "" : (c.mime || ""), fileName: undec ? "" : (c.fileName || ""), + license: undec ? "" : (c.license || ""), torrentKey: undec ? "" : (typeof c.torrentKey === "string" && c.torrentKey.startsWith("%") ? c.torrentKey : ""), torrentUrl: undec ? "" : (typeof c.torrentUrl === "string" && c.torrentUrl.startsWith("&") ? c.torrentUrl : ""), cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), @@ -199,7 +202,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ...fileExtra(extra) }; - content = await encryptIfTribe(content); + content = await encryptIfGroup(content); return new Promise((resolve, reject) => { ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); @@ -236,12 +239,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const oldMsg = await getMsg(ssbClient, tipId); if (!oldMsg) throw new Error("File not found"); - const oldDec = await decryptIfTribe(oldMsg.content); + const oldDec = await decryptIfGroup(oldMsg.content); if (!oldDec || oldDec.type !== "file") throw new Error("File not found"); assertReadable(oldDec, "File"); if (oldMsg.author !== userId) throw new Error("Not the author"); const aggFile = await this.getFileById(id, userId); - if (aggFile && Object.keys(aggFile.opinions || {}).some(k => (aggFile.opinions[k] || 0) > 0)) throw new Error("Cannot edit file after it has received opinions."); + opinionRules.assertMediaKept(aggFile, !!blobMarkdown); const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags); const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; @@ -255,7 +258,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { title: title !== undefined ? title || "" : oldDec.title || "", description: description !== undefined ? description || "" : oldDec.description || "", size: extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0), - ...(extra ? fileExtra(extra) : fileExtra(oldDec)), + ...fileExtra(blobId ? { license: oldDec.license, ...(extra || {}) } : { ...oldDec, ...(extra || {}) }), opinions: oldDec.opinions || {}, opinions_inhabitants: oldDec.opinions_inhabitants || [], author: oldDec.author || userId, @@ -264,7 +267,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { updatedAt: now }; - updated = await encryptIfTribe(updated); + updated = await encryptIfGroup(updated); const result = await new Promise((resolve, reject) => { ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res))); @@ -281,7 +284,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tipId); if (!msg) throw new Error("File not found"); - const dec = await decryptIfTribe(msg.content); + const dec = await decryptIfGroup(msg.content); if (!dec || dec.type !== "file") throw new Error("File not found"); if (msg.author !== userId) throw new Error("Not the author"); @@ -360,8 +363,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tip); if (!msg) throw new Error("File not found"); let c = msg.content; - if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) { - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (cryptoModel && (c?.encryptedPayload || cryptoModel.isSealedMsg(c)) && tribesModel) { + const dec = await cryptoModel.decryptFromGroup(c, tribesModel); c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; } if (!c || c.type !== "file") throw new Error("File not found"); diff --git a/src/models/fork_workflows.js b/src/models/fork_workflows.js index cba82e5..e320dd2 100644 --- a/src/models/fork_workflows.js +++ b/src/models/fork_workflows.js @@ -8,19 +8,18 @@ // Ver docs/FRONTERA_UPSTREAM.md. // // En el movil los workflows son los skins: cada uno enciende unos modulos y trae su -// barra de abajo. Van de menos a mas: Activista (accion directa, lo minimo), -// Acampada, y despues los del dev por numero de modulos. +// barra de abajo. Van de menos a mas: Minimo, Acampada, Activista y despues los del +// dev por numero de modulos. const wf = require('./workflows_model'); const FORK_WORKFLOWS = [ { - // accion directa: convocar, coordinarse, llamar y avisar. Lo minimo para salir a - // la calle; lo demas se enciende con otro skin - key: 'activista', + // lo minimo para salir a la calle: convocar, coordinarse, llamar y avisar + // (sin mapas: backend.js los apaga a la fuerza en el movil) + key: 'minimal', theme: 'Activista', homePage: 'activity', - // (sin mapas: backend.js los apaga a la fuerza en el movil) modules: ['chats', 'phone', 'events', 'emergencies', 'invites', 'backup'] }, { @@ -31,13 +30,27 @@ const FORK_WORKFLOWS = [ homePage: 'activity', modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup', 'tribes', 'parliament', 'votes', 'events'] + }, + { + // un colectivo que se organiza: lo de Acampada mas campaรฑas, alertas, encuestas, + // calendario, tareas, informes y documentos. Menos que el activists del dev (45) + key: 'activista', + theme: 'Activista', + homePage: 'activity', + modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup', + 'tribes', 'parliament', 'votes', 'events', 'emergencies', 'campaigns', 'polls', 'calendars', 'tasks', 'reports', + 'docs', 'mailing'] } ]; +// orden en el movil: estos tres primero y el resto por numero de modulos +const LADDER = ['minimal', 'acampada', 'activista']; + // barra de abajo de cada skin: los cuatro atajos de lo que mas se usa en el (claves // de PINNABLE_MODULES en hive_views.js). Quien la cambia, la cambia para ese skin const BARS = { - activista: ['activity', 'chats', 'events', 'phone'], + minimal: ['activity', 'chats', 'events', 'phone'], + activista: ['activity', 'tribes', 'chats', 'votes'], acampada: ['activity', 'chats', 'tribes', 'votes'], activists: ['activity', 'tribes', 'votes', 'forum'], social: ['activity', 'feed', 'images', 'chats'], @@ -89,4 +102,4 @@ const saveBar = (cfg, pins) => { return cfg; }; -module.exports = { FORK_WORKFLOWS, BARS, register, moduleCount, barFor, saveBar }; +module.exports = { FORK_WORKFLOWS, LADDER, BARS, register, moduleCount, barFor, saveBar }; diff --git a/src/models/forum_model.js b/src/models/forum_model.js index 3869e60..34600cb 100644 --- a/src/models/forum_model.js +++ b/src/models/forum_model.js @@ -6,15 +6,15 @@ const { readTyped, requestScope, memoIndex } = require('./typed_log'); const longText = require('../backend/long_text'); const logLimit = getConfig().ssbLogStream?.limit || 1000; -module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { +module.exports = ({ cooler, cryptoModel, forumCrypto }) => { let ssb, userId; - const ownCrypto = forumCrypto || tribeCrypto; - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null; + const ownCrypto = forumCrypto || cryptoModel; + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null; const decryptForumContent = (rawContent, rootId) => { if (!rawContent) return rawContent; if (!rawContent.encryptedPayload) return rawContent; - if (!ownCrypto || !tribeCrypto) return { ...rawContent, _undecryptable: true }; + if (!ownCrypto || !cryptoModel) return { ...rawContent, _undecryptable: true }; let keys = (rootId && ownCrypto.getKeys && ownCrypto.getKeys(rootId)) || []; if (!keys.length && typeof ownCrypto.getAllRootIds === 'function') { const seen = new Set(); @@ -24,7 +24,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { } } if (!keys.length) return { ...rawContent, _undecryptable: true }; - const dec = tribeCrypto.decryptContent(rawContent, keys.map(k => [k])); + const dec = cryptoModel.decryptContent(rawContent, keys.map(k => [k])); if (!dec || dec._undecryptable) return { ...rawContent, _undecryptable: true }; return { ...dec, _decrypted: true }; }; @@ -289,8 +289,8 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { isPrivate: isPrivateFlag }; let forumKey = null; - if (isPrivateFlag && ownCrypto && tribeCrypto) forumKey = ownCrypto.generateTribeKey(); - const seal = (c) => forumKey ? tribeCrypto.encryptContent(c, [forumKey], true) : c; + if (isPrivateFlag && ownCrypto && cryptoModel) forumKey = ownCrypto.generateKey(); + const seal = (c) => forumKey ? cryptoModel.encryptContent(c, [forumKey], true) : c; const publish = (c) => new Promise((resolve, reject) => ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); @@ -300,7 +300,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { ownCrypto.setKey(result.key, forumKey, 1); try { const ssbKeys = require('../server/node_modules/ssb-keys'); - const memberKeys = { [userId]: tribeCrypto.boxKeyForMember(forumKey, userId, ssbKeys) }; + const memberKeys = { [userId]: cryptoModel.boxKeyForMember(forumKey, userId, ssbKeys) }; await new Promise((resolve) => { ssbClient.publish({ type: 'tribe-keys', tribeId: result.key, generation: 1, memberKeys }, () => resolve()); }); @@ -328,11 +328,11 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }; if (parentId) content.branch = parentId; let key = null; - if (isPrivate && ownCrypto && tribeCrypto) { + if (isPrivate && ownCrypto && cryptoModel) { key = lookupKey(forumId); if (!key) throw new Error('Missing forum key โ€” cannot reply to encrypted forum'); } - const seal = (c) => key ? tribeCrypto.encryptContent(c, [key], true) : c; + const seal = (c) => key ? cryptoModel.encryptContent(c, [key], true) : c; const publish = (c) => new Promise((resolve, reject) => ssbClient.publish(seal(c), (err, res) => err ? reject(err) : resolve(res)) ); @@ -341,7 +341,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }, generateInvite: async (forumId) => { - if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable'); + if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable'); const ssbClient = await openSsb(); const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m))); if (!rawRoot || !rawRoot.content) throw new Error('Forum not found'); @@ -353,10 +353,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const key = lookupKey(forumId); if (!key) throw new Error('Missing forum key'); const code = crypto.randomBytes(16).toString('hex'); - const inviteSalt = tribeCrypto.generateInviteSalt(); - const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt); + const inviteSalt = cryptoModel.generateInviteSalt(); + const ek = cryptoModel.encryptForInvite(key, code, inviteSalt); await new Promise((resolve, reject) => { - ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve()); + ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt) }, (err) => err ? reject(err) : resolve()); }); return { code, forumId }; }, @@ -367,7 +367,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }, generateOpenInvite: async (forumId) => { - if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable'); + if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable'); const ssbClient = await openSsb(); const rawRoot = await new Promise((res, rej) => ssbClient.get(forumId, (e, m) => e ? rej(e) : res(m))); if (!rawRoot || !rawRoot.content) throw new Error('Forum not found'); @@ -380,10 +380,10 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { const key = lookupKey(forumId); if (!key) throw new Error('Missing forum key'); const code = crypto.randomBytes(16).toString('hex'); - const inviteSalt = tribeCrypto.generateInviteSalt(); - const ek = tribeCrypto.encryptForInvite(key, code, inviteSalt); + const inviteSalt = cryptoModel.generateInviteSalt(); + const ek = cryptoModel.encryptForInvite(key, code, inviteSalt); const invitePub = await new Promise((resolve, reject) => { - ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: tribeCrypto.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r)); + ssbClient.publish({ type: 'forum-invite', target: forumId, ek, salt: inviteSalt, codeHash: cryptoModel.hashInviteCode(code, inviteSalt), multi: 1 }, (err, r) => err ? reject(err) : resolve(r)); }); await new Promise((resolve, reject) => { ssbClient.publish({ type: 'forum-open-invite', v: 1, target: forumId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (err) => err ? reject(err) : resolve()); @@ -406,7 +406,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }, joinByInvite: async (code) => { - if (!ownCrypto || !tribeCrypto) throw new Error('Forum crypto unavailable'); + if (!ownCrypto || !cryptoModel) throw new Error('Forum crypto unavailable'); const ssbClient = await openSsb(); const messages = await readForumLog(); const authorOf = authorsByKey(messages); @@ -418,12 +418,12 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { if (invTomb.has(m.key)) continue; if (!authorOf.get(c.target) || authorOf.get(c.target) !== m.value.author) continue; try { - const hash = tribeCrypto.hashInviteCode(code, c.salt); + const hash = cryptoModel.hashInviteCode(code, c.salt); if (hash === c.codeHash) { matched = c; break; } } catch (_) {} } if (!matched) throw new Error('Invalid or expired invite code'); - const forumKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt); + const forumKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt); if (!forumKey) throw new Error('Could not decrypt invite'); ownCrypto.addNewKey(matched.target, forumKey); return { ok: true, forumId: matched.target }; @@ -521,7 +521,7 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { }; }, - getMessagesByForumId: async forumId => { + getMessagesByForumId: async (forumId, ssbReplies = []) => { const ssbClient = await openSsb(); const msgs = await readForumLog(); const deleted = deletedOf(msgs); @@ -537,8 +537,11 @@ module.exports = ({ cooler, tribeCrypto, forumCrypto }) => { if (c.root !== forumId) return null; return { c, m }; }; - const replies = msgs - .map(decodeReply) + const parentOf = (branch) => [].concat(branch || []).filter(b => typeof b === 'string' && b !== forumId).pop() || null; + const fromPosts = (Array.isArray(ssbReplies) ? ssbReplies : []) + .filter(m => m && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.root === forumId) + .map(m => ({ c: { ...m.value.content, branch: parentOf(m.value.content.branch) }, m })); + const replies = [...msgs.map(decodeReply), ...fromPosts] .filter(r => r && !deleted.has(r.m.key)) .map(({ c, m }) => ({ key: m.key, diff --git a/src/models/housing_model.js b/src/models/housing_model.js index 1f06356..99fa2bb 100644 --- a/src/models/housing_model.js +++ b/src/models/housing_model.js @@ -68,47 +68,47 @@ const matchSearch = (item, q) => { return hay.includes(qq) } -module.exports = ({ cooler, tribeCrypto }) => { +module.exports = ({ cooler, cryptoModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } const isEncrypted = (c) => !!(c && c.encryptedPayload) const keysForRoot = (rootId) => { - if (!tribeCrypto || !rootId) return [] - const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || [] + if (!cryptoModel || !rootId) return [] + const ks = (cryptoModel.getKeys && cryptoModel.getKeys(rootId)) || [] if (ks.length) return ks - const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null + const k = cryptoModel.getKey ? cryptoModel.getKey(rootId) : null return k ? [k] : [] } const decryptHousing = (c, rootId) => { if (!isEncrypted(c)) return c - if (!tribeCrypto) return { ...c, _undecryptable: true } + if (!cryptoModel) return { ...c, _undecryptable: true } const keys = keysForRoot(rootId) if (!keys.length) return { ...c, _undecryptable: true } - return tribeCrypto.decryptContent(c, keys.map(k => [k])) + return cryptoModel.decryptContent(c, keys.map(k => [k])) } const publishHousing = (ssbClient, content, rootId) => new Promise((resolve, reject) => { const hidden = String(content.visibility || "PUBLIC").toUpperCase() === "HIDDEN" - if (!hidden || !tribeCrypto) { + if (!hidden || !cryptoModel) { ssbClient.publish(content, (err, msg) => { if (err) return reject(err) - if (msg && msg.key && tribeCrypto && !rootId) { - try { tribeCrypto.setKey(msg.key, tribeCrypto.generateTribeKey(), 1) } catch (_) {} + if (msg && msg.key && cryptoModel && !rootId) { + try { cryptoModel.setKey(msg.key, cryptoModel.generateKey(), 1) } catch (_) {} } resolve(msg) }) return } let key = rootId ? (keysForRoot(rootId)[0] || null) : null - if (!key) key = tribeCrypto.generateTribeKey() + if (!key) key = cryptoModel.generateKey() let envelope - try { envelope = tribeCrypto.encryptContent(content, [key], true) } catch (err) { return reject(err) } + try { envelope = cryptoModel.encryptContent(content, [key], true) } catch (err) { return reject(err) } ssbClient.publish(envelope, (err, msg) => { if (err) return reject(err) - try { tribeCrypto.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {} + try { cryptoModel.setKey(rootId || (msg && msg.key), key, 1) } catch (_) {} resolve(msg) }) }) diff --git a/src/models/images_model.js b/src/models/images_model.js index e9a38e1..df35670 100644 --- a/src/models/images_model.js +++ b/src/models/images_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); @@ -170,7 +171,7 @@ module.exports = ({ cooler }) => { if (!oldMsg || oldMsg.content?.type !== "image") throw new Error("Image not found"); if (oldMsg.content.author !== userId) throw new Error("Not the author"); const aggImg = await this.getImageById(id, userId); - if (aggImg && Object.keys(aggImg.opinions || {}).some(k => (aggImg.opinions[k] || 0) > 0)) throw new Error("Cannot edit image after it has received opinions."); + opinionRules.assertMediaKept(aggImg, !!blobMarkdown); const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags); const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; diff --git a/src/models/industry_model.js b/src/models/industry_model.js index 5593924..4ff52d8 100644 --- a/src/models/industry_model.js +++ b/src/models/industry_model.js @@ -724,6 +724,7 @@ module.exports = ({ cooler }) => { builds.sort((x, y) => new Date(y.startDate || 0) - new Date(x.startDate || 0)) return { ...b.blueprint, + rootId: b.root, facilityId: g.root, facilityName: g.facility.name, members: g.facility.members, diff --git a/src/models/jobs_model.js b/src/models/jobs_model.js index d396483..f24457e 100644 --- a/src/models/jobs_model.js +++ b/src/models/jobs_model.js @@ -38,7 +38,7 @@ const matchSearch = (job, q) => { return hay.includes(qq) } -module.exports = ({ cooler, tribeCrypto }) => { +module.exports = ({ cooler, cryptoModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } @@ -106,7 +106,7 @@ module.exports = ({ cooler, tribeCrypto }) => { } const buildJobObject = (node, rootId, subscribers) => { - const visibleSubs = (tribeCrypto && tribeCrypto.getKey(rootId)) || ssb?.id === (node.author || node.c?.author) ? subscribers : []; + const visibleSubs = (cryptoModel && cryptoModel.getKey(rootId)) || ssb?.id === (node.author || node.c?.author) ? subscribers : []; const c = node.c || {} let blobId = c.image || null if (blobId && /\(([^)]+)\)/.test(String(blobId))) blobId = String(blobId).match(/\(([^)]+)\)/)[1] @@ -207,9 +207,9 @@ module.exports = ({ cooler, tribeCrypto }) => { return new Promise((res, rej) => ssbClient.publish(content, (e, m) => { if (e) return rej(e) - if (m && m.key && tribeCrypto) { - const key = tribeCrypto.generateTribeKey() - tribeCrypto.setKey(m.key, key, 1) + if (m && m.key && cryptoModel) { + const key = cryptoModel.generateKey() + cryptoModel.setKey(m.key, key, 1) } res(m) })) diff --git a/src/models/karma_model.js b/src/models/karma_model.js index c500986..a3fdca8 100644 --- a/src/models/karma_model.js +++ b/src/models/karma_model.js @@ -1,5 +1,6 @@ const pull = require("../server/node_modules/pull-stream"); const { readTyped, CONTENT_TYPES } = require("./typed_log"); +const { isComment } = require("./comments_model"); const { getConfig } = require("../configs/config-manager.js"); const { config } = require("../server/SSB_server.js"); @@ -63,6 +64,7 @@ function idsEqual(a, b) { } function normalizeType(a) { + if (isComment(a.content)) return "comment"; const t = a.type || a.content?.type || inferType(a.content) || ""; return String(t).toLowerCase(); } diff --git a/src/models/larp_model.js b/src/models/larp_model.js index 3c72b0e..9f886ee 100644 --- a/src/models/larp_model.js +++ b/src/models/larp_model.js @@ -172,7 +172,7 @@ function getGoverningPeriodId(now = new Date()) { return `${getGoverningHouseKey(now)}:${now.getFullYear()}-${month}`; } -module.exports = ({ cooler, tribesModel, tribeCrypto }) => { +module.exports = ({ cooler, tribesModel, cryptoModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; const startedAt = Date.now(); @@ -204,6 +204,11 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return candidates[0]; } + async function houseTribeReady(houseKey) { + if (await findMyHouseTribe(houseKey).catch(() => null)) return true; + return houseKey === 'academia' && !!(await findAnyHouseTribe(houseKey).catch(() => null)); + } + async function findAnyHouseTribe(houseKey) { if (!tribesModel || !VALID_KEY(houseKey)) return null; let list = []; @@ -307,8 +312,8 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { if (houseKey !== 'academia') { await publishHouseAnchorTombstone(houseKey, rootId).catch(() => {}); } - if (tribeCrypto && typeof tribeCrypto.dropKey === 'function') { - try { tribeCrypto.dropKey(rootId); } catch (_) {} + if (cryptoModel && typeof cryptoModel.dropKey === 'function') { + try { cryptoModel.dropKey(rootId); } catch (_) {} } } @@ -387,7 +392,10 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { return myCanonical.tribe; } - if (anchor) return null; + if (anchor) { + await redeemPendingAutoInvites().catch(() => {}); + return await findMyHouseTribe(houseKey); + } const existingAny = await findAnyHouseTribe(houseKey).catch(() => null); if (existingAny) { @@ -437,8 +445,8 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { if (houseKey !== 'academia') { await publishHouseAnchorTombstone(houseKey, t.rootId).catch(() => {}); } - if (tribeCrypto && typeof tribeCrypto.dropKey === 'function') { - try { tribeCrypto.dropKey(t.rootId); } catch (_) {} + if (cryptoModel && typeof cryptoModel.dropKey === 'function') { + try { cryptoModel.dropKey(t.rootId); } catch (_) {} } } } @@ -954,6 +962,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => { createHouseInvite, redeemHouseInvite, findMyHouseTribe, + houseTribeReady, ensureHouseTribe, leaveMyHouseTribe, issueAutoInvitesForMyHouse, diff --git a/src/models/logs_model.js b/src/models/logs_model.js index 741b44d..8c7dd81 100644 --- a/src/models/logs_model.js +++ b/src/models/logs_model.js @@ -1,3 +1,4 @@ +const { isComment } = require('./comments_model'); const pull = require('../server/node_modules/pull-stream'); const { requestScope } = require('./typed_log'); const util = require('util'); @@ -60,6 +61,7 @@ const ACTION_TYPES = new Set([ const ACTION_PHRASES = { post: 'published a post', + comment: 'commented on some content', about: 'updated profile information', contact: 'followed or unfollowed someone', feed: 'shared content in the feed', @@ -243,7 +245,7 @@ module.exports = ({ cooler }) => { if (!ACTION_TYPES.has(c.type)) continue; const ts = v.timestamp || 0; const summary = c.title || c.text || c.question || c.subject || c.name || c.concept || c.description || ''; - out.push({ key: m.key, ts, type: c.type, summary: compact(summary) }); + out.push({ key: m.key, ts, type: isComment(c) ? 'comment' : c.type, summary: compact(summary) }); } return out; } diff --git a/src/models/main_models.js b/src/models/main_models.js index 477692c..f06f332 100644 --- a/src/models/main_models.js +++ b/src/models/main_models.js @@ -384,7 +384,7 @@ models.about = { ); }); const now = Date.now(); - for (const [id, p] of out) if (p.name) nameCache.set(id, p.name, now); + for (const [id, p] of out) { if (p.name) nameCache.set(id, p.name, now); if (p.image) nameCache.setImage(id, p.image); } return out; })(); if (store) store.aboutProfiles = pending; @@ -433,6 +433,7 @@ models.about = { return nullImage; } if (typeof raw.link === "string") { + nameCache.setImage(feedId, raw.link); return raw.link; } return raw; diff --git a/src/models/maps_model.js b/src/models/maps_model.js index 0cae474..f862cee 100644 --- a/src/models/maps_model.js +++ b/src/models/maps_model.js @@ -19,7 +19,7 @@ const normalizeTags = (raw) => { const ALLOWED_MAP_TYPES = new Set(["OPEN", "CLOSED", "SINGLE"]); -module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, mapCrypto, tribesModel }) => { let ssb; const openSsb = async () => { @@ -27,16 +27,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { return ssb; }; - const ownCrypto = mapCrypto || tribeCrypto; - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null; + const ownCrypto = mapCrypto || cryptoModel; + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null; const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || []; if (a.length) return a; - return (tribeCrypto && tribeCrypto.getKeys(rid)) || []; + return (cryptoModel && cryptoModel.getKeys(rid)) || []; }; - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0); + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0); const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex"); } catch (_) { return null; } }; - const hashCode = (code, salt) => (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null); + const hashCode = (code, salt) => (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null); const inviteMatches = (inv, code) => { if (!code) return false; if (typeof inv === "string") return inv === code; @@ -47,16 +47,16 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || ""); const rotateMapKey = async (rootId, remainingMembers) => { - if (!ownCrypto || !tribeCrypto || !rootId) return; + if (!ownCrypto || !cryptoModel || !rootId) return; const existing = lookupKey(rootId); if (!existing) return; - const newKey = ownCrypto.generateTribeKey(); + const newKey = ownCrypto.generateKey(); const newGen = ownCrypto.addNewKey(rootId, newKey); const ssbClient = await openSsb(); const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -91,34 +91,34 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } catch (_) {} }; - const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null; - const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c; - const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c; + const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null; + const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c; + const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c; const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}; const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, ['map', 'mapMarker']) : msgs; const tombFor = async (target, tribeId, author) => tribeHelpers ? tribeHelpers.encryptTombstone(target, tribeId, author) : { type: 'tombstone', target, deletedAt: new Date().toISOString(), author }; const encryptStandalone = (content, rootId) => { - if (!tribeCrypto || !rootId) return content; + if (!cryptoModel || !rootId) return content; const key = lookupKey(rootId); if (!key) return content; - return tribeCrypto.encryptContent(content, [key], true); + return cryptoModel.encryptContent(content, [key], true); }; const tryDecryptPublicInviteKey = (invites) => { - if (!tribeCrypto || !Array.isArray(invites)) return null; + if (!cryptoModel || !Array.isArray(invites)) return null; for (const inv of invites) { if (!inv || typeof inv !== "object" || inv.public !== true) continue; if (typeof inv.code !== "string") continue; if (typeof inv.ek === "string") { try { - const k = tribeCrypto.decryptFromInvite(inv.ek, inv.code, inv.salt); + const k = cryptoModel.decryptFromInvite(inv.ek, inv.code, inv.salt); if (k) return k; } catch (_) {} } if (typeof inv.ekChain === "string") { try { - const chain = tribeCrypto.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); + const chain = cryptoModel.decryptChainFromInvite(inv.ekChain, inv.code, inv.salt); if (Array.isArray(chain) && chain.length && chain[0].key) return chain[0].key; } catch (_) {} } @@ -128,24 +128,24 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const decryptMapRoot = (content, rootId) => { if (!content || !content.encryptedPayload) return content; - if (!tribeCrypto) return content; + if (!cryptoModel) return content; const keys = lookupKeys(rootId); let candidateChains = (keys || []).map(k => [k]); const pubKey = tryDecryptPublicInviteKey(content.invites); if (pubKey) candidateChains.push([pubKey]); if (!candidateChains.length) return { ...content, _undecryptable: true }; - return tribeCrypto.decryptContent(content, candidateChains); + return cryptoModel.decryptContent(content, candidateChains); }; const decryptIndexNodes = async (idx) => { - if (!tribeCrypto) return; + if (!cryptoModel) return; for (const [k, n] of (idx.nodes ? idx.nodes.entries() : [])) { if (!n.c || !n.c.encryptedPayload) continue; const root = idx.rootOf(k); let dec = null; if (n.c.tribeId && tribesModel) { try { - const r = await tribeCrypto.decryptFromTribe(n.c, tribesModel); + const r = await cryptoModel.decryptFromGroup(n.c, tribesModel); if (r && !r._undecryptable) dec = r; } catch (_) {} } @@ -342,7 +342,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { }; const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto || !ownCrypto) return; + if (!cryptoModel || !ownCrypto) return; const leavesByRoot = new Map(); for (const m of messages) { const c = m.value && m.value.content; @@ -363,7 +363,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const key = lookupKey(rootId); const memberKeys = {}; for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue; await new Promise((resolve) => { @@ -378,8 +378,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { for (const r of raws) { if (idx.tomb.has(r.key)) continue; let c = r.c; - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (c.encryptedPayload && cryptoModel && tribesModel) { + const dec = await cryptoModel.decryptFromGroup(c, tribesModel); if (dec && !dec._undecryptable) c = dec; } list.push({ @@ -530,14 +530,14 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { }; const isPublicOpen = mType === "OPEN" && !tribeId; - const shouldEncryptStandalone = !tribeId && !isPublicOpen && tribeCrypto; + const shouldEncryptStandalone = !tribeId && !isPublicOpen && cryptoModel; let mapKey = null; let content = plainContent; if (tribeId) { - content = await encryptIfTribe(plainContent); + content = await encryptIfGroup(plainContent); } else if (shouldEncryptStandalone) { - mapKey = ownCrypto.generateTribeKey(); - content = tribeCrypto.encryptContent(plainContent, [mapKey], true); + mapKey = ownCrypto.generateKey(); + content = cryptoModel.encryptContent(plainContent, [mapKey], true); } const result = await new Promise((resolve, reject) => { @@ -548,7 +548,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { ownCrypto.setKey(result.key, mapKey, 1); try { const ssbKeys = require("../server/node_modules/ssb-keys"); - const boxedKey = tribeCrypto.boxKeyForMember(mapKey, userId, ssbKeys); + const boxedKey = cryptoModel.boxKeyForMember(mapKey, userId, ssbKeys); await new Promise((resolve) => { ssbClient.publish({ type: "tribe-keys", tribeId: result.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve()); }); @@ -556,8 +556,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (mType === "OPEN") { try { const pubCode = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex"); - const inviteSalt = tribeCrypto.generateInviteSalt(); - const ek = tribeCrypto.encryptForInvite(mapKey, pubCode, inviteSalt); + const inviteSalt = cryptoModel.generateInviteSalt(); + const ek = cryptoModel.encryptForInvite(mapKey, pubCode, inviteSalt); let updated = { type: "map", replaces: result.key, @@ -593,10 +593,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const oldMsg = await getMsg(ssbClient, tipId); if (!oldMsg) throw new Error("Map not found"); - const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content); + const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content); const tribeIdHint = isWrapped ? null : (oldMsg.content && oldMsg.content.tribeId); const oldDecrypted = (isWrapped || tribeIdHint) - ? await decryptIfTribe(oldMsg.content) + ? await decryptIfGroup(oldMsg.content) : decryptMapRoot(oldMsg.content, rootId); const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId; if (!oldDecrypted || (oldDecrypted.type && oldDecrypted.type !== "map" && !isWrapped && oldMsg.content?.type !== "map")) throw new Error("Map not found"); @@ -627,7 +627,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { }; if (effectiveTribeId) { - updated = await encryptIfTribe(updated); + updated = await encryptIfGroup(updated); } else if (mType !== "OPEN") { updated = encryptStandalone(updated, rootId); } @@ -647,7 +647,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tipId); if (!msg) throw new Error("Map not found"); - const decrypted = await decryptIfTribe(msg.content); + const decrypted = await decryptIfGroup(msg.content); if (!decrypted) throw new Error("Map not found"); if ((decrypted.author || msg.content.author) !== userId) throw new Error("Not the author"); @@ -673,7 +673,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const node = idx.nodes.get(tipId); if (!node) throw new Error("Map not found"); - const mapDecrypted = await decryptIfTribe(node.c); + const mapDecrypted = await decryptIfGroup(node.c); assertReadable(mapDecrypted, "Map"); const mapType = mapDecrypted.mapType || node.c.mapType || "SINGLE"; const mapAuthor = node.author; @@ -694,10 +694,10 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (image) content.image = image; if (node.c.tribeId) { - content = await encryptIfTribe(content); - } else if (tribeCrypto) { + content = await encryptIfGroup(content); + } else if (cryptoModel) { const mapKey = lookupKey(rootId); - if (mapKey) content = tribeCrypto.encryptContent(content, [mapKey], true); + if (mapKey) content = cryptoModel.encryptContent(content, [mapKey], true); } return new Promise((resolve, reject) => { @@ -711,8 +711,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, markerId); if (!msg || !msg.content) throw new Error("Marker not found"); let c = msg.content; - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (c.encryptedPayload && cryptoModel && tribesModel) { + const dec = await cryptoModel.decryptFromGroup(c, tribesModel); if (dec && !dec._undecryptable) c = dec; } if (c.type !== "mapMarker" && !c.encryptedPayload) throw new Error("Marker not found"); @@ -774,8 +774,8 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tip); if (!msg || msg.content?.type !== "map") throw new Error("Map not found"); let c = msg.content; - if (c.encryptedPayload && tribeCrypto && tribesModel) { - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (c.encryptedPayload && cryptoModel && tribesModel) { + const dec = await cryptoModel.decryptFromGroup(c, tribesModel); c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; } const markerList = markersFor(idx, tip, root); @@ -795,21 +795,21 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (map.author !== userId) throw new Error("Only the author can generate invites"); const code = crypto.randomBytes(INVITE_CODE_BYTES).toString("hex"); let invite = code; - if (tribeCrypto && !map.tribeId) { - const inviteSalt = tribeCrypto.generateInviteSalt(); + if (cryptoModel && !map.tribeId) { + const inviteSalt = cryptoModel.generateInviteSalt(); const ident = opts.public ? { code, public: true } : { ch: hashCode(code, inviteSalt) }; - const ekChain = tribeCrypto.encryptChainForInvite([map.rootId || map.key], code, inviteSalt); + const ekChain = cryptoModel.encryptChainForInvite([map.rootId || map.key], code, inviteSalt); const mapKey = ekChain ? null : lookupKey(map.rootId || map.key); if (ekChain) invite = { ...ident, ekChain, salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; - else if (mapKey) invite = { ...ident, ek: tribeCrypto.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; + else if (mapKey) invite = { ...ident, ek: cryptoModel.encryptForInvite(mapKey, code, inviteSalt), salt: inviteSalt, gen: lookupGen(map.rootId || map.key) || 1 }; } if (opts.public && typeof invite !== "object") invite = { code, public: true }; const tipId = await this.resolveCurrentId(mapId); const rootId = await this.resolveRootId(mapId); const oldMsg = await getMsg(ssbClient, tipId); - const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content); + const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content); const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId)) - ? await decryptIfTribe(oldMsg.content) + ? await decryptIfGroup(oldMsg.content) : decryptMapRoot(oldMsg.content, rootId); const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId; const invites = [...(Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []), invite]; @@ -831,7 +831,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { createdAt: oldDecrypted.createdAt, updatedAt: new Date().toISOString() }; - if (effectiveTribeId) updated = await encryptIfTribe(updated); + if (effectiveTribeId) updated = await encryptIfGroup(updated); else updated = encryptStandalone(updated, rootId); await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r))); const tomb1 = await tombFor(tipId, effectiveTribeId, userId); @@ -865,9 +865,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const tipId = await this.resolveCurrentId(mapId); const rootId = await this.resolveRootId(mapId); const oldMsg = await getMsg(ssbClient, tipId); - const isWrapped = tribeCrypto && tribeCrypto.isTribeMsg(oldMsg.content); + const isWrapped = cryptoModel && cryptoModel.isSealedMsg(oldMsg.content); const oldDecrypted = (isWrapped || (oldMsg.content && oldMsg.content.tribeId)) - ? await decryptIfTribe(oldMsg.content) + ? await decryptIfGroup(oldMsg.content) : decryptMapRoot(oldMsg.content, rootId); const effectiveTribeId = oldDecrypted && oldDecrypted.tribeId; const invites = (Array.isArray(oldDecrypted.invites) ? oldDecrypted.invites : []).filter(inv => !(typeof inv === "object" && inv.public === true)); @@ -889,7 +889,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { createdAt: oldDecrypted.createdAt, updatedAt: new Date().toISOString() }; - if (effectiveTribeId) updated = await encryptIfTribe(updated); + if (effectiveTribeId) updated = await encryptIfGroup(updated); else updated = encryptStandalone(updated, rootId); await new Promise((resolve, reject) => ssbClient.publish(updated, (e, r) => e ? reject(e) : resolve(r))); const tomb1 = await tombFor(tipId, effectiveTribeId, userId); @@ -920,13 +920,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const token = inviteToken(matchedInvite) || code; let mapKey = null; let chain = null; - if (tribeCrypto && typeof matchedInvite === "object") { + if (cryptoModel && typeof matchedInvite === "object") { if (matchedInvite.ekChain) { - chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3); + chain = cryptoModel.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3); if (Array.isArray(chain) && chain.length) mapKey = chain[0].key || (Array.isArray(chain[0].keys) ? chain[0].keys[0] : null); else chain = null; } else if (matchedInvite.ek) { - mapKey = tribeCrypto.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt); + mapKey = cryptoModel.decryptFromInvite(matchedInvite.ek, code, matchedInvite.salt); } } if (!isPublic) { @@ -939,9 +939,9 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { if (chain) { for (const entry of chain) { if (Array.isArray(entry.keys) && entry.keys.length) { - tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); } else if (entry.key) { - tribeCrypto.setKey(entry.rootId, entry.key, entry.gen || 1); + cryptoModel.setKey(entry.rootId, entry.key, entry.gen || 1); } } } else if (mapKey) { @@ -949,13 +949,13 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { } if (!mapKey && matched.contentEncrypted && !matched.tribeId) throw new Error("Invalid or expired invite code"); await new Promise((resolve, reject) => ssbClient.publish({ type: "mapMember", target: rootId, member: userId, on: true, createdAt: new Date().toISOString(), ...(isPublic ? {} : { code: token }), ...(mapKey ? { keyProof: keyProofFor(mapKey, userId) } : {}) }, (e, r) => e ? reject(e) : resolve(r))); - if (tribeCrypto && mapKey) { + if (cryptoModel && mapKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys"); const memberKeys = {}; - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(mapKey, userId, ssbKeys); } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(mapKey, userId, ssbKeys); } catch (_) {} if (matched.author && matched.author !== userId) { - try { memberKeys[matched.author] = tribeCrypto.boxKeyForMember(mapKey, matched.author, ssbKeys); } catch (_) {} + try { memberKeys[matched.author] = cryptoModel.boxKeyForMember(mapKey, matched.author, ssbKeys); } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -972,7 +972,7 @@ module.exports = ({ cooler, tribeCrypto, mapCrypto, tribesModel }) => { const map = await this.getMapById(mapId, userId); if (!map) throw new Error("Map not found"); if (Array.isArray(map.members) && map.members.includes(userId)) return map.rootId || map.key; - if (tribeCrypto && Array.isArray(map.invites)) { + if (cryptoModel && Array.isArray(map.invites)) { const pub = map.invites.find(inv => typeof inv === "object" && inv.public === true && inv.code && (inv.ek || inv.ekChain)); if (pub) return await this.joinByInvite(pub.code); } diff --git a/src/models/market_model.js b/src/models/market_model.js index 9bbf051..cdb1f31 100644 --- a/src/models/market_model.js +++ b/src/models/market_model.js @@ -57,7 +57,7 @@ const hasBidder = (poll, userId) => { return false } -module.exports = ({ cooler, tribeCrypto }) => { +module.exports = ({ cooler, cryptoModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open() @@ -196,9 +196,9 @@ module.exports = ({ cooler, tribeCrypto }) => { return new Promise((resolve, reject) => { ssbClient.publish(itemContent, (err, res) => { if (err) return reject(err) - if (res && res.key && tribeCrypto) { - const key = tribeCrypto.generateTribeKey() - tribeCrypto.setKey(res.key, key, 1) + if (res && res.key && cryptoModel) { + const key = cryptoModel.generateKey() + cryptoModel.setKey(res.key, key, 1) } resolve(res) }) @@ -353,7 +353,7 @@ module.exports = ({ cooler, tribeCrypto }) => { includesShipping: !!c.includesShipping, stock, deadline: c.deadline || null, - auctions_poll: (tribeCrypto && tribeCrypto.getKey(rootId)) ? poll : [], + auctions_poll: (cryptoModel && cryptoModel.getKey(rootId)) ? poll : [], mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", @@ -458,7 +458,7 @@ module.exports = ({ cooler, tribeCrypto }) => { includesShipping: !!c.includesShipping, stock, deadline: c.deadline, - auctions_poll: (tribeCrypto && tribeCrypto.getKey(rootId)) ? poll : [], + auctions_poll: (cryptoModel && cryptoModel.getKey(rootId)) ? poll : [], mapUrl: c.mapUrl || "", shopProductId: c.shopProductId || "", shopId: c.shopId || "", @@ -505,6 +505,42 @@ module.exports = ({ cooler, tribeCrypto }) => { return changed }, + async createOrder(itemId, details = {}) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getItemById(itemId) + if (!item) throw new Error("Item not found") + if (item.seller === me) throw new Error("You cannot buy your own item") + const content = { + type: "marketOrder", + itemId: item.rootId || item.id, + seller: item.seller, + title: String(item.title || ""), + price: String(item.price || ""), + deliveryAddress: String(details.deliveryAddress || ""), + contact: String(details.contact || ""), + notes: String(details.notes || ""), + createdAt: new Date().toISOString() + } + return new Promise((res, rej) => ssbClient.private.publish(content, [me, item.seller], (e, m) => e ? rej(e) : res(m))) + }, + + async listOrders(itemId) { + const ssbClient = await openSsb() + const me = ssbClient.id + const item = await this.getItemById(itemId) + if (!item) return [] + const rootId = item.rootId || item.id + const msgs = await readTyped(ssbClient, ["marketOrder"], { limit: logLimit, withPrivate: true }) + return msgs + .filter(m => { + const c = m && m.value && m.value.content + return m.value.private === true && c && c.type === "marketOrder" && c.itemId === rootId && c.seller === item.seller && (item.seller === me || m.value.author === me) + }) + .map(m => ({ id: m.key, buyer: m.value.author, ts: m.value.timestamp || 0, deliveryAddress: m.value.content.deliveryAddress || "", contact: m.value.content.contact || "", notes: m.value.content.notes || "" })) + .sort((a, b) => b.ts - a.ts) + }, + async getItemByShopProductId(shopProductId) { if (!shopProductId) return null const items = await this.listAllItems("all") diff --git a/src/models/mentions_model.js b/src/models/mentions_model.js index 85a9160..97e4bcb 100644 --- a/src/models/mentions_model.js +++ b/src/models/mentions_model.js @@ -1,6 +1,7 @@ const fs = require('fs'); const pull = require('../server/node_modules/pull-stream'); const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { isComment } = require('./comments_model'); const SEEN_FILE = 'oasis-mentions-seen'; @@ -133,7 +134,7 @@ module.exports = ({ cooler }) => { id: m.key, key: m.key, author: v.author, - type: c.type, + type: isComment(c) ? 'comment' : c.type, title: titleOf(c), text: snippet(c), content: c, diff --git a/src/models/opinions_model.js b/src/models/opinions_model.js index 90e7236..870cec6 100644 --- a/src/models/opinions_model.js +++ b/src/models/opinions_model.js @@ -20,20 +20,30 @@ module.exports = ({ cooler }) => { return ssb; }; - const hasBlob = async (ssbClient, url) => { - return new Promise(resolve => { - ssbClient.blobs.has(url, (err, has) => { - resolve(!err && has); - }); - }); - }; - const validTypes = [ 'bookmark', 'votes', 'transfer', 'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file', 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', - 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute', 'wikiPage', 'map', 'industryBlueprint', 'job' ]; + const OPINION_TYPES = [...new Set(Object.values(categories.messageTypes))]; + + const tallyOf = async (versionIds) => { + const want = new Set((versionIds || []).filter(Boolean)); + const agg = { opinions: {}, opinions_inhabitants: [] }; + if (!want.size) return agg; + const ssbClient = await openSsb(); + const msgs = await readTyped(ssbClient, [...OPINION_TYPES, 'tombstone'], { limit: logLimit }); + const gone = buildValidatedTombstoneSet(msgs); + for (const m of msgs) { + const c = m.value?.content; + if (!c || !OPINION_TYPES.includes(c.type) || !want.has(c.target) || gone.has(m.key)) continue; + if (agg.opinions_inhabitants.includes(m.value.author)) continue; + agg.opinions_inhabitants.push(m.value.author); + if (categories.includes(c.category)) agg.opinions[c.category] = (agg.opinions[c.category] || 0) + 1; + } + return agg; + }; const getPreview = c => { if (c.type === 'bookmark' && c.bookmark) return `๐Ÿ”– ${c.bookmark}`; @@ -55,6 +65,7 @@ module.exports = ({ cooler }) => { const otype = OPINION_MSG_TYPE[type]; if (!otype) throw new Error("Voting not allowed on this content type."); if (msg.content.opinions_inhabitants?.includes(userId)) throw new Error("Already voted."); + if ((await tallyOf([contentId])).opinions_inhabitants.includes(userId)) throw new Error("Already voted."); const content = { type: otype, target: contentId, category, createdAt: new Date().toISOString() }; return new Promise((resolve, reject) => ssbClient.publish(content, (err, result) => err ? reject(err) : resolve(result)) @@ -77,7 +88,7 @@ module.exports = ({ cooler }) => { opinionMsgs.push({ target: c.target, author: msg.value.author, category: c.category }); continue; } - if (c.opinions && !['task', 'event', 'report'].includes(c.type)) { + if ((c.opinions || categories.opinable(c)) && !['task', 'event', 'report'].includes(c.type)) { byId.set(key, { key, value: { @@ -136,23 +147,6 @@ module.exports = ({ cooler }) => { if (c.encryptedPayload) return false; return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; }); - const blobTypes = ['document', 'image', 'audio', 'video']; - const blobCheckCache = new Map(); - - filtered = await Promise.all( - filtered.map(async m => { - const c = m.value.content; - if (blobTypes.includes(c.type) && c.url) { - if (!blobCheckCache.has(c.url)) { - const valid = await hasBlob(ssbClient, c.url); - blobCheckCache.set(c.url, valid); - } - if (!blobCheckCache.get(c.url)) return null; - } - return m; - }) - ); - filtered = filtered.filter(Boolean); const signatureOf = (m) => { const c = m.value?.content || {}; @@ -267,6 +261,7 @@ module.exports = ({ cooler }) => { }; return { + tallyOf, createVote, listOpinions, getMessageById, diff --git a/src/models/pads_model.js b/src/models/pads_model.js index 3a9c310..929222c 100644 --- a/src/models/pads_model.js +++ b/src/models/pads_model.js @@ -19,18 +19,18 @@ const INVITE_SALT = "SolarNET.HuB-pads" const INVITE_BYTES = 16 const MEMBER_COLORS = ["#e74c3c","#3498db","#2ecc71","#f39c12","#9b59b6","#1abc9c","#e67e22","#e91e63","#00bcd4","#8bc34a"] -module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) => { +module.exports = ({ cooler, cipherModel, cryptoModel, padCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } - const ownCrypto = padCrypto || tribeCrypto - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const ownCrypto = padCrypto || cryptoModel + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a - return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + return (cryptoModel && cryptoModel.getKeys(rid)) || [] } - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0) let keyringPath = null let migratedToTribeCrypto = false @@ -60,7 +60,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (ownCrypto) return lookupKey(rootId) try { return JSON.parse(fs.readFileSync(getLegacyKeyringPath(), "utf8"))[rootId] || null } catch (_) { return null } } - const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null) const inviteMatches = (inv, token) => { if (!token) return false if (typeof inv === "string") return inv === token @@ -71,7 +71,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } const inviteToken = (inv) => (inv && typeof inv === "object" && typeof inv.ch === "string") ? inv.ch : (typeof inv === "string" ? inv : (inv && inv.code) || "") const keyProofFor = (keyHex, id) => { try { return crypto.createHmac("sha256", Buffer.from(keyHex, "hex")).update(String(id), "utf8").digest("hex") } catch (_) { return null } } - const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(tribeCrypto && typeof tribeCrypto.getKeys === "function" ? tribeCrypto.getKeys(rootId) : []) + const padKeys = (rootId) => (ownCrypto && typeof ownCrypto.getKeys === "function" ? ownCrypto.getKeys(rootId) : []).concat(cryptoModel && typeof cryptoModel.getKeys === "function" ? cryptoModel.getKeys(rootId) : []) const setPadKey = (rootId, keyHex) => { migrateLegacyKeyring() if (ownCrypto) { ownCrypto.setKey(rootId, keyHex, 1); return } @@ -113,10 +113,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } const getTribeKeysFor = async (tribeId) => { - if (!tribeCrypto || !tribesModel || !tribeId) return [] + if (!cryptoModel || !tribesModel || !tribeId) return [] try { const rootId = await tribesModel.getRootId(tribeId) - const keys = tribeCrypto.getKeys(rootId) || [] + const keys = cryptoModel.getKeys(rootId) || [] return keys } catch (_) { return [] } } @@ -137,7 +137,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } const ensureMemberKeys = async (ssbClient, messages, items) => { - if (!tribeCrypto || !ownCrypto) return + if (!cryptoModel || !ownCrypto) return const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content @@ -161,7 +161,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const key = lookupKey(rootId) const memberKeys = {} for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue await new Promise((resolve) => { @@ -199,7 +199,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const generateInviteSalt = () => crypto.randomBytes(16).toString("hex") const rotatePadKey = async (rootId, remainingMembers) => { - if (!ownCrypto || !tribeCrypto || !rootId) return + if (!ownCrypto || !cryptoModel || !rootId) return const existing = getPadKey(rootId) if (!existing) return const newKey = crypto.randomBytes(32).toString("hex") @@ -208,7 +208,7 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -537,10 +537,10 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) if (err) return reject(err) if (!usesTribeKey) { setPadKey(msg.key, keyHex) - if (tribeCrypto) { + if (cryptoModel) { try { const ssbKeys = require("../server/node_modules/ssb-keys") - const boxedKey = tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys) + const boxedKey = cryptoModel.boxKeyForMember(keyHex, userId, ssbKeys) ssbClient.publish({ type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: boxedKey } }, () => resolve(msg)) return } catch (_) {} @@ -808,13 +808,13 @@ module.exports = ({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel }) } if (padKey) setPadKey(resolvedRootId, padKey) await this.addMemberToPad(matchedPad.rootId, userId, isPublic ? "" : token) - if (tribeCrypto && padKey && resolvedRootId) { + if (cryptoModel && padKey && resolvedRootId) { try { const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(padKey, userId, ssbKeys) } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(padKey, userId, ssbKeys) } catch (_) {} if (matchedPad.author && matchedPad.author !== userId) { - try { memberKeys[matchedPad.author] = tribeCrypto.boxKeyForMember(padKey, matchedPad.author, ssbKeys) } catch (_) {} + try { memberKeys[matchedPad.author] = cryptoModel.boxKeyForMember(padKey, matchedPad.author, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { diff --git a/src/models/parliament_model.js b/src/models/parliament_model.js index ec18860..7664615 100644 --- a/src/models/parliament_model.js +++ b/src/models/parliament_model.js @@ -259,7 +259,7 @@ module.exports = ({ cooler, services = {} }) => { if ((n.c.replaces && nodes.has(n.c.replaces)) || !settle(n.key)) continue; const tip = tipOf(n.key); if (tomb.has(tip)) continue; - items.push({ ...nodes.get(tip).c, id: tip }); + items.push({ ...nodes.get(tip).c, id: tip, rootId: n.key }); } const resolve = (k) => (nodes.has(k) && settle(k) ? tipOf(rootOf(k)) : null); const authorOf = (k) => (nodes.has(k) ? nodes.get(k).author : null); diff --git a/src/models/podcasts_model.js b/src/models/podcasts_model.js index 5d0d6a7..d6330a2 100644 --- a/src/models/podcasts_model.js +++ b/src/models/podcasts_model.js @@ -131,7 +131,7 @@ module.exports = ({ cooler }) => { channels.set(root, { id: root, rootId: root, tipId: tip.key, title: safeText(c.title), description: safeText(c.description), category: normalizeCategory(c.category), - cover: parseMedia(c.cover), tags: normalizeTags(c.tags), + cover: parseMedia(c.cover), tags: normalizeTags(c.tags), license: typeof c.license === 'string' ? c.license : '', author: first.author, createdAt: first.c.createdAt || new Date(first.ts).toISOString(), updatedAt: c.updatedAt || new Date(tip.ts).toISOString(), ts: tip.ts, episodes: eps, episodeCount: eps.length, playCount, opinionCount, @@ -163,10 +163,7 @@ module.exports = ({ cooler }) => { }; return { - CATEGORIES, - parseMedia, - - async createChannel({ title, description, category, cover, tags }) { + async createChannel({ title, description, category, cover, tags, license }) { const ssbClient = await openSsb(); const t = safeText(title); if (!t) throw new Error('Title is required'); @@ -174,7 +171,7 @@ module.exports = ({ cooler }) => { const media = parseMedia(cover); return publish(ssbClient, { type: CHANNEL_TYPE, title: t, description: safeText(description), category: normalizeCategory(category), - ...(media ? { cover: media } : {}), tags: normalizeTags(tags), author: ssbClient.id, createdAt: now, updatedAt: now + ...(media ? { cover: media } : {}), tags: normalizeTags(tags), ...(license ? { license } : {}), author: ssbClient.id, createdAt: now, updatedAt: now }); }, @@ -183,6 +180,7 @@ module.exports = ({ cooler }) => { const ch = findChannel(idx, id); if (!ch) throw new Error('Podcast not found'); if (ch.author !== ssbClient.id) throw new Error('Only the author can update this podcast'); + categories.assertMediaKept(ch, data.cover !== undefined && !!data.cover); const media = data.cover !== undefined && data.cover ? parseMedia(data.cover) : ch.cover; const res = await publish(ssbClient, { type: CHANNEL_TYPE, @@ -191,6 +189,7 @@ module.exports = ({ cooler }) => { category: data.category !== undefined ? normalizeCategory(data.category) : ch.category, ...(media ? { cover: media } : {}), tags: data.tags !== undefined ? normalizeTags(data.tags) : ch.tags, + ...((data.license !== undefined ? data.license : ch.license) ? { license: data.license !== undefined ? data.license : ch.license } : {}), author: ssbClient.id, createdAt: ch.createdAt, updatedAt: new Date().toISOString(), replaces: ch.tipId }); return { key: res.key, rootId: ch.id }; @@ -228,6 +227,7 @@ module.exports = ({ cooler }) => { const ep = findEpisode(idx, id); if (!ep) throw new Error('Episode not found'); if (ep.author !== ssbClient.id) throw new Error('Only the author can update this episode'); + categories.assertMediaKept(ep, !!data.media); const m = data.media ? parseMedia(data.media) : ep.media; const res = await publish(ssbClient, { type: EPISODE_TYPE, channel: ep.channelId, number: data.number !== undefined && num(data.number) ? num(data.number) : ep.number, diff --git a/src/models/polls_model.js b/src/models/polls_model.js index c61a5c2..da05d0a 100644 --- a/src/models/polls_model.js +++ b/src/models/polls_model.js @@ -3,6 +3,7 @@ const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { readTyped, memoIndex } = require('./typed_log'); const { versionGroups } = require('../backend/dedupe'); +const opinionRules = require('../backend/opinion_categories'); const logLimit = getConfig().ssbLogStream?.limit || 1000; @@ -35,7 +36,7 @@ const normalizeTags = (raw) => { return list.map(t => clean(t)).filter(Boolean).slice(0, 20); }; -module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = null }) => { +module.exports = ({ cooler, isPublic = false, cryptoModel = null, chatsModel = null }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -44,10 +45,10 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n const getAllMessages = async (ssbClient) => readTyped(ssbClient, POLLS_TYPES, { limit: logLimit }); const decryptField = (value, keys) => { - if (!tribeCrypto || typeof value !== 'string' || !value) return null; + if (!cryptoModel || typeof value !== 'string' || !value) return null; for (const k of keys) { try { - const out = tribeCrypto.decryptWithKey(value, k); + const out = cryptoModel.decryptWithKey(value, k); if (typeof out === 'string' && out) return out; } catch (_) {} } @@ -312,9 +313,9 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n ...(data.houseKey ? { houseKey: String(data.houseKey) } : {}) }; const key = await scopeKey({ chatId: data.chatId, tribeId: data.tribeId }); - if (key && tribeCrypto) { - content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); - content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + if (key && cryptoModel) { + content.encryptedQuestion = cryptoModel.encryptWithKey(question, key); + content.encryptedOptions = cryptoModel.encryptWithKey(JSON.stringify(options), key); } else { content.question = question; content.options = options; @@ -338,6 +339,7 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n if (!Number.isFinite(t)) throw new Error('Invalid deadline'); } const question = data.question !== undefined ? clean(data.question) : poll.question; + opinionRules.assertMediaKept(poll, question !== poll.question || JSON.stringify(options) !== JSON.stringify(poll.options)); const content = { type: POLL_TYPE, replaces: poll.tipId, @@ -356,9 +358,9 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n ...(poll.houseKey ? { houseKey: poll.houseKey } : {}) }; const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); - if (key && tribeCrypto) { - content.encryptedQuestion = tribeCrypto.encryptWithKey(question, key); - content.encryptedOptions = tribeCrypto.encryptWithKey(JSON.stringify(options), key); + if (key && cryptoModel) { + content.encryptedQuestion = cryptoModel.encryptWithKey(question, key); + content.encryptedOptions = cryptoModel.encryptWithKey(JSON.stringify(options), key); } else { content.question = question; content.options = options; @@ -386,7 +388,7 @@ module.exports = ({ cooler, isPublic = false, tribeCrypto = null, chatsModel = n ...(poll.chatId ? { chatId: poll.chatId } : {}) }; const key = await scopeKey({ chatId: poll.chatId, tribeId: poll.tribeId }); - if (key && tribeCrypto) content.encryptedChoices = tribeCrypto.encryptWithKey(JSON.stringify(unique), key); + if (key && cryptoModel) content.encryptedChoices = cryptoModel.encryptWithKey(JSON.stringify(unique), key); else content.choices = unique; return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); }, diff --git a/src/models/projects_model.js b/src/models/projects_model.js index 76d3b3a..3fc4ade 100644 --- a/src/models/projects_model.js +++ b/src/models/projects_model.js @@ -137,7 +137,7 @@ module.exports = ({ cooler }) => { const groupKeys = [] { let x = root, g = 0; groupKeys.push(x); while (naiveNext.has(x) && g++ < 100000) { x = naiveNext.get(x); groupKeys.push(x) } } - const project = { ...best.c, id: tip } + const project = { ...best.c, id: tip, rootId: root } const followers = new Set() const opinions = {} diff --git a/src/models/reports_model.js b/src/models/reports_model.js index 39631d9..2378073 100644 --- a/src/models/reports_model.js +++ b/src/models/reports_model.js @@ -13,38 +13,11 @@ const ensureArray = (v) => Array.isArray(v) ? v.filter(Boolean) : []; const trimStr = (v) => String(v || '').trim(); -const normalizeTemplate = (category, tpl) => { - const cat = normU(category); +const LEGACY_TEMPLATE_KEYS = ['problemStatement', 'userStory', 'acceptanceCriteria', 'stepsToReproduce', 'expectedBehavior', 'actualBehavior', 'environment', 'whatHappened', 'reportedUser', 'contentLocation', 'whyInappropriate', 'evidenceLinks']; + +const withLegacyText = (description, tpl) => { const t = tpl && typeof tpl === 'object' ? tpl : {}; - - const pick = (keys) => { - const out = {}; - for (const k of keys) { - const val = trimStr(t[k]); - if (val) out[k] = val; - } - return out; - }; - - if (cat === 'BUGS') { - const out = pick(['stepsToReproduce', 'expectedBehavior', 'actualBehavior', 'environment', 'reproduceRate']); - if (out.reproduceRate) out.reproduceRate = normU(out.reproduceRate); - return out; - } - - if (cat === 'FEATURES') { - return pick(['problemStatement', 'userStory', 'acceptanceCriteria']); - } - - if (cat === 'ABUSE') { - return pick(['whatHappened', 'reportedUser', 'evidenceLinks']); - } - - if (cat === 'CONTENT') { - return pick(['contentLocation', 'whyInappropriate', 'evidenceLinks']); - } - - return {}; + return [trimStr(description), ...LEGACY_TEMPLATE_KEYS.map(k => trimStr(t[k]))].filter(Boolean).join('\n\n'); }; module.exports = ({ cooler }) => { @@ -113,6 +86,7 @@ module.exports = ({ cooler }) => { for (const cf of (confirmsByRoot.get(root) || [])) confirmSet.add(cf.author); return { + root, contentTip, contentNode, opinions, @@ -126,7 +100,7 @@ module.exports = ({ cooler }) => { }; const buildReport = (node, agg) => { - const c = node.c || {}; + const { template, ...c } = node.c || {}; const cat = normU(c.category); const opinions = agg ? agg.opinions : (c.opinions || {}); const voters = agg ? agg.opinions_inhabitants : ensureArray(c.opinions_inhabitants); @@ -134,6 +108,8 @@ module.exports = ({ cooler }) => { return { id: node.key, ...c, + description: withLegacyText(c.description, template), + rootId: agg && agg.root ? agg.root : node.key, category: cat, status: normalizeStatus(c.status || 'OPEN'), images: normalizeImages(c.images && c.images.length ? c.images : c.image), @@ -141,14 +117,13 @@ module.exports = ({ cooler }) => { severity: normalizeSeverity(c.severity) || 'low', confirmations, tags: ensureArray(c.tags), - template: normalizeTemplate(cat, c.template || {}), opinions, opinions_inhabitants: voters }; }; return { - async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}, media = {}) { + async createReport(title, description, category, image, tagsRaw = [], severity = 'low', media = {}) { const ssb = await openSsb(); const userId = ssb.id; @@ -175,7 +150,6 @@ module.exports = ({ cooler }) => { confirmations: [], severity: normalizeSeverity(severity) || 'low', status: 'OPEN', - template: normalizeTemplate(cat, template), opinions: {}, opinions_inhabitants: [] }; @@ -193,7 +167,8 @@ module.exports = ({ cooler }) => { const agg = idx.resolveGroup(root); if (!agg || agg.tombstoned) throw new Error('Report not found'); - const report = { content: agg.contentNode.c }; + const { template: legacyTemplate, ...previous } = agg.contentNode.c || {}; + const report = { content: previous }; const tipId = agg.contentTip; if (agg.contentNode.author !== userId) throw new Error('Not the author'); @@ -226,11 +201,9 @@ module.exports = ({ cooler }) => { const confirmations = ensureArray(agg.confirmations); - const baseTemplate = Object.prototype.hasOwnProperty.call(updatedContent, 'template') - ? updatedContent.template - : (report.content.template || {}); - - const nextTemplate = normalizeTemplate(nextCategory, baseTemplate); + const nextDescription = Object.prototype.hasOwnProperty.call(updatedContent, 'description') + ? trimStr(updatedContent.description) + : withLegacyText(report.content.description, legacyTemplate); const updated = { ...report.content, @@ -247,7 +220,7 @@ module.exports = ({ cooler }) => { severity: nextSeverity, status: nextStatus, category: nextCategory, - template: nextTemplate, + description: nextDescription, updatedAt: new Date().toISOString(), author: report.content.author }; diff --git a/src/models/rooms_model.js b/src/models/rooms_model.js index 2de9adb..29b2619 100644 --- a/src/models/rooms_model.js +++ b/src/models/rooms_model.js @@ -35,21 +35,21 @@ const rpcValue = (fn, ...args) => new Promise((resolve) => { } catch (_) { resolve(null) } }) -module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, roomCrypto, tribesModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } const phone = async () => { const s = await openSsb(); return s && s.phone ? s.phone : null } - const ownCrypto = roomCrypto || tribeCrypto - const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (tribeCrypto && tribeCrypto.getKey(rid)) || null + const ownCrypto = roomCrypto || cryptoModel + const lookupKey = (rid) => (ownCrypto && ownCrypto.getKey(rid)) || (cryptoModel && cryptoModel.getKey(rid)) || null const lookupKeys = (rid) => { const a = (ownCrypto && ownCrypto.getKeys(rid)) || [] if (a.length) return a - return (tribeCrypto && tribeCrypto.getKeys(rid)) || [] + return (cryptoModel && cryptoModel.getKeys(rid)) || [] } - const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (tribeCrypto && tribeCrypto.getGen(rid)) || 0) + const lookupGen = (rid) => ((ownCrypto && ownCrypto.getGen(rid)) || (cryptoModel && cryptoModel.getGen(rid)) || 0) const setRoomKey = (rootId, keyHex) => { if (ownCrypto) ownCrypto.setKey(rootId, keyHex, 1) } - const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (tribeCrypto ? tribeCrypto.hashInviteCode(code, salt) : null) + const hashCode = (code, salt) => (ownCrypto && typeof ownCrypto.hashInviteCode === "function") ? ownCrypto.hashInviteCode(code, salt) : (cryptoModel ? cryptoModel.hashInviteCode(code, salt) : null) const inviteMatches = (inv, token) => { if (!token) return false if (typeof inv === "string") return inv === token @@ -87,10 +87,10 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { } const getTribeKeysFor = async (tribeId) => { - if (!tribeCrypto || !tribesModel || !tribeId) return [] + if (!cryptoModel || !tribesModel || !tribeId) return [] try { const rootId = await tribesModel.getRootId(tribeId) - return tribeCrypto.getKeys(rootId) || [] + return cryptoModel.getKeys(rootId) || [] } catch (_) { return [] } } @@ -143,7 +143,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { const generateInviteSalt = () => crypto.randomBytes(16).toString("hex") const rotateRoomKey = async (rootId, remainingMembers) => { - if (!ownCrypto || !tribeCrypto || !rootId) return + if (!ownCrypto || !cryptoModel || !rootId) return if (!lookupKey(rootId)) return const newKey = crypto.randomBytes(32).toString("hex") const newGen = ownCrypto.addNewKey(rootId, newKey) @@ -151,7 +151,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} for (const m of new Set([ssbClient.id, ...(Array.isArray(remainingMembers) ? remainingMembers : [])])) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(newKey, m, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) { await new Promise((resolve) => { @@ -163,7 +163,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { const readAll = async (ssbClient) => readTyped(ssbClient, ROOM_TYPES, { limit: logLimit }) const ensureMemberKeys = async (ssbClient, messages, rooms) => { - if (!tribeCrypto || !ownCrypto) return + if (!cryptoModel || !ownCrypto) return const leavesByRoot = new Map() for (const m of messages) { const c = m.value && m.value.content @@ -187,7 +187,7 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { const key = lookupKey(rootId) const memberKeys = {} for (const m of plan.missing) { - try { memberKeys[m] = tribeCrypto.boxKeyForMember(key, m, ssbKeys) } catch (_) {} + try { memberKeys[m] = cryptoModel.boxKeyForMember(key, m, ssbKeys) } catch (_) {} } if (!Object.keys(memberKeys).length) continue await asP(ssbClient.publish, { type: "tribe-keys", tribeId: rootId, generation: lookupGen(rootId) || 1, memberKeys }) @@ -524,10 +524,10 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { }) if (!usesTribeKey) { setRoomKey(msg.key, keyHex) - if (tribeCrypto) { + if (cryptoModel) { try { const ssbKeys = require("../server/node_modules/ssb-keys") - await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: tribeCrypto.boxKeyForMember(keyHex, userId, ssbKeys) } }) + await asP(ssbClient.publish, { type: "tribe-keys", tribeId: msg.key, generation: 1, memberKeys: { [userId]: cryptoModel.boxKeyForMember(keyHex, userId, ssbKeys) } }) } catch (_) {} } } @@ -714,13 +714,13 @@ module.exports = ({ cooler, tribeCrypto, roomCrypto, tribesModel }) => { if (roomKey) setRoomKey(resolvedRootId, roomKey) } await this.addMemberToRoom(matchedRoom.rootId, userId, isPublic ? "" : token) - if (tribeCrypto && roomKey) { + if (cryptoModel && roomKey) { try { const ssbKeys = require("../server/node_modules/ssb-keys") const memberKeys = {} - try { memberKeys[userId] = tribeCrypto.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {} + try { memberKeys[userId] = cryptoModel.boxKeyForMember(roomKey, userId, ssbKeys) } catch (_) {} if (matchedRoom.author && matchedRoom.author !== userId) { - try { memberKeys[matchedRoom.author] = tribeCrypto.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {} + try { memberKeys[matchedRoom.author] = cryptoModel.boxKeyForMember(roomKey, matchedRoom.author, ssbKeys) } catch (_) {} } if (Object.keys(memberKeys).length) await asP(ssbClient.publish, { type: "tribe-keys", tribeId: resolvedRootId, generation: 1, memberKeys }) } catch (_) {} diff --git a/src/models/school_model.js b/src/models/school_model.js index 7e63678..8a2f551 100644 --- a/src/models/school_model.js +++ b/src/models/school_model.js @@ -386,7 +386,7 @@ module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => { if (!schoolCrypto) return null let key = schoolCrypto.getKey(rootId) if (key) return key - key = schoolCrypto.generateTribeKey() + key = schoolCrypto.generateKey() schoolCrypto.setKey(rootId, key, 1) await publishKeyGrant(ssbClient, rootId, key, [ssbClient.id]) return key @@ -441,7 +441,7 @@ module.exports = ({ cooler, transfersModel, schoolCrypto, chatsModel }) => { let toPublish = content let courseKey = null if (content.visibility === "INVITE" && schoolCrypto) { - courseKey = schoolCrypto.generateTribeKey() + courseKey = schoolCrypto.generateKey() toPublish = schoolCrypto.encryptContent(content, [courseKey], true) } const published = await new Promise((res, rej) => ssbClient.publish(toPublish, (e, m) => e ? rej(e) : res(m))) diff --git a/src/models/search_model.js b/src/models/search_model.js index 1849c42..692d4b2 100644 --- a/src/models/search_model.js +++ b/src/models/search_model.js @@ -7,7 +7,7 @@ const longText = require('../backend/long_text'); const { versionGroups, versionEntry } = require('../backend/dedupe'); const logLimit = getConfig().ssbLogStream?.limit || 1000; -module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { +module.exports = ({ cooler, padsModel, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); @@ -17,13 +17,13 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { const STANDALONE_ENCRYPTED_TYPES = new Set(['chat', 'pad', 'map', 'calendar']); const tryDecryptStandalone = (content) => { - if (!tribeCrypto || !content || !content.encryptedPayload) return null; + if (!cryptoModel || !content || !content.encryptedPayload) return null; const rootCandidates = [content.calendarId, content.chatId, content.padId, content.mapId, content.roomId, content.parentId, content.dateId, content.rootId].filter(Boolean); for (const cand of rootCandidates) { - const keys = tribeCrypto.getKeys(cand); + const keys = cryptoModel.getKeys(cand); if (!keys || !keys.length) continue; try { - const dec = tribeCrypto.decryptContent(content, keys.map(k => [k])); + const dec = cryptoModel.decryptContent(content, keys.map(k => [k])); if (dec && !dec._undecryptable) return dec; } catch (_) {} } @@ -31,9 +31,9 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { }; const tryDecryptTribe = async (content) => { - if (!tribeCrypto || !tribesModel || !content || !content.encryptedPayload || !content.tribeId) return null; + if (!cryptoModel || !tribesModel || !content || !content.encryptedPayload || !content.tribeId) return null; try { - const dec = await tribeCrypto.decryptFromTribe(content, tribesModel); + const dec = await cryptoModel.decryptFromGroup(content, tribesModel); if (dec && !dec._undecryptable) return dec; } catch (_) {} return null; @@ -74,7 +74,7 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { case 'market': return [content?.item_type, content?.title, content?.description, content?.price, ...(content?.tags || []), content?.status, content?.item_status, content?.deadline, content?.includesShipping, content?.seller, content?.image, content?.auctions_poll, content?.stock]; case 'bookmark': - return [content?.author, content?.url, ...(content?.tags || []), content?.description, content?.category, content?.lastVisit]; + return [content?.author, content?.title, content?.url, ...(content?.tags || []), content?.description, content?.category, content?.lastVisit]; case 'task': return [content?.title, content?.description, content?.startTime, content?.endTime, content?.priority, content?.location, ...(content?.tags || []), content?.isPublic, content?.assignees?.length, content?.status, content?.author]; case 'report': @@ -395,10 +395,10 @@ module.exports = ({ cooler, padsModel, tribeCrypto, tribesModel }) => { let dec = null; if (c.tribeId) dec = await tryDecryptTribe(c); if (!dec && STANDALONE_ENCRYPTED_TYPES.has(c.type)) { - const keys = tribeCrypto && tribeCrypto.getKeys ? tribeCrypto.getKeys(k) : []; + const keys = cryptoModel && cryptoModel.getKeys ? cryptoModel.getKeys(k) : []; if (keys && keys.length) { try { - const out = tribeCrypto.decryptContent(c, keys.map(kk => [kk])); + const out = cryptoModel.decryptContent(c, keys.map(kk => [kk])); if (out && !out._undecryptable) dec = out; } catch (_) {} } diff --git a/src/models/shops_model.js b/src/models/shops_model.js index 54f79ae..639b56a 100644 --- a/src/models/shops_model.js +++ b/src/models/shops_model.js @@ -16,7 +16,7 @@ const normalizeTags = (raw) => { } const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0) -module.exports = ({ cooler, tribeCrypto }) => { +module.exports = ({ cooler, cryptoModel }) => { let ssb const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb } @@ -106,8 +106,8 @@ module.exports = ({ cooler, tribeCrypto }) => { const decryptBuyers = (val, key) => { if (Array.isArray(val)) return val - if (typeof val === 'string' && tribeCrypto && key) { - try { return JSON.parse(tribeCrypto.decryptWithKey(val, key)) } catch {} + if (typeof val === 'string' && cryptoModel && key) { + try { return JSON.parse(cryptoModel.decryptWithKey(val, key)) } catch {} } return [] } @@ -116,25 +116,25 @@ module.exports = ({ cooler, tribeCrypto }) => { const productShopId = (c) => (c && (c.shopId || c.tribeId)) || "" const keysForRoot = (rootId) => { - if (!tribeCrypto || !rootId) return [] - const ks = (tribeCrypto.getKeys && tribeCrypto.getKeys(rootId)) || [] + if (!cryptoModel || !rootId) return [] + const ks = (cryptoModel.getKeys && cryptoModel.getKeys(rootId)) || [] if (ks.length) return ks - const k = tribeCrypto.getKey ? tribeCrypto.getKey(rootId) : null + const k = cryptoModel.getKey ? cryptoModel.getKey(rootId) : null return k ? [k] : [] } const decryptShopContent = (c, keyRootId) => { if (!isEncrypted(c)) return c - if (!tribeCrypto) return { ...c, _undecryptable: true } + if (!cryptoModel) return { ...c, _undecryptable: true } const keys = keysForRoot(keyRootId) if (!keys.length) return { ...c, _undecryptable: true } - return tribeCrypto.decryptContent(c, keys.map(k => [k])) + return cryptoModel.decryptContent(c, keys.map(k => [k])) } const encryptForShop = (plainContent, shopRootId) => { const keys = keysForRoot(shopRootId) if (!keys.length) throw new Error("Missing shop key") - return tribeCrypto.encryptContent(plainContent, [keys[0]], true) + return cryptoModel.encryptContent(plainContent, [keys[0]], true) } const buildIndex = (messages) => { @@ -243,7 +243,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const undecryptable = !!c._undecryptable const agg = idx ? idx.aggregateFor(rootId, c) : { opinions: c.opinions || {}, opinions_inhabitants: safeArr(c.opinions_inhabitants) } const sellerStock = Number(c.stock) || 0 - const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, tribeCrypto ? tribeCrypto.getKey(rootId) : null) + const sellerBuyers = encrypted ? safeArr(c.buyers) : decryptBuyers(c.buyers, cryptoModel ? cryptoModel.getKey(rootId) : null) const buyers = Array.from(new Set(sellerBuyers.concat(idx ? idx.purchaseAuthorsFor(rootId) : []))) return { key: node.key, @@ -324,14 +324,14 @@ module.exports = ({ cooler, tribeCrypto }) => { opinions_inhabitants: [] } - const wantsEncryption = vis === "CLOSED" && !!tribeCrypto + const wantsEncryption = vis === "CLOSED" && !!cryptoModel return new Promise((resolve, reject) => { if (wantsEncryption) { - const shopKey = tribeCrypto.generateTribeKey() - const encrypted = tribeCrypto.encryptContent(content, [shopKey], true) + const shopKey = cryptoModel.generateKey() + const encrypted = cryptoModel.encryptContent(content, [shopKey], true) ssbClient.publish(encrypted, (err, msg) => { if (err) return reject(err) - if (msg && msg.key) tribeCrypto.setKey(msg.key, shopKey, 1) + if (msg && msg.key) cryptoModel.setKey(msg.key, shopKey, 1) resolve(msg) }) } else { @@ -500,14 +500,14 @@ module.exports = ({ cooler, tribeCrypto }) => { return new Promise((resolve, reject) => { if (e2e) { let enc - try { enc = tribeCrypto.encryptContent(content, [shopKeys[0]], true) } catch (e) { return reject(e) } + try { enc = cryptoModel.encryptContent(content, [shopKeys[0]], true) } catch (e) { return reject(e) } ssbClient.publish(enc, (err, msg) => err ? reject(err) : resolve(msg)) } else { ssbClient.publish(content, (err, msg) => { if (err) return reject(err) - if (msg && msg.key && tribeCrypto) { - const key = tribeCrypto.generateTribeKey() - tribeCrypto.setKey(msg.key, key, 1) + if (msg && msg.key && cryptoModel) { + const key = cryptoModel.generateKey() + cryptoModel.setKey(msg.key, key, 1) } resolve(msg) }) @@ -846,7 +846,7 @@ module.exports = ({ cooler, tribeCrypto }) => { }, async generateInvite(shopId) { - if (!tribeCrypto) throw new Error("Shop crypto unavailable") + if (!cryptoModel) throw new Error("Shop crypto unavailable") const rootId = await this.resolveRootId(shopId) const ssbClient = await openSsb() const userId = ssbClient.id @@ -857,10 +857,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const keys = keysForRoot(rootId) if (!keys.length) throw new Error("Missing shop key") const code = require("crypto").randomBytes(16).toString("hex") - const salt = tribeCrypto.generateInviteSalt() - const ek = tribeCrypto.encryptForInvite(keys[0], code, salt) + const salt = cryptoModel.generateInviteSalt() + const ek = cryptoModel.encryptForInvite(keys[0], code, salt) await new Promise((resolve, reject) => - ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: tribeCrypto.hashInviteCode(code, salt) }, (e) => e ? reject(e) : resolve()) + ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: cryptoModel.hashInviteCode(code, salt) }, (e) => e ? reject(e) : resolve()) ) return { code, shopId: rootId } }, @@ -891,7 +891,7 @@ module.exports = ({ cooler, tribeCrypto }) => { }, async generateOpenInvite(shopId) { - if (!tribeCrypto) throw new Error("Shop crypto unavailable") + if (!cryptoModel) throw new Error("Shop crypto unavailable") const rootId = await this.resolveRootId(shopId) const ssbClient = await openSsb() const userId = ssbClient.id @@ -903,10 +903,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const keys = keysForRoot(rootId) if (!keys.length) throw new Error("Missing shop key") const code = require("crypto").randomBytes(16).toString("hex") - const salt = tribeCrypto.generateInviteSalt() - const ek = tribeCrypto.encryptForInvite(keys[0], code, salt) + const salt = cryptoModel.generateInviteSalt() + const ek = cryptoModel.encryptForInvite(keys[0], code, salt) const invitePub = await new Promise((resolve, reject) => - ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: tribeCrypto.hashInviteCode(code, salt), multi: 1 }, (e, r) => e ? reject(e) : resolve(r)) + ssbClient.publish({ type: "shop-invite", target: rootId, ek, salt, codeHash: cryptoModel.hashInviteCode(code, salt), multi: 1 }, (e, r) => e ? reject(e) : resolve(r)) ) await new Promise((resolve, reject) => ssbClient.publish({ type: "shop-open-invite", v: 1, target: rootId, code, inviteKey: invitePub.key, by: userId, createdAt: new Date().toISOString() }, (e) => e ? reject(e) : resolve()) @@ -931,7 +931,7 @@ module.exports = ({ cooler, tribeCrypto }) => { }, async joinByCode(code) { - if (!tribeCrypto) throw new Error("Shop crypto unavailable") + if (!cryptoModel) throw new Error("Shop crypto unavailable") const ssbClient = await openSsb() const messages = await readAll(ssbClient) const invTomb = new Set() @@ -945,14 +945,14 @@ module.exports = ({ cooler, tribeCrypto }) => { if (!c || c.type !== "shop-invite") continue if (invTomb.has(m.key)) continue try { - if (tribeCrypto.hashInviteCode(code, c.salt) === c.codeHash) { matched = c; break } + if (cryptoModel.hashInviteCode(code, c.salt) === c.codeHash) { matched = c; break } } catch (_) {} } if (!matched) throw new Error("Invalid or expired invite code") - const shopKey = tribeCrypto.decryptFromInvite(matched.ek, code, matched.salt) + const shopKey = cryptoModel.decryptFromInvite(matched.ek, code, matched.salt) if (!shopKey) throw new Error("Could not decrypt invite") - if (tribeCrypto.addNewKey) tribeCrypto.addNewKey(matched.target, shopKey) - else tribeCrypto.setKey(matched.target, shopKey, 1) + if (cryptoModel.addNewKey) cryptoModel.addNewKey(matched.target, shopKey) + else cryptoModel.setKey(matched.target, shopKey, 1) return { ok: true, shopId: matched.target } }, @@ -969,7 +969,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const curShop = isEncrypted(shopRaw) ? decryptShopContent(shopRaw, rootId) : shopRaw if (curShop._undecryptable) throw new Error("Cannot decrypt shop") - const wantEncrypted = tgt === "CLOSED" && !!tribeCrypto + const wantEncrypted = tgt === "CLOSED" && !!cryptoModel const messages = await readAll(ssbClient) const idx = buildIndex(messages) @@ -983,9 +983,9 @@ module.exports = ({ cooler, tribeCrypto }) => { } if (wantEncrypted) { - const newKey = tribeCrypto.generateTribeKey() - if (tribeCrypto.addNewKey) tribeCrypto.addNewKey(rootId, newKey) - else tribeCrypto.setKey(rootId, newKey, 1) + const newKey = cryptoModel.generateKey() + if (cryptoModel.addNewKey) cryptoModel.addNewKey(rootId, newKey) + else cryptoModel.setKey(rootId, newKey, 1) } const publish = (c) => new Promise((res, rej) => ssbClient.publish(c, (e, m) => e ? rej(e) : res(m))) diff --git a/src/models/stats_model.js b/src/models/stats_model.js index fed5fa6..ada0a9d 100644 --- a/src/models/stats_model.js +++ b/src/models/stats_model.js @@ -5,6 +5,7 @@ const ssbDirOf = () => require('../server/ssb_config').path; const { getConfig } = require('../configs/config-manager.js'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { readTyped, CONTENT_TYPES } = require('./typed_log'); +const { isComment } = require('./comments_model'); const { versionGroups, versionEntry } = require('../backend/dedupe'); const opinionCategories = require('../backend/opinion_categories'); const CONSTRUCTIVE_OPINIONS = new Set(opinionCategories.karma.constructive); @@ -43,7 +44,7 @@ const HIDDEN_ENVELOPE_TYPES = new Set([ 'tribe-invite-tombstone' ]); -module.exports = ({ cooler, tribeCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -203,6 +204,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }; const normalizeActionType = (a) => { + if (isComment(a.content)) return 'comment'; const t = a.type || a.content?.type || inferType(a.content) || ''; return String(t).toLowerCase(); }; @@ -387,13 +389,13 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { versionsOf[t] = []; } - const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null; + const fpIdx = cryptoModel ? cryptoModel.buildFingerprintIndex() : null; const accessibleTribeIds = await buildAccessibleTribeIds(); for (const m of allMsgs) { const k = m.key; let c = m.value.content; - if (tribeCrypto && tribeCrypto.isTribeMsg(c)) { - const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null; + if (cryptoModel && cryptoModel.isSealedMsg(c)) { + const r = fpIdx ? cryptoModel.unwrapMsg(c, fpIdx) : null; if (!r || !r.body) continue; const inner = r.body; if (inner.k === 'tribe') { diff --git a/src/models/torrent_downloads_model.js b/src/models/torrent_downloads_model.js index 7da185f..d6ec4be 100644 --- a/src/models/torrent_downloads_model.js +++ b/src/models/torrent_downloads_model.js @@ -161,9 +161,6 @@ module.exports = ({ cooler }) => { }, async hasLocal(blobId) { try { return await hasLocal(await openSsb(), String(blobId || '')); } catch (_) { return false; } - }, - async seedsFor(blobId) { - try { return (await findSeeds(await openSsb(), String(blobId || ''))).map((s) => s.id); } catch (_) { return []; } } }; }; diff --git a/src/models/torrents_model.js b/src/models/torrents_model.js index 485e05d..bfa53a2 100644 --- a/src/models/torrents_model.js +++ b/src/models/torrents_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { getConfig } = require("../configs/config-manager.js"); const categories = require("../backend/opinion_categories"); const { readTyped, memoIndex } = require("./typed_log"); @@ -29,7 +30,14 @@ const parseBlobId = (blobMarkdown) => { const voteSum = (opinions = {}) => Object.values(opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); -module.exports = ({ cooler, tribeCrypto, tribesModel }) => { +const torrentMeta = (meta) => { + const out = {}; + if (meta && typeof meta.license === "string" && meta.license) out.license = meta.license; + if (meta && /^[0-9a-f]{40}$/.test(String(meta.infoHash || ""))) out.infoHash = meta.infoHash; + return out; +}; + +module.exports = ({ cooler, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { @@ -37,9 +45,9 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return ssb; }; - const tribeHelpers = tribeCrypto ? tribeCrypto.createHelpers(tribesModel) : null; - const encryptIfTribe = tribeHelpers ? tribeHelpers.encryptIfTribe : async (c) => c; - const decryptIfTribe = tribeHelpers ? tribeHelpers.decryptIfTribe : async (c) => c; + const tribeHelpers = cryptoModel ? cryptoModel.createHelpers(tribesModel) : null; + const encryptIfGroup = tribeHelpers ? tribeHelpers.encryptIfGroup : async (c) => c; + const decryptIfGroup = tribeHelpers ? tribeHelpers.decryptIfGroup : async (c) => c; const assertReadable = tribeHelpers ? tribeHelpers.assertReadable : () => {}; const decryptIndexNodes = tribeHelpers ? tribeHelpers.decryptIndexNodes : async () => {}; const unwrapForIndex = (msgs) => tribeHelpers ? tribeHelpers.unwrapMessagesForKind(msgs, 'torrent') : msgs; @@ -134,6 +142,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { size: c.size || 0, source: undec ? "" : (typeof c.source === "string" && c.source.startsWith("&") ? c.source : ""), sourceName: undec ? "" : (c.sourceName || ""), + license: undec ? "" : (c.license || ""), + infoHash: undec ? "" : (/^[0-9a-f]{40}$/.test(String(c.infoHash || "")) ? c.infoHash : ""), cipher: undec ? null : (c.cipher && typeof c.cipher === "object" && typeof c.cipher.key === "string" && typeof c.cipher.manifestBlobId === "string" ? { v: Number(c.cipher.v) || 1, key: c.cipher.key, manifestBlobId: c.cipher.manifestBlobId, chunkCount: Number(c.cipher.chunkCount) || 0 } : null), sourceMime: undec ? "" : (c.sourceMime || ""), opinions, @@ -169,7 +179,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { return root; }, - async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null) { + async createTorrent(blobMarkdown, tagsRaw, title, description, size, tribeId, source = null, meta = {}) { const ssbClient = await openSsb(); const blobId = parseBlobId(blobMarkdown); const tags = normalizeTags(tagsRaw) || []; @@ -191,10 +201,11 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { ...(source && typeof source.source === "string" && source.source.startsWith("&") ? { source: source.source, sourceName: String(source.sourceName || ""), sourceMime: String(source.sourceMime || "") } : {}), - ...cipherField(source && source.cipher) + ...cipherField(source && source.cipher), + ...torrentMeta(meta) }; - content = await encryptIfTribe(content); + content = await encryptIfGroup(content); return new Promise((resolve, reject) => { ssbClient.publish(content, (err, res) => (err ? reject(err) : resolve(res))); @@ -208,12 +219,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const oldMsg = await getMsg(ssbClient, tipId); if (!oldMsg) throw new Error("Torrent not found"); - const oldDec = await decryptIfTribe(oldMsg.content); + const oldDec = await decryptIfGroup(oldMsg.content); if (!oldDec || oldDec.type !== "torrent") throw new Error("Torrent not found"); assertReadable(oldDec, "Torrent"); if (oldMsg.author !== userId) throw new Error("Not the author"); const aggTorrent = await this.getTorrentById(id, userId); - if (aggTorrent && Object.keys(aggTorrent.opinions || {}).some(k => (aggTorrent.opinions[k] || 0) > 0)) throw new Error("Cannot edit torrent after it has received opinions."); + opinionRules.assertMediaKept(aggTorrent, !!blobMarkdown); const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldDec.tags); const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; @@ -227,7 +238,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { tags, title: title !== undefined ? title || "" : oldDec.title || "", description: description !== undefined ? description || "" : oldDec.description || "", - size: oldDec.size || 0, + size: blobId && extra && Number(extra.size) > 0 ? Number(extra.size) : (oldDec.size || 0), + ...torrentMeta({ license: oldDec.license, infoHash: blobId ? "" : oldDec.infoHash, ...(extra || {}) }), opinions: oldDec.opinions || {}, opinions_inhabitants: oldDec.opinions_inhabitants || [], author: oldDec.author || userId, @@ -236,7 +248,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { updatedAt: now }; - updated = await encryptIfTribe(updated); + updated = await encryptIfGroup(updated); const result = await new Promise((resolve, reject) => { ssbClient.publish(updated, (err, res) => (err ? reject(err) : resolve(res))); @@ -253,7 +265,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tipId); if (!msg) throw new Error("Torrent not found"); - const dec = await decryptIfTribe(msg.content); + const dec = await decryptIfGroup(msg.content); if (!dec || dec.type !== "torrent") throw new Error("Torrent not found"); if (msg.author !== userId) throw new Error("Not the author"); @@ -332,8 +344,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const msg = await getMsg(ssbClient, tip); if (!msg) throw new Error("Torrent not found"); let c = msg.content; - if (tribeCrypto && (c?.encryptedPayload || tribeCrypto.isTribeMsg(c)) && tribesModel) { - const dec = await tribeCrypto.decryptFromTribe(c, tribesModel); + if (cryptoModel && (c?.encryptedPayload || cryptoModel.isSealedMsg(c)) && tribesModel) { + const dec = await cryptoModel.decryptFromGroup(c, tribesModel); c = dec && !dec._undecryptable ? { ...dec, _decrypted: true } : { ...c, _decrypted: false }; } if (!c || c.type !== "torrent") throw new Error("Torrent not found"); @@ -357,3 +369,99 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { } }; }; + +const BENCODE_MAX_DEPTH = 64; +const TORRENT_FILES_SHOWN = 50; +const MAGNET_TRACKERS_MAX = 8; +const MAGNET_SEEDS_MAX = 4; +const TORRENT_LINK_RE = /^(https?|udp|wss?):\/\//i; + +const decodeBencode = (buf) => { + let pos = 0; + let infoRange = null; + const fail = () => { throw new Error("Invalid torrent"); }; + const parse = (depth) => { + if (depth > BENCODE_MAX_DEPTH || pos >= buf.length) fail(); + const ch = buf[pos]; + if (ch === 0x69) { + const end = buf.indexOf(0x65, pos); + if (end < 0) fail(); + const n = Number(buf.toString("latin1", pos + 1, end)); + pos = end + 1; + return Number.isFinite(n) ? n : fail(); + } + if (ch === 0x6c) { + pos++; + const out = []; + while (buf[pos] !== 0x65) out.push(parse(depth + 1)); + pos++; + return out; + } + if (ch === 0x64) { + pos++; + const out = {}; + while (buf[pos] !== 0x65) { + const key = parse(depth + 1); + if (!Buffer.isBuffer(key)) fail(); + const start = pos; + const name = key.toString("latin1"); + out[name] = parse(depth + 1); + if (depth === 0 && name === "info") infoRange = [start, pos]; + } + pos++; + return out; + } + if (ch >= 0x30 && ch <= 0x39) { + const colon = buf.indexOf(0x3a, pos); + if (colon < 0) fail(); + const len = Number(buf.toString("latin1", pos, colon)); + const start = colon + 1; + if (!Number.isInteger(len) || len < 0 || start + len > buf.length) fail(); + pos = start + len; + return buf.subarray(start, pos); + } + return fail(); + }; + return { value: parse(0), infoRange }; +}; + +const text = (v) => (Buffer.isBuffer(v) ? v.toString("utf8") : ""); + +const parseTorrent = (buf) => { + const { value, infoRange } = decodeBencode(Buffer.from(buf)); + const info = value && value.info; + if (!info || typeof info !== "object" || !infoRange) throw new Error("Invalid torrent"); + const crypto = require("crypto"); + const raw = Buffer.from(buf).subarray(infoRange[0], infoRange[1]); + const v2Only = !info.pieces && info["file tree"]; + const infoHash = crypto.createHash(v2Only ? "sha256" : "sha1").update(raw).digest("hex"); + const name = text(info["name.utf-8"] || info.name); + const files = Array.isArray(info.files) + ? info.files.map(f => ({ path: (Array.isArray(f["path.utf-8"] || f.path) ? (f["path.utf-8"] || f.path) : []).map(text).join("/"), length: Number(f.length) || 0 })) + : [{ path: name, length: Number(info.length) || 0 }]; + const announce = [text(value.announce), ...(Array.isArray(value["announce-list"]) ? value["announce-list"].flat().map(text) : [])]; + const seeds = Array.isArray(value["url-list"]) ? value["url-list"].map(text) : [text(value["url-list"])]; + return { + infoHash, + v2: !!v2Only, + name, + length: files.reduce((sum, f) => sum + f.length, 0), + fileCount: files.length, + files: files.slice(0, TORRENT_FILES_SHOWN), + trackers: [...new Set(announce.filter(t => TORRENT_LINK_RE.test(t)))].slice(0, MAGNET_TRACKERS_MAX), + webSeeds: [...new Set(seeds.filter(u => /^https?:\/\//i.test(u)))].slice(0, MAGNET_SEEDS_MAX) + }; +}; + +const magnetFor = (t) => { + if (!t || !t.infoHash) return ""; + const params = [t.v2 ? `xt=urn:btmh:1220${t.infoHash}` : `xt=urn:btih:${t.infoHash}`]; + if (t.name) params.push(`dn=${encodeURIComponent(t.name)}`); + if (t.length) params.push(`xl=${t.length}`); + for (const tr of t.trackers || []) params.push(`tr=${encodeURIComponent(tr)}`); + for (const ws of t.webSeeds || []) params.push(`ws=${encodeURIComponent(ws)}`); + return `magnet:?${params.join("&")}`; +}; + +module.exports.parseTorrent = parseTorrent; +module.exports.magnetFor = magnetFor; diff --git a/src/models/transfers_model.js b/src/models/transfers_model.js index a4c2014..c200a5d 100644 --- a/src/models/transfers_model.js +++ b/src/models/transfers_model.js @@ -175,7 +175,7 @@ module.exports = ({ cooler }) => { for (const cf of rootConfirms) { if (cf.author === best.c.to) confirmedBy.add(cf.author) } for (const op of (opinionsByRoot.get(root) || [])) { if (!opinionSet.has(op.author)) { opinionSet.add(op.author); opinions[op.category] = (opinions[op.category] || 0) + 1 } } const ubiPayerTrusted = !!best.author && (best.author === defaultPub || !!(claimedPubs.get(best.c.to) && claimedPubs.get(best.c.to).has(best.author))) - return { contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted } + return { root, contentTip, best, confirmedBy: [...confirmedBy], opinions, opinions_inhabitants: [...opinionSet], tombstoned: tomb.has(contentTip) || tomb.has(best.key), ubiPayerTrusted } } return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup } @@ -204,6 +204,7 @@ module.exports = ({ cooler }) => { const opinions_inhabitants = agg ? agg.opinions_inhabitants : (Array.isArray(c.opinions_inhabitants) ? c.opinions_inhabitants : []) return { id: node.key, + rootId: agg && agg.root ? agg.root : node.key, author: node.author, from: c.from, to: c.to, @@ -289,7 +290,6 @@ module.exports = ({ cooler }) => { const aggU = gU ? buildTransfer(gU.best, gU) : buildTransfer({ key: tipId, c: current }) const currentStatus = aggU.status - if (Object.keys(aggU.opinions || {}).some(k => (aggU.opinions[k] || 0) > 0)) throw new Error("Cannot edit transfer after it has received opinions.") if (current.from !== userId) throw new Error("Not the author") if (currentStatus !== "UNCONFIRMED") throw new Error("Can only edit unconfirmed") @@ -300,6 +300,7 @@ module.exports = ({ cooler }) => { const num = parseNum(amount) if (!Number.isFinite(num) || num <= 0) throw new Error("Amount must be positive") + categories.assertMediaKept(aggU, to !== current.to || num.toFixed(6) !== Number(current.amount || 0).toFixed(6)) const dl = moment(deadline, moment.ISO_8601, true) if (!dl.isValid() || dl.isBefore(moment())) throw new Error("Deadline must be in the future") diff --git a/src/models/trending_model.js b/src/models/trending_model.js index e25dd9d..0f5cfe8 100644 --- a/src/models/trending_model.js +++ b/src/models/trending_model.js @@ -20,17 +20,11 @@ module.exports = ({ cooler }) => { return ssb; }; - const hasBlob = async (ssbClient, url) => { - return new Promise(resolve => { - ssbClient.blobs.has(url, (err, has) => resolve(!err && has)); - }); - }; - const types = [ 'bookmark', 'votes', 'poll', 'feed', 'post', 'image', 'audio', 'video', 'document', 'torrent', 'file', 'transfer', 'industry', 'project', 'report', 'task', 'event', 'shopProduct', 'housing', 'market', 'schoolCourse', - 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute' + 'podcast', 'podcastEpisode', 'campaign', 'logisticsRoute', 'wikiPage', 'map', 'industryBlueprint', 'job' ]; const categories = opinionCategories; @@ -55,7 +49,7 @@ module.exports = ({ cooler }) => { continue; } - if (c.opinions && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m); + if ((c.opinions || categories.opinable(c)) && !['task', 'event', 'report'].includes(c.type)) itemsById.set(k, m); if (c.type === 'feed' && !itemsById.has(k)) itemsById.set(k, m); if (isRootBlogPost(c) && !itemsById.has(k)) itemsById.set(k, m); @@ -101,19 +95,7 @@ module.exports = ({ cooler }) => { if (c.encryptedPayload) return false; return String(c.visibility || 'PUBLIC').toUpperCase() !== 'INVITE'; }); - const blobTypes = ['document', 'image', 'audio', 'video']; - - let items = await Promise.all( - rawItems.map(async m => { - const c = m.value?.content; - if (blobTypes.includes(c.type) && c.url) { - const valid = await hasBlob(ssbClient, c.url); - if (!valid) return null; - } - return m; - }) - ); - items = items.filter(Boolean); + let items = rawItems.slice(); const signatureOf = (m) => { const c = m.value?.content || {}; diff --git a/src/models/tribes_content_model.js b/src/models/tribes_content_model.js index 048f3e9..5c5f4f6 100644 --- a/src/models/tribes_content_model.js +++ b/src/models/tribes_content_model.js @@ -27,7 +27,7 @@ const snapshotOf = (item, replies = []) => { return out; }; -module.exports = ({ cooler, tribeCrypto, tribesModel }) => { +module.exports = ({ cooler, cryptoModel, tribesModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -38,15 +38,15 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { const fingerprintsForRoot = (rootId) => { const fps = new Set(); - for (const k of tribeCrypto.getKeys(rootId)) fps.add(tribeCrypto.fingerprint(k)); + for (const k of cryptoModel.getKeys(rootId)) fps.add(cryptoModel.fingerprint(k)); return fps; }; const wrapAndPublishContent = async (rootId, body) => { const client = await openSsb(); - const key = tribeCrypto.getKey(rootId); + const key = cryptoModel.getKey(rootId); if (!key) throw new Error('Missing tribe key for ' + rootId); - const envelope = tribeCrypto.wrapMsg(body, key); + const envelope = cryptoModel.wrapMsg(body, key); return new Promise((resolve, reject) => client.publish(envelope, (err, r) => err ? reject(err) : resolve(r)) ); @@ -59,7 +59,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { }; const decodeContentMsgs = async (msgs, opts) => { - const fpIdx = tribeCrypto.buildFingerprintIndex(); + const fpIdx = cryptoModel.buildFingerprintIndex(); const targetRootId = opts && opts.rootId ? opts.rootId : null; const wantType = opts && opts.contentType ? opts.contentType : null; const allowedFps = targetRootId ? fingerprintsForRoot(targetRootId) : null; @@ -72,10 +72,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => { for (const m of msgs) { const c = m.value && m.value.content; if (!c) continue; - if (!tribeCrypto.isTribeMsg(c)) continue; + if (!cryptoModel.isSealedMsg(c)) continue; if (allowedFps && !allowedFps.has(c.fp)) continue; - const r = tribeCrypto.unwrapMsg(c, fpIdx); + const r = cryptoModel.unwrapMsg(c, fpIdx); if (!r || !r.body) continue; const b = r.body; diff --git a/src/models/tribes_model.js b/src/models/tribes_model.js index 8aa345b..6ac7891 100644 --- a/src/models/tribes_model.js +++ b/src/models/tribes_model.js @@ -12,7 +12,7 @@ const VALID_INVITE_MODES = ['strict', 'open']; const STRUCTURAL_FIELDS = ['title', 'description', 'image', 'location', 'tags', 'isAnonymous', 'inviteMode', 'status', 'parentTribeId', 'mapUrl']; -module.exports = ({ cooler, tribeCrypto }) => { +module.exports = ({ cooler, cryptoModel }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -70,7 +70,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const v = m && m.value; if (!v) continue; const c = v.content; - const dec = (c && typeof c === 'object') ? (v.private === true && tribeCrypto.isKeyDistribContent(c) ? c : null) : tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); + const dec = (c && typeof c === 'object') ? (v.private === true && cryptoModel.isKeyDistribContent(c) ? c : null) : cryptoModel.tryUnboxKeyDistrib(c, config.keys, ssbKeys); if (dec) out.push({ m, dec }); } return { msgs, distribs: out }; @@ -79,7 +79,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const buildTribeIndex = async () => { subscribeInvalidation().catch(() => {}); if (tribeIndex && Date.now() - tribeIndexTs < 5000) return tribeIndex; - const fpIdx = tribeCrypto.buildFingerprintIndex(); + const fpIdx = cryptoModel.buildFingerprintIndex(); const msgs = await streamLog(); const tribeMsgs = new Map(); @@ -102,8 +102,8 @@ module.exports = ({ cooler, tribeCrypto }) => { } let body = null; - if (tribeCrypto.isTribeMsg(c)) { - const r = tribeCrypto.unwrapMsg(c, fpIdx); + if (cryptoModel.isSealedMsg(c)) { + const r = cryptoModel.unwrapMsg(c, fpIdx); if (!r || !r.body || !r.body.k) continue; if (r.body.k === 'open-invite' || r.body.k === 'open-invite-tombstone') { sealedInvites.push({ key: m.key, author, body: r.body, keyRoot: r.rootId, ts }); @@ -280,7 +280,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const c = m && m.value && m.value.content; if (!c) return; if (typeof c === 'string' && c.endsWith('.box')) { tribeIndex = null; return; } - if (tribeCrypto.isTribeMsg(c)) { tribeIndex = null; return; } + if (cryptoModel.isSealedMsg(c)) { tribeIndex = null; return; } if (c.type === 'tribe-invite-msg' || c.type === 'tribe-invite-tombstone') tribeIndex = null; }, () => { started = false; }) ); @@ -314,9 +314,9 @@ module.exports = ({ cooler, tribeCrypto }) => { const wrapAndPublish = async (rootId, body) => { const client = await openSsb(); - const key = tribeCrypto.getKey(rootId); + const key = cryptoModel.getKey(rootId); if (!key) throw new Error('Missing tribe key for ' + rootId); - const envelope = tribeCrypto.wrapMsg(body, key); + const envelope = cryptoModel.wrapMsg(body, key); const result = await new Promise((resolve, reject) => client.publish(envelope, (err, r) => err ? reject(err) : resolve(r)) ); @@ -389,18 +389,18 @@ module.exports = ({ cooler, tribeCrypto }) => { if (typeof c.ch !== 'string' || typeof c.s !== 'string' || typeof c.ek !== 'string') continue; const multi = c.multi === 1 || c.multi === true; if (!multi && inviteTombstonedBy.has(m.key)) continue; - if (tribeCrypto.hashInviteCode(code, c.s) !== c.ch) continue; - const chain = tribeCrypto.decryptChainFromInvite(c.ek, code, c.s, 3); + if (cryptoModel.hashInviteCode(code, c.s) !== c.ch) continue; + const chain = cryptoModel.decryptChainFromInvite(c.ek, code, c.s, 3); if (Array.isArray(chain) && chain.length) { matched = { msgKey: m.key, codeHash: c.ch, chain, multi, inviteAuthor: v.author }; break; } } if (!matched) return null; - const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !tribeCrypto.getKeys(entry.rootId).length); - for (const entry of fresh) tribeCrypto.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + const fresh = matched.chain.filter(entry => entry && Array.isArray(entry.keys) && entry.keys.length && !cryptoModel.getKeys(entry.rootId).length); + for (const entry of fresh) cryptoModel.setKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); tribeIndex = null; - const undo = () => { for (const entry of fresh) tribeCrypto.dropKey(entry.rootId); tribeIndex = null; }; + const undo = () => { for (const entry of fresh) cryptoModel.dropKey(entry.rootId); tribeIndex = null; }; const rootId = matched.chain[0].rootId; const idx = await buildTribeIndex().catch(() => null); const entry = idx && idx.tribes.get(idx.tipByRoot.get(rootId) || rootId); @@ -428,7 +428,7 @@ module.exports = ({ cooler, tribeCrypto }) => { : String(tagsRaw || '').split(',').map(t => t.trim()).filter(Boolean); const isPrivate = Boolean(isAnonymous); - const newKey = tribeCrypto.generateTribeKey(); + const newKey = cryptoModel.generateKey(); const now = new Date().toISOString(); const baseFields = { title, @@ -449,14 +449,14 @@ module.exports = ({ cooler, tribeCrypto }) => { }; let envelope; if (isPrivate) { - envelope = tribeCrypto.wrapMsg({ k: 'tribe', op: 'create', rootId: null, replaces: null, ...baseFields }, newKey); + envelope = cryptoModel.wrapMsg({ k: 'tribe', op: 'create', rootId: null, replaces: null, ...baseFields }, newKey); } else { envelope = { type: 'tribe', op: 'create', ...baseFields }; } const result = await new Promise((resolve, reject) => client.publish(envelope, (err, r) => err ? reject(err) : resolve(r)) ); - tribeCrypto.setKey(result.key, newKey, 1); + cryptoModel.setKey(result.key, newKey, 1); tribeIndex = null; subscribeInvalidation().catch(() => {}); return result; @@ -710,10 +710,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex'); const targetRoot = await this.getRootId(tribeId); if (!targetRoot) throw new Error('Cannot resolve tribe root'); - const salt = tribeCrypto.generateInviteSalt(); - const ekChain = tribeCrypto.encryptChainForInvite([targetRoot], code, salt); + const salt = cryptoModel.generateInviteSalt(); + const ekChain = cryptoModel.encryptChainForInvite([targetRoot], code, salt); if (!ekChain) throw new Error('Cannot encrypt invite chain โ€” missing keys'); - const codeHash = tribeCrypto.hashInviteCode(code, salt); + const codeHash = cryptoModel.hashInviteCode(code, salt); const inviteMsg = { type: 'tribe-invite-msg', v: 1, @@ -727,7 +727,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const inviteRef = { codeHash, salt, - gen: tribeCrypto.getGen(targetRoot), + gen: cryptoModel.getGen(targetRoot), msgKey: invitePub.key }; const invites = Array.isArray(tribe.invites) ? [...tribe.invites, inviteRef] : [inviteRef]; @@ -770,10 +770,10 @@ module.exports = ({ cooler, tribeCrypto }) => { const code = crypto.randomBytes(INVITE_CODE_BYTES).toString('hex'); const targetRoot = await this.getRootId(tribeId); if (!targetRoot) throw new Error('Cannot resolve tribe root'); - const salt = tribeCrypto.generateInviteSalt(); - const ekChain = tribeCrypto.encryptChainForInvite([targetRoot], code, salt); + const salt = cryptoModel.generateInviteSalt(); + const ekChain = cryptoModel.encryptChainForInvite([targetRoot], code, salt); if (!ekChain) throw new Error('Cannot encrypt invite chain โ€” missing keys'); - const codeHash = tribeCrypto.hashInviteCode(code, salt); + const codeHash = cryptoModel.hashInviteCode(code, salt); const invitePub = await new Promise((resolve, reject) => client.publish({ type: 'tribe-invite-msg', v: 1, multi: 1, ch: codeHash, s: salt, ek: ekChain }, (err, r) => err ? reject(err) : resolve(r)) ); @@ -843,8 +843,8 @@ module.exports = ({ cooler, tribeCrypto }) => { } for (const entry of matched.chain) { if (!entry || !Array.isArray(entry.keys) || !entry.keys.length) continue; - if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) tribeCrypto.dropKey(entry.rootId); continue; } - if (lineage.has(entry.rootId)) tribeCrypto.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); + if (fresh.includes(entry)) { if (!lineage.has(entry.rootId)) cryptoModel.dropKey(entry.rootId); continue; } + if (lineage.has(entry.rootId)) cryptoModel.mergeKeys(entry.rootId, entry.keys, entry.gen || entry.keys.length); } tribeIndex = null; const members = [...tribe.members, userId]; @@ -894,12 +894,12 @@ module.exports = ({ cooler, tribeCrypto }) => { if (!Array.isArray(toMembers) || !toMembers.length) return; const client = await openSsb(); const rootId = await this.getRootId(tribeId); - const keys = tribeCrypto.getKeys(rootId); - const gen = tribeCrypto.getGen(rootId); + const keys = cryptoModel.getKeys(rootId); + const gen = cryptoModel.getGen(rootId); if (!keys.length) return; - const payload = tribeCrypto.buildKeyDistribPayload(rootId, keys, gen); + const payload = cryptoModel.buildKeyDistribPayload(rootId, keys, gen); const others = [...new Set(toMembers)].filter(m => m && m !== client.id); - const batch = tribeCrypto.KEY_DISTRIB_BATCH - 1; + const batch = cryptoModel.KEY_DISTRIB_BATCH - 1; let i = 0; do { const recps = [client.id, ...others.slice(i, i + batch)]; @@ -912,10 +912,10 @@ module.exports = ({ cooler, tribeCrypto }) => { async rotateTribeKey(tribeId, remainingMembers) { const rootId = await this.getRootId(tribeId); - const oldKey = tribeCrypto.getKey(rootId); + const oldKey = cryptoModel.getKey(rootId); if (!oldKey) return; - const newKey = tribeCrypto.generateTribeKey(); - tribeCrypto.addNewKey(rootId, newKey); + const newKey = cryptoModel.generateKey(); + cryptoModel.addNewKey(rootId, newKey); if (Array.isArray(remainingMembers) && remainingMembers.length > 0) { await this.distributeTribeKey(tribeId, remainingMembers).catch(() => {}); } @@ -928,11 +928,11 @@ module.exports = ({ cooler, tribeCrypto }) => { try { tribe = await this.getTribeById(tribeId); } catch (_) { return; } if (!tribe || tribe.author !== userId) return; const rootId = await this.getRootId(tribeId); - if (!tribeCrypto.getKey(rootId)) return; + if (!cryptoModel.getKey(rootId)) return; const others = (tribe.members || []).filter(m => m !== userId); const { distribs } = await readKeyDistribs(); const distributed = new Set(); - const currentGen = tribeCrypto.getGen(rootId); + const currentGen = cryptoModel.getGen(rootId); const holdersOfCurrent = new Set(); for (const { m, dec } of distribs) { if (m.value.author !== userId) continue; @@ -968,7 +968,7 @@ module.exports = ({ cooler, tribeCrypto }) => { entries.sort((a, b) => b.generation - a.generation); const top = entries[0]; if (top && Array.isArray(top.keys) && top.keys.length) { - tribeCrypto.mergeKeys(rootId, top.keys, top.generation); + cryptoModel.mergeKeys(rootId, top.keys, top.generation); } } }, @@ -978,7 +978,7 @@ module.exports = ({ cooler, tribeCrypto }) => { const me = client.id; let tribe; try { tribe = await this.getTribeById(tribeId); } catch (_) { return; } - const fpIdx = tribeCrypto.buildFingerprintIndex(); + const fpIdx = cryptoModel.buildFingerprintIndex(); const rootId = await this.getRootId(tribeId).catch(() => tribeId); const tribeChainIds = await this.getChainIds(tribeId).catch(() => [tribeId]); const tribeRootSet = new Set([rootId]); @@ -1006,15 +1006,15 @@ module.exports = ({ cooler, tribeCrypto }) => { if (myInviteMsgKeys.has(c.target)) discovered.add(v.author); continue; } - const dec = tribeCrypto.tryUnboxKeyDistrib(c, config.keys, ssbKeys); + const dec = cryptoModel.tryUnboxKeyDistrib(c, config.keys, ssbKeys); if (dec && tribeRootSet.has(dec.rootId)) { const recps = Array.isArray(dec.recps) ? dec.recps : []; for (const r of recps) discovered.add(r); if (v.author) discovered.add(v.author); continue; } - if (tribeCrypto.isTribeMsg(c)) { - const r = tribeCrypto.unwrapMsg(c, fpIdx); + if (cryptoModel.isSealedMsg(c)) { + const r = cryptoModel.unwrapMsg(c, fpIdx); if (!r || !r.body) continue; if (r.body.k !== 'tribe') continue; if (!tribeChainSet.has(m.key) && !tribeChainSet.has(r.body.replaces || '') && r.rootId !== rootId) continue; @@ -1072,7 +1072,7 @@ module.exports = ({ cooler, tribeCrypto }) => { while (idx.child.has(tipId)) tipId = idx.child.get(tipId); let tribe; try { tribe = await this.getTribeById(tribeId); } catch (_) { tribe = null; } - const isPrivate = tribe ? !!tribe.isAnonymous : !!tribeCrypto.getKey(rootId); + const isPrivate = tribe ? !!tribe.isAnonymous : !!cryptoModel.getKey(rootId); let result; if (isPrivate) { result = await wrapAndPublish(rootId, { k: 'tombstone', rootId, target: tipId, author: userId, deletedAt: new Date().toISOString() }); @@ -1087,13 +1087,13 @@ module.exports = ({ cooler, tribeCrypto }) => { }, async pruneOrphanKeys() { - if (!tribeCrypto || typeof tribeCrypto.getAllRootIds !== 'function') return 0; + if (!cryptoModel || typeof cryptoModel.getAllRootIds !== 'function') return 0; const idx = await buildTribeIndex(); - const all = tribeCrypto.getAllRootIds(); + const all = cryptoModel.getAllRootIds(); let removed = 0; for (const rid of all) { if (idx.effectivelyTombstoned.has(rid)) { - try { tribeCrypto.dropKey(rid); removed++; } catch (_) {} + try { cryptoModel.dropKey(rid); removed++; } catch (_) {} } } return removed; diff --git a/src/models/videos_model.js b/src/models/videos_model.js index d9bc7a3..d659abb 100644 --- a/src/models/videos_model.js +++ b/src/models/videos_model.js @@ -1,4 +1,5 @@ const pull = require("../server/node_modules/pull-stream"); +const opinionRules = require("../backend/opinion_categories"); const { getConfig } = require("../configs/config-manager.js"); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { dedupeBy, norm, versionGroups } = require('../backend/dedupe'); @@ -171,7 +172,7 @@ module.exports = ({ cooler }) => { if (!oldMsg || oldMsg.content?.type !== "video") throw new Error("Video not found"); if (oldMsg.content.author !== userId) throw new Error("Not the author"); const aggV = await this.getVideoById(id, userId); - if (aggV && Object.keys(aggV.opinions || {}).some(k => (aggV.opinions[k] || 0) > 0)) throw new Error("Cannot edit video after it has received opinions."); + opinionRules.assertMediaKept(aggV, !!blobMarkdown); const tags = tagsRaw !== undefined ? normalizeTags(tagsRaw) || [] : safeArr(oldMsg.content.tags); const blobId = blobMarkdown ? parseBlobId(blobMarkdown) : null; diff --git a/src/models/votes_model.js b/src/models/votes_model.js index 96deddb..814eb70 100644 --- a/src/models/votes_model.js +++ b/src/models/votes_model.js @@ -1,4 +1,5 @@ const pull = require('../server/node_modules/pull-stream'); +const opinionRules = require('../backend/opinion_categories'); const moment = require('../server/node_modules/moment'); const { buildValidatedTombstoneSet } = require('./tombstone_validator'); const { getConfig } = require('../configs/config-manager.js'); @@ -15,6 +16,10 @@ const isVoteLocked = (v) => !!v && ( || Object.values(v.opinions || {}).some(n => (Number(n) || 0) > 0) || (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t)))) ); +const isVoteEditLocked = (v) => !!v && ( + (parseInt(v.totalVotes || 0, 10) || 0) > 0 + || (Array.isArray(v.tags) && v.tags.some(t => INSTITUTIONAL_VOTE_TAG.test(String(t)))) +); const lockedError = () => Object.assign(new Error('Vote locked'), { code: 'VOTE_LOCKED' }); module.exports = ({ cooler }) => { @@ -151,6 +156,7 @@ module.exports = ({ cooler }) => { id: best.key, latestId: best.key, ...best.content, + rootId: root, votes: votesMap, voters, totalVotes, @@ -253,7 +259,7 @@ module.exports = ({ cooler }) => { if (c.createdBy !== userId) throw new Error('Not the author'); const agg = await this.getVoteById(id); const aggTotalVotes = agg ? (parseInt(agg.totalVotes || 0, 10) || 0) : 0; - if (isVoteLocked(c) || isVoteLocked(agg)) throw lockedError(); + if (isVoteEditLocked(c) || isVoteEditLocked(agg)) throw lockedError(); let newDeadline = c.deadline; if (deadline != null && deadline !== '') { @@ -276,6 +282,9 @@ module.exports = ({ cooler }) => { options.some((o, i) => o !== newOptions[i]) ); + const questionChanged = question !== undefined && String(question) !== String(c.question || ''); + opinionRules.assertMediaKept(agg && Object.keys(agg.opinions || {}).length ? agg : c, questionChanged || optionsChanged); + if (optionsChanged) { if (aggTotalVotes > 0) { throw new Error('Cannot change options after voting has started'); @@ -387,3 +396,4 @@ module.exports = ({ cooler }) => { }; }; module.exports.isVoteLocked = isVoteLocked; +module.exports.isVoteEditLocked = isVoteEditLocked; diff --git a/src/models/wiki_model.js b/src/models/wiki_model.js index ee8ffe6..848bfdd 100644 --- a/src/models/wiki_model.js +++ b/src/models/wiki_model.js @@ -68,7 +68,7 @@ const normalizePolicy = (v) => { return EDIT_POLICIES.includes(s) ? s : 'open'; }; -module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { +module.exports = ({ cooler, cryptoModel = null, tribesModel = null }) => { let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; @@ -78,9 +78,9 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { }; const tribeKeyFor = async (tribeId) => { - if (!tribeCrypto || !tribeId) return null; + if (!cryptoModel || !tribeId) return null; const rootId = await tribeRootId(tribeId); - return tribeCrypto.getKey(rootId) || null; + return cryptoModel.getKey(rootId) || null; }; const storeBody = async (ssbClient, body, tribeId) => { @@ -90,7 +90,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { if (tribeId) { const key = await tribeKeyFor(tribeId); if (!key) throw new Error('Missing tribe key โ€” cannot publish encrypted wiki page'); - payload = JSON.stringify(tribeCrypto.encryptContent({ body: text }, [key], true)); + payload = JSON.stringify(cryptoModel.encryptContent({ body: text }, [key], true)); } const ref = await addBlob(ssbClient, Buffer.from(payload, 'utf8')); bodyCache.set(ref, text); @@ -106,7 +106,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { if (c.tribeId) { try { const key = await tribeKeyFor(c.tribeId); - const r = tribeCrypto.decryptContent(JSON.parse(text), key ? [[key]] : []); + const r = cryptoModel.decryptContent(JSON.parse(text), key ? [[key]] : []); text = r && !r._undecryptable && typeof r.body === 'string' ? r.body : ''; } catch (_) { text = ''; } } @@ -116,15 +116,15 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { const readEntries = async (ssbClient) => { const msgs = await readTyped(ssbClient, [WIKI_TYPE, 'tombstone', ENVELOPE_TYPE], { limit: logLimit }); - const fpIdx = tribeCrypto && typeof tribeCrypto.buildFingerprintIndex === 'function' ? tribeCrypto.buildFingerprintIndex() : null; + const fpIdx = cryptoModel && typeof cryptoModel.buildFingerprintIndex === 'function' ? cryptoModel.buildFingerprintIndex() : null; const out = []; for (const m of msgs) { const v = m && m.value; const c = v && v.content; if (!c || typeof c !== 'object') continue; if (c.type === WIKI_TYPE || c.type === 'tombstone') { out.push(m); continue; } - if (!fpIdx || !tribeCrypto.isTribeMsg(c)) continue; - const r = tribeCrypto.unwrapMsg(c, fpIdx); + if (!fpIdx || !cryptoModel.isSealedMsg(c)) continue; + const r = cryptoModel.unwrapMsg(c, fpIdx); if (!r || !r.body || typeof r.body !== 'object') continue; const body = r.body; if (body.k === WIKI_TYPE) { @@ -298,7 +298,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { if (content.tribeId) { const key = await tribeKeyFor(content.tribeId); if (!key) throw new Error('Missing tribe key โ€” cannot publish encrypted wiki page'); - msg = tribeCrypto.wrapMsg({ k: WIKI_TYPE, ...content }, key); + msg = cryptoModel.wrapMsg({ k: WIKI_TYPE, ...content }, key); } return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); }; @@ -407,7 +407,7 @@ module.exports = ({ cooler, tribeCrypto = null, tribesModel = null }) => { if (page.tribeId) { const key = await tribeKeyFor(page.tribeId); if (!key) throw new Error('Missing tribe key โ€” cannot delete encrypted wiki page'); - msg = tribeCrypto.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key); + msg = cryptoModel.wrapMsg({ k: 'tombstone', target: page.id, deletedAt, author: ssbClient.id }, key); } return new Promise((resolve, reject) => ssbClient.publish(msg, (err, res) => err ? reject(err) : resolve(res))); }, diff --git a/src/patches/patch-node-modules.js b/src/patches/patch-node-modules.js new file mode 100644 index 0000000..ad84e93 --- /dev/null +++ b/src/patches/patch-node-modules.js @@ -0,0 +1,499 @@ +const fs = require('fs'); +const path = require('path'); + +const verbose = process.argv.includes('--verbose') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; +const log = (msg) => { if (verbose) console.log(`[OASIS] [PATCH] ${msg}`); }; + +// === Patch ssb-ref === +const ssbRefPath = path.resolve(__dirname, '../server/node_modules/ssb-ref/index.js'); +if (fs.existsSync(ssbRefPath)) { + const data = fs.readFileSync(ssbRefPath, 'utf8'); + const alreadyClean = /exports\.parseAddress\s*=\s*parseAddress/.test(data); + if (!alreadyClean) { + const patched = data.replace( + /exports\.parseAddress\s*=\s*deprecate\(\s*['"][^'"]*['"]\s*,\s*parseAddress\s*\)/, + 'exports.parseAddress = parseAddress' + ); + if (patched !== data) { + fs.writeFileSync(ssbRefPath, patched); + log('Patched ssb-ref to remove deprecated usage of parseAddress'); + } else { + log('ssb-ref patch skipped: unexpected parseAddress export format'); + } + } +} else { + log('ssb-ref patch skipped: file not found at ' + ssbRefPath); +} + +// === Patch ssb-blobs === +const ssbBlobsPath = path.resolve(__dirname, '../server/node_modules/ssb-blobs/inject.js'); +if (fs.existsSync(ssbBlobsPath)) { + let data = fs.readFileSync(ssbBlobsPath, 'utf8'); + + const marker = 'want: function (id, cb)'; + const startIndex = data.indexOf(marker); + if (data.includes('if (wantCallbacks[id]) registerWant(id);')) { + log('ssb-blobs already patched'); + } else if (startIndex !== -1) { + const endIndex = data.indexOf('},', startIndex); + if (endIndex !== -1) { + const before = data.slice(0, startIndex); + const after = data.slice(endIndex + 2); + + const replacement = ` + want: function (id, cb) { + id = toBlobId(id); + if (!isBlobId(id)) return cb(new Error('invalid id:' + id)); + + if (blobStore.isEmptyHash(id)) return cb(null, true); + + if (wantCallbacks[id]) { + if (!Array.isArray(wantCallbacks[id])) wantCallbacks[id] = []; + wantCallbacks[id].push(cb); + } else { + wantCallbacks[id] = [cb]; + blobStore.size(id, function (err, size) { + if (err) return cb(err); + if (size != null) { + while (wantCallbacks[id].length) { + const fn = wantCallbacks[id].shift(); + if (typeof fn === 'function') fn(null, true); + } + delete wantCallbacks[id]; + } + }); + } + + const peerId = findPeerWithBlob(id); + if (peerId) get(peerId, id); + + if (wantCallbacks[id]) registerWant(id); + },`; + + const finalData = before + replacement + after; + fs.writeFileSync(ssbBlobsPath, finalData); + log('Patched ssb-blobs to fix wantCallbacks handling'); + } else { + log('ssb-blobs patch skipped: end of want function not found'); + } + } else { + log('ssb-blobs patch skipped: want function not found'); + } +} + +// === Patch ssb-db2 (onceWhen leaks its listener when the condition already holds) === +const ssbDb2UtilsPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/utils.js'); +if (fs.existsSync(ssbDb2UtilsPath)) { + const data = fs.readFileSync(ssbDb2UtilsPath, 'utf8'); + if (!data.includes('answered = true\n cb()\n return false')) { + const start = data.indexOf('function onceWhen(obv, filter, cb) {'); + const end = data.indexOf('\n}\n', start); + if (start >= 0 && end > start) { + const replacement = `function onceWhen(obv, filter, cb) { + if (!obv) return cb() + let answered = false + obv((x) => { + if (answered) return false + if (!filter(x)) return + answered = true + cb() + return false + }) +}`; + fs.writeFileSync(ssbDb2UtilsPath, data.slice(0, start) + replacement + data.slice(end + 2)); + log('Patched ssb-db2 utils.js so onceWhen releases its listener once answered'); + } else { + log('ssb-db2 patch skipped: onceWhen not found'); + } + } +} else { + log('ssb-db2 patch skipped: file not found'); +} + +const ssbDb2PluginPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/indexes/plugin.js'); +if (fs.existsSync(ssbDb2PluginPath)) { + const data = fs.readFileSync(ssbDb2PluginPath, 'utf8'); + const target = ` this.onRecord = function onRecord(record, isLive, pValue) {`; + const flushTarget = ` if (changes > chunkSize) this.flush(thenMaybeReportError)\n else if (isLive) liveFlush(thenMaybeReportError)`; + if (!data.includes('lastFlushAt') && data.includes(target) && data.includes(flushTarget)) { + fs.writeFileSync(ssbDb2PluginPath, data + .replace(target, ` let lastFlushAt = Date.now()\n${target}`) + .replace(flushTarget, ` if (changes > chunkSize || Date.now() - lastFlushAt > 1000) {\n lastFlushAt = Date.now()\n this.flush(thenMaybeReportError)\n } else if (isLive) liveFlush(thenMaybeReportError)`)); + log('Patched ssb-db2 indexes so their progress is saved at least once per second'); + } +} else { + log('ssb-db2 index patch skipped: file not found'); +} + +// === Patch @xenova/transformers (onnxruntime 1.19 Tensor getter) === +const xenovaTensorPath = path.resolve(__dirname, '../AI/node_modules/@xenova/transformers/src/utils/tensor.js'); +if (fs.existsSync(xenovaTensorPath)) { + let data = fs.readFileSync(xenovaTensorPath, 'utf8'); + if (!data.includes('this.data = args[0].data')) { + const patched = data + .replace( + " if (args[0] instanceof ONNXTensor) {\n // Create shallow copy\n Object.assign(this, args[0]);\n", + " if (args[0] instanceof ONNXTensor) {\n // Create shallow copy\n Object.assign(this, args[0]);\n if (this.data === undefined) this.data = args[0].data;\n" + ) + .replace( + " args[2]\n ));\n }", + " args[2]\n ));\n if (this.data === undefined && this.cpuData !== undefined) this.data = this.cpuData;\n }" + ); + if (patched !== data) { + fs.writeFileSync(xenovaTensorPath, patched); + log('Patched @xenova/transformers tensor.js for onnxruntime 1.19 data getter'); + } else { + log('@xenova/transformers patch skipped: unexpected tensor.js format'); + } + } +} else { + log('@xenova/transformers patch skipped: file not found'); +} + +// === Patch ssb-conn (the scheduler prefers up-to-date peers that replicate what we follow) === +const connSchedulerPath = path.resolve(__dirname, '../server/node_modules/ssb-conn/lib/conn-scheduler.js'); +if (fs.existsSync(connSchedulerPath)) { + const data = fs.readFileSync(connSchedulerPath, 'utf8'); + const best = " .z((peers) => typeof this.ssb.oasisPeerRank === 'function' ? peers.sort((a, b) => this.ssb.oasisPeerRank(b) - this.ssb.oasisPeerRank(a)) : peers)\n"; + const worst = " .z((peers) => typeof this.ssb.oasisPeerRank === 'function' ? peers.sort((a, b) => this.ssb.oasisPeerRank(a) - this.ssb.oasisPeerRank(b)) : peers)\n"; + const connectAnchor = " .z(sortByCooldownAscending)\n .z(take(freeSlots))"; + const rotateAnchor = " .z(sortByOldestConnection)\n .z(take(1))"; + if (data.includes('this.ssb.oasisPeerRank')) { + log('ssb-conn scheduler already patched'); + } else if (data.includes(connectAnchor) && data.includes(rotateAnchor)) { + fs.writeFileSync(connSchedulerPath, data + .replace(connectAnchor, " .z(sortByCooldownAscending)\n" + best + " .z(take(freeSlots))") + .replace(rotateAnchor, " .z(sortByOldestConnection)\n" + worst + " .z(take(1))")); + log('Patched ssb-conn scheduler to rank peers by version and shared replication'); + } else { + log('ssb-conn scheduler patch skipped: unexpected conn-scheduler.js format'); + } +} else { + log('ssb-conn scheduler patch skipped: file not found'); +} + +// === Patch ssb-conn (connection limits apply to the connections we open, never to the ones others open to us) === +const connHubPath = path.resolve(__dirname, '../server/node_modules/ssb-conn-hub/lib/index.js'); +if (fs.existsSync(connHubPath) && fs.existsSync(connSchedulerPath)) { + const hub = fs.readFileSync(connHubPath, 'utf8'); + const sched = fs.readFileSync(connSchedulerPath, 'utf8'); + const hubFrom = 'this._setPeer(address, { ...data, state, disconnect });'; + const hubTo = 'this._setPeer(address, { ...data, state, disconnect, inbound: !isClient });'; + const schedFrom = 'const peersUp = query.peersConnected().filter(isDesiredPeer);'; + const schedTo = 'const peersUp = query.peersConnected().filter(isDesiredPeer).filter((p) => !p[1].inbound);'; + if (hub.includes(hubTo) && sched.includes(schedTo)) { + log('ssb-conn inbound limits already patched'); + } else if ((hub.includes(hubFrom) || hub.includes(hubTo)) && (sched.includes(schedFrom) || sched.includes(schedTo))) { + fs.writeFileSync(connHubPath, hub.replace(hubFrom, hubTo)); + fs.writeFileSync(connSchedulerPath, sched.replace(schedFrom, schedTo)); + log('Patched ssb-conn so its connection limits only apply to the connections it opens'); + } else { + log('ssb-conn inbound patch skipped: unexpected format'); + } +} else { + log('ssb-conn inbound patch skipped: file not found'); +} + +const CONN_QUOTA = 10; +const connQuotaFrom = [4, CONN_QUOTA].map((n) => `this.maintainConnections(${n}, isNotRoom, 'dbAndStaging', null, 2 * HOUR);`); +const connQuotaTo = `this.maintainConnections(typeof this.ssb.oasisConnQuota === 'function' ? this.ssb.oasisConnQuota() : ${CONN_QUOTA}, isNotRoom, 'dbAndStaging', null, 2 * HOUR);`; +if (fs.existsSync(connSchedulerPath)) { + const sched = fs.readFileSync(connSchedulerPath, 'utf8'); + const from = connQuotaFrom.find((f) => sched.includes(f)); + if (sched.includes(connQuotaTo)) { + log('ssb-conn already takes its outgoing connections from the node settings'); + } else if (from) { + fs.writeFileSync(connSchedulerPath, sched.replace(from, connQuotaTo)); + log('Patched ssb-conn to take its outgoing connections from the node settings'); + } else { + log('ssb-conn quota patch skipped: unexpected conn-scheduler.js format'); + } +} else { + log('ssb-conn quota patch skipped: file not found'); +} + +// === Patch ssb-lan (broadcast address detection throws without a private IPv4) === +const ssbLanPath = path.resolve(__dirname, '../server/node_modules/ssb-lan/lib/index.js'); +if (fs.existsSync(ssbLanPath)) { + const data = fs.readFileSync(ssbLanPath, 'utf8'); + if (!data.includes("e.family === 'IPv4' && IP.isPrivate(addr)")) { + const start = data.indexOf(' getBroadcastIPs() {'); + const end = data.indexOf('\n }\n', start); + if (start >= 0 && end > start) { + const replacement = ` getBroadcastIPs() { + if (process.platform === 'ios') + return ['255.255.255.255']; + try { + const details = nonPrivateIP(null, (addr, e) => e.family === 'IPv4' && IP.isPrivate(addr), true); + if (!details || details.family !== 'IPv4') + return ['255.255.255.255']; + const { broadcastAddress } = IP.subnet(details.address, details.netmask); + return [broadcastAddress]; + } + catch (err) { + debug('LAN broadcast address detection failed: %s', err); + return ['255.255.255.255']; + } + }`; + fs.writeFileSync(ssbLanPath, data.slice(0, start) + replacement + data.slice(end + 6)); + log('Patched ssb-lan to fall back to the global broadcast address'); + } else { + log('ssb-lan patch skipped: getBroadcastIPs not found'); + } + } +} else { + log('ssb-lan patch skipped: file not found'); +} + +// === Patch ssb-box (map leaks the index as libsodium's output format without native bindings) === +const ssbBoxPath = path.resolve(__dirname, '../server/node_modules/ssb-box/format.js'); +if (fs.existsSync(ssbBoxPath)) { + const data = fs.readFileSync(ssbBoxPath, 'utf8'); + const target = '.map(sodium.crypto_sign_ed25519_pk_to_curve25519);'; + if (data.includes(target)) { + fs.writeFileSync(ssbBoxPath, data.replace(target, '.map((pk) => sodium.crypto_sign_ed25519_pk_to_curve25519(pk));')); + log('Patched ssb-box so recipient keys are converted one argument at a time'); + } +} else { + log('ssb-box patch skipped: file not found'); +} + +// === Patch multiserver (voice frames are small and frequent: send them without Nagle's delay) === +const msNetPath = path.resolve(__dirname, '../server/node_modules/multiserver/plugins/net.js'); +if (fs.existsSync(msNetPath)) { + let data = fs.readFileSync(msNetPath, 'utf8'); + if (data.includes('setNoDelay(true)')) { + log('multiserver already patched'); + } else { + const serverTarget = ` function connectionListener(stream) { + onConnection(toDuplex(stream))`; + const clientTarget = ` .on('connect', function onConnect() { + if (started) return + started = true + cb(null, toDuplex(stream))`; + if (data.includes(serverTarget) && data.includes(clientTarget)) { + data = data + .replace(serverTarget, ` function connectionListener(stream) { + try { stream.setNoDelay(true) } catch (_) {} + onConnection(toDuplex(stream))`) + .replace(clientTarget, ` .on('connect', function onConnect() { + if (started) return + started = true + try { stream.setNoDelay(true) } catch (_) {} + cb(null, toDuplex(stream))`); + fs.writeFileSync(msNetPath, data); + log('Patched multiserver net plugin to disable Nagle on every connection'); + } else { + log('multiserver patch skipped: unexpected net plugin layout'); + } + } +} else { + log('multiserver patch skipped: file not found'); +} + +// === Patch hyperaxe (only strings and nodes become children; raw HTML only through a private symbol) === +const hyperaxePath = path.resolve(__dirname, '../server/node_modules/hyperaxe/factory.js'); +if (fs.existsSync(hyperaxePath)) { + let data = fs.readFileSync(hyperaxePath, 'utf8'); + if (data.includes('oasis.rawHtml')) { + log('hyperaxe already patched'); + } else { + const target = ` return function (props) { + return isObject(props) + ? fn(tag, props, sliceKids(arguments, 1)) + : fn(tag, sliceKids(arguments)) + }`; + if (data.includes(target)) { + data = data.replace(target, ` return function (props) { + return isObject(props) + ? fn(tag, cleanProps(props), scrubKids(sliceKids(arguments, 1))) + : fn(tag, scrubKids(sliceKids(arguments))) + }`); + data += ` +const RAW_HTML = Symbol.for('oasis.rawHtml') +function isNodeLike (v) { + return !!(v && v.nodeName && v.nodeType) +} +function scrubKids (arr) { + return arr.map(function (v) { + if (Array.isArray(v)) return scrubKids(v) + if (v !== null && typeof v === 'object' && !isNodeLike(v) && !(v instanceof Date) && !(v instanceof RegExp)) return null + return v + }) +} +function cleanProps (props) { + if (isNodeLike(props)) return props + const out = {} + for (const k of Object.keys(props)) if (k !== 'innerHTML') out[k] = props[k] + if (typeof props[RAW_HTML] === 'string') out.innerHTML = props[RAW_HTML] + return out +} +`; + fs.writeFileSync(hyperaxePath, data); + log('Patched hyperaxe so peer objects never become attributes or raw HTML'); + } else { + log('hyperaxe patch skipped: unexpected factory layout'); + } + } +} else { + log('hyperaxe patch skipped: file not found'); +} + +// === Patch html-element (standard attributes missing from its list are written instead of dropped) === +const htmlAttributesPath = path.resolve(__dirname, '../server/node_modules/html-element/html-attributes.js'); +if (fs.existsSync(htmlAttributesPath)) { + const data = fs.readFileSync(htmlAttributesPath, 'utf8'); + const anchor = 'function isStandardAttribute(attrName, tagName) {'; + const marker = 'OASIS_EXTRA_ATTRIBUTES'; + if (data.includes(marker)) { + log('html-element attributes already patched'); + } else if (data.includes(anchor)) { + const extra = `var OASIS_EXTRA_ATTRIBUTES = { + 'formmethod': ['input', 'button'], + 'formenctype': ['input', 'button'], + 'formnovalidate': ['input', 'button'], + 'formtarget': ['input', 'button'], + 'minlength': ['input', 'textarea'], + 'inputmode': 'GLOBAL', + 'capture': ['input'], + 'loading': ['img', 'iframe'], + 'decoding': ['img'], + 'crossorigin': ['audio', 'img', 'link', 'script', 'video'], + 'referrerpolicy': ['a', 'area', 'iframe', 'img', 'link', 'script'], + 'playsinline': ['video'], + 'role': 'GLOBAL', + 'label': ['option', 'optgroup', 'track'] +}; +Object.keys(OASIS_EXTRA_ATTRIBUTES).forEach(function (name) { + var tags = OASIS_EXTRA_ATTRIBUTES[name]; + var current = HTML_ATTRIBUTES[name]; + if (tags === 'GLOBAL' || current === 'GLOBAL') { HTML_ATTRIBUTES[name] = 'GLOBAL'; return; } + var set = current instanceof Set ? current : new Set(); + tags.forEach(function (t) { set.add(t); }); + HTML_ATTRIBUTES[name] = set; +}); + +`; + fs.writeFileSync(htmlAttributesPath, data.replace(anchor, extra + anchor)); + log('Patched html-element so standard attributes such as formmethod, minlength and loading are written'); + } else { + log('html-element attributes patch skipped: unexpected html-attributes.js format'); + } +} else { + log('html-element attributes patch skipped: file not found'); +} + +// === Patch ssb-invite (local read of how many uses each invite has and how many are spent) === +const ssbInvitePath = path.resolve(__dirname, '../server/node_modules/ssb-invite/index.js'); +const ssbInviteManifestPath = path.resolve(__dirname, '../server/node_modules/ssb-invite/manifest.json'); +if (fs.existsSync(ssbInvitePath) && fs.existsSync(ssbInviteManifestPath)) { + const data = fs.readFileSync(ssbInvitePath, 'utf8'); + const anchor = " }, 'string')\n }\n }\n}"; + if (data.includes('codesDB.createReadStream')) { + log('ssb-invite already patched'); + } else if (data.includes(anchor)) { + const list = ` }, 'string'), + list: function (cb) { + const out = [] + let done = false + const finish = (err) => { if (done) return; done = true; err ? cb(err) : cb(null, out) } + codesDB.createReadStream() + .on('data', (d) => { const v = d.value || {}; out.push({ id: d.key, total: +v.total || 0, used: +v.used || 0 }) }) + .on('error', finish) + .on('end', () => finish()) + } + } + } +}`; + fs.writeFileSync(ssbInvitePath, data.replace(anchor, list)); + const manifest = JSON.parse(fs.readFileSync(ssbInviteManifestPath, 'utf8')); + manifest.list = 'async'; + fs.writeFileSync(ssbInviteManifestPath, JSON.stringify(manifest, null, 2) + '\n'); + log('Patched ssb-invite with a local list of invite uses'); + } else { + log('ssb-invite patch skipped: unexpected index.js layout'); + } +} else { + log('ssb-invite patch skipped: file not found'); +} + +// === Patch on-wakeup (a busy event loop is not a wake-up: only time the machine spent suspended counts) === +const onWakeupPath = path.resolve(__dirname, '../server/node_modules/on-wakeup/index.js'); +if (fs.existsSync(onWakeupPath)) { + const data = fs.readFileSync(onWakeupPath, 'utf8'); + const tickFrom = ` var t = Date.now() + if (lastSleepCheck && (t - lastSleepCheck) > SLEEP_CHECK_INTERVAL*NUM_MISSABLE_INTERVALS) + emitter.emit('wakeup') // missed NUM_MISSABLE_INTERVALS checks, let's run the callbacks + lastSleepCheck = t`; + const tickTo = ` var t = Date.now() + var hr = process.hrtime() + var awake = hr[0] * 1e3 + hr[1] / 1e6 + var gap = t - lastSleepCheck + var suspended = process.platform === 'win32' ? gap : gap - (awake - lastAwake) + if (lastSleepCheck && gap > SLEEP_CHECK_INTERVAL*NUM_MISSABLE_INTERVALS && suspended > SLEEP_CHECK_INTERVAL*(NUM_MISSABLE_INTERVALS-1)) + emitter.emit('wakeup') + lastSleepCheck = t + lastAwake = awake`; + const varsFrom = 'var lastSleepCheck = false\n'; + const varsTo = 'var lastSleepCheck = false\nvar lastAwake = 0\n'; + if (data.includes('var lastAwake = 0')) { + log('on-wakeup already patched'); + } else if (data.includes(tickFrom) && data.includes(varsFrom)) { + fs.writeFileSync(onWakeupPath, data.replace(varsFrom, varsTo).replace(tickFrom, tickTo)); + log('Patched on-wakeup so a long busy stretch is not taken for a wake-up'); + } else { + log('on-wakeup patch skipped: unexpected index.js layout'); + } +} else { + log('on-wakeup patch skipped: file not found'); +} + +// === Patch ssb-db2 (createHistoryStream returns a feed in arrival order, which is its sequence order, not by timestamp) === +const historyStreamPath = path.resolve(__dirname, '../server/node_modules/ssb-db2/compat/history-stream.js'); +if (fs.existsSync(historyStreamPath)) { + const data = fs.readFileSync(historyStreamPath, 'utf8'); + const importFrom = "const { author, and, gte, where, batch, toPullStream } = require('../operators')"; + const importTo = "const { author, and, gte, where, batch, sortByArrival, toPullStream } = require('../operators')"; + const queryFrom = ' where(query),\n'; + const queryTo = ' where(query),\n sortByArrival(),\n'; + if (data.includes(importTo)) { + log('ssb-db2 history stream already patched'); + } else if (data.includes(importFrom) && data.split(queryFrom).length === 3) { + fs.writeFileSync(historyStreamPath, data.replace(importFrom, importTo).split(queryFrom).join(queryTo)); + log('Patched ssb-db2 so createHistoryStream keeps sequence order'); + } else { + log('ssb-db2 history stream patch skipped: unexpected history-stream.js layout'); + } +} else { + log('ssb-db2 history stream patch skipped: file not found'); +} + +// === Patch ssb-invite-client (a pub that already follows us is not a failed invite: keep the pub and finish joining) === +const inviteClientPath = path.resolve(__dirname, '../server/node_modules/ssb-invite-client/lib/index.js'); +if (fs.existsSync(inviteClientPath)) { + const data = fs.readFileSync(inviteClientPath, 'utf8'); + const useFrom = ` const [e2] = await run(rpc.invite.use)({ feed: this.ssb.id }); + if (e2) + return cb(clarify(e2, 'Invite not accepted by the pub')); + if (shouldPublish) {`; + const useTo = ` const [e2] = await run(rpc.invite.use)({ feed: this.ssb.id }); + const knownToPub = !!e2 && /already following/i.test(String((e2 && e2.message) || e2)); + if (e2 && !knownToPub) + return cb(clarify(e2, 'Invite not accepted by the pub')); + let followsPub = false; + if (knownToPub && this.ssb.friends && typeof this.ssb.friends.isFollowing === 'function') { + const [, yes] = await run(this.ssb.friends.isFollowing)({ source: this.ssb.id, dest: parsed.key }); + followsPub = !!yes; + } + if (shouldPublish && !followsPub) {`; + if (data.includes('knownToPub')) { + log('ssb-invite-client already patched'); + } else if (data.includes(useFrom)) { + fs.writeFileSync(inviteClientPath, data.replace(useFrom, useTo)); + log('Patched ssb-invite-client so a pub that already follows us still gets joined'); + } else { + log('ssb-invite-client patch skipped: unexpected lib/index.js layout'); + } +} else { + log('ssb-invite-client patch skipped: file not found'); +} diff --git a/src/server/SSB_server.js b/src/server/SSB_server.js index ea12deb..281710f 100644 --- a/src/server/SSB_server.js +++ b/src/server/SSB_server.js @@ -80,7 +80,7 @@ const Server = SecretStack({ caps }) .use(require('ssb-unix-socket')) .use(require('ssb-no-auth')) .use(require('./snapshot_plugin')) - .use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); } } catch (_) {} return pm; })()) + .use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); pm.setStreamFactory(require('./phone_stream_android')); pm.setScreenFactory(require('./phone_screen_android')); } } catch (_) {} return pm; })()) .use(require('./network_pause')); if (!config.pub) { diff --git a/src/server/network_pause.js b/src/server/network_pause.js index 9b92677..de94770 100644 --- a/src/server/network_pause.js +++ b/src/server/network_pause.js @@ -1,5 +1,8 @@ const fs = require('fs'); const path = require('path'); +const { connectionsOf } = require('../configs/config-manager'); + +const QUOTA_TTL_MS = 5000; const readOasisConfig = () => { try { @@ -15,6 +18,12 @@ module.exports = { manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' }, init(server, config) { server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; + let quota = connectionsOf(readOasisConfig().connections); + let quotaAt = Date.now(); + server.oasisConnQuota = () => { + if (Date.now() - quotaAt > QUOTA_TTL_MS) { quota = connectionsOf(readOasisConfig().connections); quotaAt = Date.now(); } + return quota; + }; const refused = () => new Error('network paused'); if (server.auth && typeof server.auth.hook === 'function') { @@ -46,18 +55,26 @@ module.exports = { const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/); return canonicalKey((ev && ev.key) || (m ? m[1] : '')); }; - const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0); - const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })(); - const upToDate = (v) => { - const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion); - return a1 > a2 || (a1 === a2 && b1 >= b2); + const versionParts = (v) => String(v || '').split('.').slice(0, 3).map(n => parseInt(n, 10) || 0); + const compareVersions = (a, b) => { + const x = versionParts(a), y = versionParts(b); + for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]; + return 0; + }; + const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })(); + const rank = { versions: new Map(), graph: {}, newest: ownVersion }; + const versionTier = (v, total) => { + if (!v) return total > 0 ? 0 : 2; + if (compareVersions(v, rank.newest) >= 0) return 4; + const [a1, b1] = versionParts(v), [a2, b2] = versionParts(rank.newest); + return a1 === a2 && b1 === b2 ? 3 : 1; }; - const rank = { versions: new Map(), graph: {} }; const refreshRank = () => { const pull = require('pull-stream'); const latest = new Map(); const done = () => { rank.versions = new Map([...latest].map(([k, m]) => [k, m.version])); + rank.newest = [...rank.versions.values()].reduce((n, v) => (compareVersions(v, n) > 0 ? v : n), ownVersion); try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {} }; try { @@ -83,9 +100,8 @@ module.exports = { total++; if (mine[k] >= 0) shared++; } - const v = rank.versions.get(key); - const tier = v ? (upToDate(v) ? 2 : 0) : (total > 0 ? 0 : 1); - return tier * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999); + const carriesMe = follows[server.id] >= 0 ? 1 : 0; + return carriesMe * 1e12 + versionTier(rank.versions.get(key), total) * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999); }; server.oasisPeerRank = peerRank; setImmediate(refreshRank); diff --git a/src/server/package-lock.json b/src/server/package-lock.json index 734be1b..8bbd94e 100644 --- a/src/server/package-lock.json +++ b/src/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@krakenslab/oasis", - "version": "1.3.3", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@krakenslab/oasis", - "version": "1.2.3", + "version": "1.3.4", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -31,6 +31,7 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", + "mupdf": "^1.28.1", "open": "^8.4.2", "openpgp": "^6.3.0", "opusscript": "^0.1.1", @@ -4961,6 +4962,12 @@ "non-private-ip": "^2.0.0" } }, + "node_modules/mupdf": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/mupdf/-/mupdf-1.28.1.tgz", + "integrity": "sha512-Gi11Ow2G1SlrXKJNZBL1eAIGFVih5+4ZKqjptamTVaj/5hnlrcVrVbyb7lHE2lfFKdxZPyv9ZtfOOq7XgjzEig==", + "license": "AGPL-3.0-or-later" + }, "node_modules/mutexify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz", diff --git a/src/server/package.json b/src/server/package.json index 3acea84..66661ad 100644 --- a/src/server/package.json +++ b/src/server/package.json @@ -1,6 +1,6 @@ { "name": "@krakenslab/oasis", - "version": "1.3.3", + "version": "1.3.5", "description": "Oasis - Social Networking Utopia", "repository": { "type": "git", @@ -18,7 +18,7 @@ "start:ssb": "node SSB_server.js start &", "dev": "npm run dev:backend", "dev:backend": "npx --yes nodemon@3.1.14", - "postinstall": "node ../../scripts/patch-node-modules.js" + "postinstall": "node ../patches/patch-node-modules.js" }, "dependencies": { "@koa/router": "^13.1.0", @@ -42,6 +42,7 @@ "minimist": "^1.2.8", "module-alias": "^2.2.3", "moment": "^2.30.1", + "mupdf": "^1.28.1", "open": "^8.4.2", "openpgp": "^6.3.0", "opusscript": "^0.1.1", diff --git a/src/server/phone_module.js b/src/server/phone_module.js index 32a6f41..dec72a3 100644 --- a/src/server/phone_module.js +++ b/src/server/phone_module.js @@ -1,7 +1,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const { spawn } = require('child_process'); +const { spawn, execFile } = require('child_process'); const nodeCrypto = require('crypto'); const pull = require('./node_modules/pull-stream'); const Pushable = require('./node_modules/pull-pushable'); @@ -64,6 +64,10 @@ const VIDEO_FRAME_MAX = 256 * 1024; const VIDEO_FPS = 8; const VIDEO_SIZE = { w: 320, h: 240 }; const STREAM_SIZE = { w: 640, h: 480 }; +const SCREEN_SIZE = { w: 1280, h: 720 }; +const SCREEN_FPS = 2; +const SCREEN_QUALITY = 12; +const SCREEN_PROBE_MS = 5000; const GRID_TILE_LARGE = { w: 640, h: 480 }; const GRID_LARGE_MAX = 4; const ROOM_VIDEO_CHUNK_EXTRA = 45; @@ -245,46 +249,76 @@ const STREAM_BACKENDS = [ ]; const STREAM_STALL_MS = 5000; const STREAM_START_MS = 20000; +const frameProcess = ({ bin, args, retry }, { onJpeg, onPcm, onEnd }) => { + let proc = null; + let frames = 0; + let lastAt = 0; + let ended = false; + const startedAt = Date.now(); + const watch = setInterval(() => { + const now = Date.now(); + if (frames ? now - lastAt > STREAM_STALL_MS : now - startedAt > STREAM_START_MS) finish(); + }, 1000); + if (watch.unref) watch.unref(); + const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} }; + const finish = () => { if (ended) return; halt(); onEnd(); }; + const run = (first) => { + const p = spawn(bin, args(first), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] }); + proc = p; + p.on('error', () => {}); + p.stdout.on('error', () => {}); + p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); })); + if (p.stdio[3]) { + p.stdio[3].on('error', () => {}); + p.stdio[3].on('data', (pcm) => { if (!ended && proc === p && onPcm) onPcm(pcm); }); + } + p.on('close', () => { + if (ended || proc !== p) return; + if (first && !frames && retry) return run(false); + finish(); + }); + }; + run(true); + return { stop() { if (!ended) halt(); } }; +}; const systemStream = () => { const backend = STREAM_BACKENDS.find(b => hasBin(b.bin())); if (!backend) return null; return { - open(file, { onJpeg, onPcm, onEnd }) { - let proc = null; - let frames = 0; - let lastAt = 0; - let ended = false; - const startedAt = Date.now(); - const watch = setInterval(() => { - const now = Date.now(); - if (frames ? now - lastAt > STREAM_STALL_MS : now - startedAt > STREAM_START_MS) finish(); - }, 1000); - if (watch.unref) watch.unref(); - const halt = () => { ended = true; clearInterval(watch); try { if (proc) proc.kill('SIGTERM'); } catch (_) {} }; - const finish = () => { if (ended) return; halt(); onEnd(); }; - const run = (withAudio) => { - const p = spawn(backend.bin(), backend.args(file, withAudio), { stdio: ['ignore', 'pipe', 'ignore', 'pipe'] }); - proc = p; - p.on('error', () => {}); - p.stdout.on('error', () => {}); - p.stdout.on('data', jpegSplitter((jpeg) => { if (ended || proc !== p) return; frames++; lastAt = Date.now(); onJpeg(jpeg); })); - if (p.stdio[3]) { - p.stdio[3].on('error', () => {}); - p.stdio[3].on('data', (pcm) => { if (!ended && proc === p) onPcm(pcm); }); - } - p.on('close', () => { - if (ended || proc !== p) return; - if (withAudio && !frames && backend.retryWithoutAudio) return run(false); - finish(); - }); - }; - run(true); - return { stop() { if (!ended) halt(); } }; + open(file, handlers) { + return frameProcess({ bin: backend.bin(), args: (withAudio) => backend.args(file, withAudio), retry: !!backend.retryWithoutAudio }, handlers); } }; }; let streamFactory = systemStream; +const x11Screen = (env) => (env.DISPLAY && !env.WAYLAND_DISPLAY && String(env.XDG_SESSION_TYPE || '').toLowerCase() !== 'wayland' + ? { device: 'x11grab', input: ['-f', 'x11grab', '-framerate', String(SCREEN_FPS), '-i', env.DISPLAY] } + : null); +const SCREEN_SOURCES = { + win32: () => ({ device: 'gdigrab', input: ['-f', 'gdigrab', '-framerate', String(SCREEN_FPS), '-i', 'desktop'] }), + darwin: () => ({ device: 'avfoundation', input: ['-f', 'avfoundation', '-capture_cursor', '1', '-i', 'Capture screen 0:none'] }), + android: () => null +}; +const screenSource = (platform = process.platform, env = process.env) => (SCREEN_SOURCES[platform] || x11Screen)(env || {}); +const screenArgs = (source) => [ + '-loglevel', 'quiet', ...source.input, + '-vf', `fps=${SCREEN_FPS},scale=${SCREEN_SIZE.w}:${SCREEN_SIZE.h}:force_original_aspect_ratio=decrease:flags=lanczos,pad=${SCREEN_SIZE.w}:${SCREEN_SIZE.h}:(ow-iw)/2:(oh-ih)/2`, + '-c:v', 'mjpeg', '-q:v', String(SCREEN_QUALITY), '-f', 'image2pipe', 'pipe:1' +]; +const systemScreen = () => { + const source = screenSource(); + const bin = process.env.OASIS_FFMPEG || 'ffmpeg'; + if (!source || !hasBin(bin)) return null; + let ready = false; + execFile(bin, ['-hide_banner', '-devices'], { timeout: SCREEN_PROBE_MS }, (err, out) => { ready = !err && String(out || '').split(/\s+/).includes(source.device); }); + return { + available: () => ready, + open(handlers) { return frameProcess({ bin, args: () => screenArgs(source), retry: false }, { onJpeg: handlers.onJpeg, onEnd: handlers.onEnd }); } + }; +}; +let screenFactory = systemScreen; + const videoChunks = (seq, jpeg, track = 0) => { const n = Math.ceil(jpeg.length / VIDEO_CHUNK); const out = []; @@ -526,6 +560,7 @@ module.exports = { const roomQuietAt = Number(policy().roomQuietAt) > 0 ? Number(policy().roomQuietAt) : ROOM_QUIET_AT; const video = videoFactory(server, policy().videoDevice); const streamer = streamFactory(server); + const screener = screenFactory(server); const videoAllowed = () => policy().video !== false; const videoOn = () => !!video && videoAllowed(); const codecsOffered = () => [...(OpusScript && policy().opus !== false ? ['opus'] : []), ...(videoOn() ? ['mjpeg'] : [])]; @@ -1120,6 +1155,7 @@ module.exports = { secure: !!r.kid, count: humansIn(r) + 1, max: r.max, public: [...r.peers.values()].some(p => p.viewer), speaking: now - r.sentAt < 1000, cam: !!r.cam, videoAvailable: videoOn() && r.hubVideo === true, grid: !!sharpLib, gridOrder: gridIds(r), streaming: r.stream ? r.stream.name : null, streamAvailable: fileStreamOn() && r.hubVideo === true, + sharingScreen: !!(r.stream && r.stream.screen), screenAvailable: screenOn() && r.hubVideo === true, notify: !!r.notify, recording: !!r.rec, recordingSince: r.rec ? r.rec.at : 0, recordingBy: [...r.recPeers], events: r.notify ? r.events.slice(-ROOM_EVENTS_MAX) : [], peers: [...r.peers.values()].map(p => ({ id: p.id, speaking: now - p.heardAt < 1000, muted: now - p.mutedAt < MUTE_TTL_MS, hand: now - (p.handBeat || 0) < MUTE_TTL_MS, handSince: p.handSince || 0, silenced: silenced.has(p.id), recording: r.recPeers.has(p.id), cam: now - (p.frameAt || 0) < VIDEO_HOLD_MS, film: now - (p.filmAt || 0) < VIDEO_HOLD_MS, viewer: !!p.viewer })) @@ -1396,6 +1432,7 @@ module.exports = { r.out.push(Buffer.concat([Buffer.from([K_VIDEO]), Buffer.from(r.kid, 'hex'), r.sealer(plain)])); }; const fileStreamOn = () => videoAllowed() && !!streamer; + const screenOn = () => videoAllowed() && !!screener && (typeof screener.available !== 'function' || !!screener.available()); const streamFile = (file) => { const raw = String(file || ''); if (!path.isAbsolute(raw)) return null; @@ -1427,13 +1464,12 @@ module.exports = { if (notify) roomChanged(); return true; }; - const startRoomStream = (r, file, name) => { - if (!r || r.phase !== 'live' || !r.hubVideo || !fileStreamOn()) return false; + const startFilm = (r, name, screen, open) => { stopRoomStream(r, false); - const s = { name: String(name || '').slice(0, 120), pcm: Buffer.alloc(0), seq: 0, handle: null }; + const s = { name: String(name || '').slice(0, 120), screen, pcm: Buffer.alloc(0), seq: 0, handle: null }; r.stream = s; try { - s.handle = streamer.open(file, { + s.handle = open({ onJpeg: (jpeg) => { if (r.stream !== s || room !== r || !Buffer.isBuffer(jpeg) || jpeg.length > VIDEO_FRAME_MAX) return; s.seq = (s.seq + 1) & 0xffff; @@ -1442,7 +1478,7 @@ module.exports = { sendRoomJpeg(r, s.seq, jpeg, 1); }, onPcm: (pcm) => { - if (r.stream !== s || !pcm || !pcm.length) return; + if (r.stream !== s || screen || !pcm || !pcm.length) return; s.pcm = s.pcm.length ? Buffer.concat([s.pcm, pcm]) : Buffer.from(pcm); const drop = (s.pcm.length - RATE * 2) & ~1; if (drop > 0) s.pcm = s.pcm.subarray(drop); @@ -1453,6 +1489,9 @@ module.exports = { if (!s.handle || r.stream !== s) { if (r.stream === s) r.stream = null; return false; } return true; }; + const filmReady = (r) => !!r && r.phase === 'live' && !!r.hubVideo; + const startRoomStream = (r, file, name) => filmReady(r) && fileStreamOn() && startFilm(r, name, false, (h) => streamer.open(file, h)); + const startRoomScreen = (r) => filmReady(r) && screenOn() && startFilm(r, '', true, (h) => screener.open(h)); const streamFrame = (r) => { const s = r.stream; if (!s || s.pcm.length < FRAME_BYTES) return null; @@ -1798,6 +1837,11 @@ module.exports = { const r = room; if (!r || r.phase !== 'live') return cb(new Error('no-room')); if (o.stop) { stopRoomStream(r); return cb(null, roomSnapshot()); } + if (o.screen) { + if (!startRoomScreen(r)) return cb(new Error('unavailable')); + roomChanged(); + return cb(null, roomSnapshot()); + } const file = streamFile(o.file); if (!file) return cb(new Error('invalid')); if (!startRoomStream(r, file, o.name || path.basename(file))) return cb(new Error('unavailable')); @@ -2108,6 +2152,8 @@ module.exports = { module.exports.setAudioFactory = (factory) => { audioFactory = typeof factory === 'function' ? factory : systemAudio; }; module.exports.setVideoFactory = (factory) => { videoFactory = typeof factory === 'function' ? factory : systemVideo; }; module.exports.setStreamFactory = (factory) => { streamFactory = typeof factory === 'function' ? factory : systemStream; }; +module.exports.setScreenFactory = (factory) => { screenFactory = typeof factory === 'function' ? factory : systemScreen; }; +module.exports.screenSource = screenSource; module.exports.jpegSplitter = jpegSplitter; module.exports.VIDEO_CHUNK = VIDEO_CHUNK; module.exports.VIDEO_HOLD_MS = VIDEO_HOLD_MS; diff --git a/src/server/phone_screen_android.js b/src/server/phone_screen_android.js new file mode 100644 index 0000000..cda05c5 --- /dev/null +++ b/src/server/phone_screen_android.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +const net = require('net'); +const path = require('path'); +const { jpegSplitter } = require('./phone_module'); + +const MARKER = path.join(__dirname, '..', '..', '.audio_bridge'); + +const bridgePort = () => { + try { + const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim()); + return Number.isFinite(port) && port > 0 ? port : 0; + } catch (_) { + return 0; + } +}; + +module.exports = () => ({ + available: () => bridgePort() > 0, + open({ onJpeg, onEnd }) { + const port = bridgePort(); + if (!port) return null; + let ended = false; + const finish = () => { + if (ended) return; + ended = true; + try { onEnd(); } catch (_) {} + }; + const sock = net.connect(port, '127.0.0.1'); + sock.setNoDelay(true); + sock.on('error', finish); + sock.on('close', finish); + sock.on('connect', () => sock.write('C')); + sock.on('data', jpegSplitter((jpeg) => { if (!ended) onJpeg(jpeg); })); + return { stop() { ended = true; try { sock.destroy(); } catch (_) {} } }; + } +}); diff --git a/src/server/ssb_config.js b/src/server/ssb_config.js index 881a94d..c0163b7 100644 --- a/src/server/ssb_config.js +++ b/src/server/ssb_config.js @@ -46,6 +46,14 @@ config.blobs.max = 75 * megabyte; config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) }; +const envPort = Number(process.env.ssb_port); +if (Number.isInteger(envPort) && envPort > 0 && envPort < 65536) { + config.port = envPort; + for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) { + if (entry && typeof entry === 'object') entry.port = envPort; + } +} + const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })(); const bindHost = config.pub ? '0.0.0.0' : lanOn ? null : '127.0.0.1'; for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) { diff --git a/src/server/ssb_metadata.js b/src/server/ssb_metadata.js index 1646ebd..5eb9e3f 100644 --- a/src/server/ssb_metadata.js +++ b/src/server/ssb_metadata.js @@ -74,7 +74,7 @@ async function checkForUpdate() { await updater.getRemoteVersion(); } -async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', offline = false, isPublic = false) { +async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, httpHost = 'localhost', isPublic = false) { if (printed) return; printed = true; diff --git a/src/views/activity_view.js b/src/views/activity_view.js index 2e852fc..659eb52 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,6 +1,6 @@ const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); -const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction } = require('./main_views'); +const { template, i18n, paged, userLink, userLinkLabel, renderStateChip, renderPhoneChip, renderSpreadButton, renderContentActions, renderCardTop, renderShareButton, renderOpinionsPill, renderEngagement, commentCountFor, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, contentDeleteAction, renderPdfDocument, forumReplyAnchor, renderCommentsSummary, commentsOpenHref } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); const { roomNumberOf } = require('../models/phone_number'); const mobileUi = require("./mobile_ui"); @@ -200,7 +200,9 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { .sort((a, b) => (a.ts || 0) - (b.ts || 0)); if (!rootAction) continue; - const rootType = rootAction.type || 'post'; + const rootUnknown = !byId.has(threadId) && getThreadIdFromPost(rootAction) !== safeMsgId(rootAction); + const located = rootUnknown ? commentOf(rootAction) : null; + const rootType = rootUnknown ? ((located && located.rootType) || 'comment') : (rootAction.type || 'post'); out.push({ id: `thread:${threadId}`, type: 'postThread', @@ -209,7 +211,7 @@ function buildActivityItemsWithPostThreads(deduped, allActions) { content: { threadId, rootType, - rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : getViewDetailsAction(rootType, rootAction), + rootHref: rootType === 'post' ? `/blogs/${encodeURIComponent(threadId)}` : rootUnknown ? commentHref({ rootId: threadId, rootType: located && located.rootType }) : getViewDetailsAction(rootType, rootAction), root: rootAction ? { id: safeMsgId(rootAction), @@ -256,12 +258,15 @@ const renderTribeOpenedCard = (action, date, authorName) => { : null; const bookmarkHref = c.mediaType === 'bookmark' ? safeExternalHref(c.url) : ''; return div({ class: 'trending-card' }, - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, + renderCardTop(null, origin.href, { + author: action.author, + authorName, + date: action.ts, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, label)), renderTribeOriginChip(origin) - ) - ), + )] + }), div({ class: 'card-section' }, c.title ? h2(origin.href ? a({ href: origin.href }, c.title) : c.title) : null, c.description ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(c.description).slice(0, 600))) : null, @@ -269,10 +274,7 @@ const renderTribeOpenedCard = (action, date, authorName) => { media, bookmarkHref ? a({ href: bookmarkHref, target: '_blank', rel: 'noopener noreferrer' }, c.url) : null ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date}`), - userLink(action.author, authorName) - ) + origin.href ? renderCardMetaRow(renderShareButton(safeMsgId(action.id || action.key), origin.href, c.title || '')) : null ); }; @@ -404,6 +406,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e headerText = `[${String(i18n.typePodcast || 'PODCAST').toUpperCase()} ยท ${String(i18n.typePodcastEpisode || 'EPISODE').toUpperCase()}]`; } else if (type === 'larpHousePost') { headerText = `[${String(i18n.typeLarp || 'L.A.R.P.').toUpperCase()}]`; + } else if (type === 'post' && commentOf(action)) { + headerText = `[${String(commentLabel(commentOf(action))).toUpperCase()}]`; } else { const typeLabel = i18n[`type${capitalize(type)}`] || type; headerText = `[${String(typeLabel).toUpperCase()}]`; @@ -417,7 +421,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const commentCount = typeof action.commentCount === 'number' ? action.commentCount - : (typeof content.commentCount === 'number' ? content.commentCount : 0); + : (typeof content.commentCount === 'number' ? content.commentCount : commentCountFor([action.id, action.rootId, action.key])); const votesList = votes && typeof votes === 'object' ? Object.entries(votes).map(([option, count]) => ({ option, count })) @@ -517,7 +521,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }), span(`L.A.R.P ยท ${String(house).toUpperCase()}`) ) : null, - text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text))) : null + text ? p({ class: 'post-text post-text-pre' }, ...renderStyledText(String(text), { zoomImages: true })) : null ) ); } @@ -699,11 +703,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section document' }, title?.trim() ? h2({ class: 'document-title' }, title) : "", - div({ - id: `pdf-container-${key || url}`, - class: 'pdf-viewer-container', - 'data-pdf-url': `/blob/${encodeURIComponent(url)}` - }) + renderPdfDocument(`/blob/${encodeURIComponent(url)}`, { title }) ) ); } @@ -813,13 +813,14 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e bodyNode = div({ class: 'feed-text post-text post-text-clamped', [RAW_HTML]: sanitizeHtml(linkified) }); } else { bodyNode = div({ class: 'feed-text' }, - p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText)) + p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderStyledText(displayText, { zoomImages: true })) ); } const threadId = getThreadIdFromPost(action); const replyToId = getReplyToIdFromPost(action, byIdAll); const isReply = !!(threadId && threadId !== action.id); - const ctxHref = isReply ? `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(replyToId || threadId)}` : ''; + const onContent = commentOf(action) && commentOf(action).rootType && commentOf(action).rootType !== 'post' ? commentOf(action) : null; + const ctxHref = !isReply ? '' : onContent ? commentHref({ rootId: threadId, rootType: onContent.rootType }) : `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(replyToId || threadId)}`; const parent = isReply ? (byIdAll.get(replyToId) || byIdAll.get(threadId)) : null; const parentContent = parent ? (parent.value?.content || parent.content || {}) : {}; const parentAuthor = parent?.author || ''; @@ -853,10 +854,8 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const rootType = c.rootType || 'post'; const isBlogThread = rootType === 'post'; const baseHref = c.rootHref || `/blogs/${encodeURIComponent(threadId)}`; - const href = isBlogThread ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`; - const rootTypeLabel = isBlogThread - ? (i18n.typePost || 'BLOG') - : (i18n[`type${rootType.charAt(0).toUpperCase()}${rootType.slice(1)}`] || rootType); + const href = isBlogThread || baseHref.includes('comments=open') ? baseHref : `${baseHref}${baseHref.includes('?') ? '&' : '?'}comments=open#comments-latest`; + const threadLabel = rootType === 'comment' ? i18n.typeComment : `${rootTypeLabel(rootType)} ยท ${i18n.typeComment}`; const root = c.root; const replies = Array.isArray(c.replies) ? c.replies : []; const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)); @@ -866,19 +865,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ? (titleSource.length > 90 ? `${titleSource.slice(0, 90)}โ€ฆ` : titleSource) : threadId; return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, + renderCardTop(threadId, baseHref, { + bylineAuthor: action.author, + authorName: action.authorNames && action.authorNames[action.author], + date: action.ts, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, `${String(rootTypeLabel).toUpperCase()} ยท ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) + span({ class: 'pm-exposition-text' }, String(threadLabel).toUpperCase()) ) - ), - renderContentActions(threadId, baseHref, { - author: c.rootAuthor || (root && root.author) || action.author, - reportTitle: titleText, - spread: spreadMap.get(threadId) || null, - ...favOptsFor(rootType, threadId, extras) - }) - ), + )], + author: c.rootAuthor || (root && root.author) || action.author, + reportTitle: titleText, + spread: spreadMap.get(threadId) || null, + ...favOptsFor(rootType, threadId, extras) + }), div({ class: 'card-body' }, div({ class: 'card-section' }, div({ class: 'card-field activity-update-title' }, @@ -887,15 +887,12 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ), latest && latest.text ? div({ class: 'feed-text activity-update-msg' }, - p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text)) + p({ class: 'post-text post-text-pre' }, ...renderStyledText(latest.text, { zoomImages: true })) ) : '' ) ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) + renderCardMetaRow(renderShareButton(threadId, baseHref, titleText)) ); } @@ -910,19 +907,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e const limit = 2; const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit)); return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, + renderCardTop(chatRoot, href, { + bylineAuthor: action.author, + authorName: action.authorNames && action.authorNames[action.author], + date: action.ts, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.activityUpdateLabel || 'UPDATE').toUpperCase()}`) ) - ), - renderContentActions(chatRoot, href, { - author: c.chatAuthor || action.author, - reportTitle: chatTitle, - spread: spreadMap.get(chatRoot) || null, - ...favOptsFor('chat', chatRoot, extras) - }) - ), + )], + author: c.chatAuthor || action.author, + reportTitle: chatTitle, + spread: spreadMap.get(chatRoot) || null, + ...favOptsFor('chat', chatRoot, extras) + }), div({ class: 'card-body' }, div({ class: 'card-section chat' }, div({ class: 'card-field activity-update-title' }, @@ -947,13 +945,13 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e : renderZoomableImage(attSrc, { imgClass: 'post-image' })) : renderMediaBlob(latest.image); const textNode = latest.text - ? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text)) + ? p({ class: 'post-text post-text-pre' }, ...(latest.poll ? [span({ class: 'card-label' }, `${String(i18n.pollInChat || 'Poll').toUpperCase()}: `)] : []), ...renderStyledText(latest.text, { zoomImages: true })) : null; const quoted = latest.reply || null; const replyNode = quoted ? div({ class: 'reply-context' }, span({ class: 'reply-context-meta' }, - a({ href: `${href}#msg-${String(quoted.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), + a({ href: `${href}#${forumReplyAnchor(quoted.id)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'), quoted.author ? span(' ', userLink(quoted.author, action.authorNames && action.authorNames[quoted.author])) : '' ), quoted.text ? p({ class: 'post-text reply-context-text' }, quoted.text) : '' @@ -965,10 +963,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e })() ) ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) + renderCardMetaRow(renderShareButton(chatRoot, href, chatTitle)) ); } @@ -1001,13 +996,13 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e div( { class: 'reply-context' }, span({ class: 'reply-context-meta' }, - a({ href: `/forum/${encodeURIComponent(hrefKey)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread), + a({ href: `/forum/${encodeURIComponent(hrefKey)}#${forumReplyAnchor(action.tipId || action.id)}`, class: 'tag-link' }, repliedAuthor ? i18n.inReplyTo : i18n.forumInThread), repliedAuthor ? span(' ', userLink(repliedAuthor, repliedName)) : '' ), parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : '' ), div({ class: 'card-field forum-reply-body' }, - p({ class: 'forum-reply-text' }, ...renderStyledText(text)) + p({ class: 'forum-reply-text' }, ...renderStyledText(text, { zoomImages: true })) ) ) ); @@ -1050,7 +1045,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '', spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '', spreadExcerpt - ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt)) + ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderStyledText(spreadExcerpt, { zoomImages: true })) : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'), spreadOriginalAuthor ? div({ class: 'card-field' }, @@ -1315,7 +1310,6 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e cardBody.push( div({ class: 'card-section' }, div({ class: 'card-field' }, emergencyKey ? a({ href: `/emergencies/${encodeURIComponent(emergencyKey)}`, class: 'card-value user-link' }, emergencyTitle || emergencyKey) : span({ class: 'card-value' }, emergencyTitle || '')), - content.status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.statusLabel || 'Status') + ':'), span({ class: 'card-value' }, String(content.status).toUpperCase())) : '', content.category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.emergencyCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, String(content.category).toUpperCase())) : '', renderMediaBlob(content.text, null), stripMediaMarkdown(content.text) ? p({ class: 'tribe-description' }, ...renderStyledText(stripMediaMarkdown(content.text))) : '' @@ -1526,19 +1520,20 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e .replace('%ACTION%', () => `${escapeHtml(actionWord)}`); return div({ class: 'card card-rpg' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`), - form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` }, - button({ type: "submit", class: "filter-btn" }, i18n.viewDetails) - ) - ), + renderCardTop(null, `/projects/${encodeURIComponent(action.tipId || action.id)}`, { + author: action.author, + authorName: action.authorNames && action.authorNames[action.author], + date: action.ts, + chips: [div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, (i18n.typeProject || 'PROJECT').toUpperCase()) + ) + )] + }), div( p({ [RAW_HTML]: sanitizeHtml(msgHtml) }) ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${action.ts ? moment(action.ts).format("YYYY/MM/DD HH:mm") : ''}`), - userLink(action.author, action.authorNames && action.authorNames[action.author]) - ) + renderEngagement(safeMsgId(action.tipId || action.id), null, null, { href: `/projects/${encodeURIComponent(action.tipId || action.id)}`, title: title || '' }) ); } @@ -1879,53 +1874,39 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e : null; const msgId = safeMsgId(action.tipId || action.id || action.key); const isOwn = action.author && String(action.author) === String(userId); + const bylineAuthorId = action.author || (content && content.proposer) || ''; return div({ class: 'trending-card' + (isOwn ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, + renderCardTop(msgId, detailHref, { + date: action.ts, + authorName: (action.authorNames && action.authorNames[bylineAuthorId]) || getProfile(bylineAuthorId).name, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(headerText || type || '').replace(/^\[|\]$/g, '')) ) - ), - renderContentActions(msgId, detailHref, { - author: action.author, - contact: type === 'about', - reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', - spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, - ...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}), - ...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&') - ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } - : {}), - ...favOptsFor(type, msgId, extras), - deleteAction: isOwn ? activityDeleteAction(type, action, content, msgId, userId) : undefined - }) - ), + )], + author: bylineAuthorId, + contact: type === 'about', + reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '', + spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined, + ...(type === 'torrent' || type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(TORRENTABLE_TYPES.has(type) && content && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...(commentOf(action) ? {} : favOptsFor(type, msgId, extras)), + deleteAction: isOwn ? (commentOf(action) ? `/comments/delete/${encodeURIComponent(msgId)}` : activityDeleteAction(type, action, content, msgId, userId)) : undefined + }), ...cardBody, (() => { - const footerAuthorId = action.author || (content && content.proposer) || ''; - const footerNode = p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date}`), - userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name) - ); - const commentTotal = Number(action.commentCount) || 0; - const commentsHref = detailHref - ? `${detailHref}${detailHref.includes('?') ? '&' : '?'}comments=open${commentTotal ? '#comments-latest' : ''}` - : ''; - const commentsNode = detailHref && type !== 'about' - ? a({ - href: commentsHref, - class: commentTotal ? 'comments-summary-link engage-on' : 'comments-summary-link', - title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments' - }, - span({ class: 'comments-summary-icon' }, 'โœ‘'), - span({ class: 'comments-summary-count' }, `(${commentTotal})`) - ) - : null; + const shareNode = detailHref ? renderShareButton(msgId, detailHref, (content && (content.title || content.question || content.concept || content.name)) || '') : null; + const commentTotal = typeof action.commentCount === 'number' ? action.commentCount : commentCountFor([action.id, action.rootId, action.key, msgId]); + const commentsHref = !detailHref ? '' : commentOf(action) ? detailHref : commentsOpenHref(detailHref, commentTotal); + const commentsNode = type !== 'about' ? renderCommentsSummary(commentsHref, commentTotal) : null; const routeFn = OPINION_TYPES.has(type) ? OPINION_ROUTES[type] : null; - if (!routeFn) return commentsNode ? [renderCardMetaRow(commentsNode, footerNode)] : footerNode; + if (!routeFn) return renderCardMetaRow(type !== 'about' && detailHref ? renderOpinionsPill(msgId, detailHref) : null, commentsNode, shareNode); const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {}; const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0); const votingNode = details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, + summary({ class: opsTotal > 0 ? 'opinions-summary engage-on' : 'opinions-summary' }, span({ class: 'opinions-summary-icon' }, '๊”'), span({ class: 'opinions-summary-count' }, `(${opsTotal})`)), div({ class: 'voting-buttons' }, @@ -1936,7 +1917,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map(), e ) ) ); - return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, footerNode)]; + return [renderVotesSummary(ops), renderCardMetaRow(votingNode, commentsNode, shareNode)]; })() ); }); @@ -1974,6 +1955,51 @@ const favOptsFor = (type, id, extras = {}) => { }; }; +const commentOf = (action) => action && action.comment && action.comment.rootId ? action.comment : null; + +function commentHref(comment, commentId) { + const rootId = comment && comment.rootId; + if (!rootId) return commentId ? `/thread/${encodeURIComponent(commentId)}` : '/activity'; + if (comment.rootType === 'forum') return `/forum/${encodeURIComponent(rootId)}${commentId ? `#${forumReplyAnchor(commentId)}` : ''}`; + const tail = commentId ? `#${encodeURIComponent(commentId)}` : ''; + const base = comment.rootType === 'post' ? `/blogs/${encodeURIComponent(rootId)}` : comment.rootType ? getViewDetailsAction(comment.rootType, { id: rootId }) : null; + if (!base || base === '/activity') return `/thread/${encodeURIComponent(commentId || rootId)}`; + if (base.includes('#')) return base; + return `${base}${base.includes('?') ? '&' : '?'}comments=open${tail || '#comments-latest'}`; +} + +const GROUP_SUBTYPES = { + parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], + courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], + task: ['task', 'taskAssignment'], + votes: ['votes', 'poll'], + shop: ['shop', 'shopProduct'], + larp: ['larpHousePost'], + inhabitants:['about'], + chat: ['chat', 'chatThread'], + wiki: ['wikiPage'], + mailing: ['mailingList'], + emergency: ['emergency', 'emergencyUpdate'], + logistics: ['logisticsRoute'], + podcast: ['podcast', 'podcastEpisode'], + campaign: ['campaign', 'campaignUpdate'], + industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'] +}; +const MODULE_OF_TYPE = Object.entries(GROUP_SUBTYPES).reduce((acc, [mod, types]) => { + for (const t of types) if (t !== mod && t !== 'poll') acc[t] = mod; + return acc; +}, { schoolCourse: 'school', mapMarker: 'map' }); + +function rootTypeLabel(rt) { + return rt === 'post' ? (i18n.typePost || 'BLOG') : rt === 'poll' ? (i18n.pollsTitle || 'POLLS') : rt ? (i18n[`type${capitalize(MODULE_OF_TYPE[rt] || rt)}`] || rt) : null; +} + +function commentLabel(comment) { + const base = rootTypeLabel(comment && comment.rootType); + const word = i18n.typeComment; + return base ? `${base} ยท ${word}` : word; +} + function getViewDetailsAction(type, action) { const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action)); switch (type) { @@ -1997,9 +2023,8 @@ function getViewDetailsAction(type, action) { } case 'post': { const root = action.content && action.content.root; - return root - ? `/blogs/${encodeURIComponent(root)}#${id}` - : `/blogs/${id}`; + if (commentOf(action) || root) return commentHref(commentOf(action) || { rootId: root }, safeMsgId(action.tipId || action.id || action.key || action)); + return `/blogs/${id}`; } case 'feed': return `/feed/${id}`; case 'vote': return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`; @@ -2021,7 +2046,7 @@ function getViewDetailsAction(type, action) { case 'forum': { const root = action.content?.root; const rootId = typeof root === 'string' ? root : (root && (root.key || root.id)) || ''; - if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#msg-${String(action.tipId || action.id || '').replace(/[^a-zA-Z0-9]/g, '')}`; + if (rootId) return `/forum/${encodeURIComponent(action.content?.rootKey || rootId)}#${forumReplyAnchor(action.tipId || action.id)}`; return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`; } case 'document': return `/documents/${id}`; @@ -2061,6 +2086,9 @@ function getViewDetailsAction(type, action) { } exports.getViewDetailsAction = getViewDetailsAction; +exports.commentHref = commentHref; +exports.commentLabel = commentLabel; +exports.rootTypeLabel = rootTypeLabel; exports.activityView = (actions, filter, userId, q = '', extras = {}) => { const spreadMap = (extras && extras.spreadMap) || new Map(); @@ -2112,23 +2140,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { ]; const FILTER_META = new Set(['all', 'mine', 'recent']); - const GROUP_SUBTYPES = { - parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'], - courts: ['courtsCase', 'courtsNomination', 'courtsNominationVote'], - task: ['task', 'taskAssignment'], - votes: ['votes', 'poll'], - shop: ['shop', 'shopProduct'], - larp: ['larpHousePost'], - inhabitants:['about'], - chat: ['chat', 'chatThread'], - wiki: ['wikiPage'], - mailing: ['mailingList'], - emergency: ['emergency', 'emergencyUpdate'], - logistics: ['logisticsRoute'], - podcast: ['podcast', 'podcastEpisode'], - campaign: ['campaign', 'campaignUpdate'], - industry: ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'] - }; const ALLOWED_TYPES = new Set(['tribeOpened']); for (const { type } of activityTypes) { if (FILTER_META.has(type)) continue; @@ -2185,13 +2196,18 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone'); } else { filteredActions = actions.filter(action => - (action.type === filter + ((action.type === filter && !(filter === 'post' && commentOf(action) && commentOf(action).rootType !== 'post')) || filter === 'all' || openedIn(action, filter) || (filter === 'shop' && action.type === 'shopProduct') || (filter === 'votes' && action.type === 'poll')) && action.type !== 'tombstone'); } + if (!FILTER_META.has(filter) && filter !== 'post') { + const kinds = new Set(GROUP_SUBTYPES[filter] || [filter]); + const listed = new Set(filteredActions); + filteredActions = filteredActions.concat(actions.filter(a => !listed.has(a) && commentOf(a) && kinds.has(commentOf(a).rootType))); + } const larpEarliest = new Map(); for (const action of filteredActions) { @@ -2332,12 +2348,6 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { ) ); - const hasDocument = actions.some(a => a && a.type === 'document'); - if (hasDocument) { - html += ` - - `; - } return html; }; diff --git a/src/views/agenda_view.js b/src/views/agenda_view.js index e7e5512..ee44ab3 100644 --- a/src/views/agenda_view.js +++ b/src/views/agenda_view.js @@ -1,6 +1,6 @@ -const { div, h2, p, section, button, form, img, input, textarea, a, br, h1, span } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, input, a, span } = require("../server/node_modules/hyperaxe"); const { safeExternalHref } = require("../backend/renderStyledText"); -const { template, i18n, userLink, renderContentActions, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views'); +const { template, i18n, userLink, renderCardTop, renderModuleStats, renderStateChip, CONTENT_FAV_KIND, CONTENT_SPREADABLE, contentDeleteAction, paged } = require('./main_views'); const moment = require('../server/node_modules/moment'); const { config } = require('../server/SSB_server.js'); @@ -151,27 +151,24 @@ const renderAgendaItem = (item, userId, filter, extras = {}) => { const href = getViewDetailsAction(item); const title = item.title || item.name || item.concept || ''; return div({ class: 'trending-card agenda-card' + (isOwn ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase()) - ), - renderContentActions(item.id, href, { - author, - reportTitle: title, - spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined, - ...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}), - returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`, - deleteAction: isOwn ? agendaDeleteAction(item) : undefined - }) - ), + renderCardTop(item.id, href, { + date: item.createdAt, + chips: [ + span({ class: 'pm-exposition-chip pm-exposition-whole' }, + span({ class: 'pm-exposition-text' }, String(item.type || '').toUpperCase()) + ), + chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null + ], + author, + reportTitle: title, + spread: CONTENT_SPREADABLE.has(item.type) ? ((spreadMap && spreadMap.get(item.id)) || null) : undefined, + ...(favKind ? { favKind, isFavorite: item.isFavorite === true || (!!favIndex && [item.rootId, item.id].some(id => id && favIndex.get(String(id)) === favKind)) } : {}), + returnTo: `/agenda?filter=${encodeURIComponent(filter || 'all')}`, + deleteAction: isOwn ? agendaDeleteAction(item) : undefined + }), div({ class: 'card-section agenda-card-body' }, div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, a({ href }, title))), - chips.filter(Boolean).length ? div({ class: 'card-chips-row' }, ...chips.filter(Boolean)) : null, ...body.filter(Boolean), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${item.createdAt ? moment(item.createdAt).format('YYYY/MM/DD HH:mm') : ''}`), - author ? userLink(author) : '' - ), div({ class: 'agenda-card-actions' }, actionButton, ...extraActions) ) ); diff --git a/src/views/audio_view.js b/src/views/audio_view.js index 83e9b6b..1206d7e 100644 --- a/src/views/audio_view.js +++ b/src/views/audio_view.js @@ -18,7 +18,8 @@ const { const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderEcoTax, renderLifespanChip, renderCardTop, renderCardByline, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); +const { renderDownloadButton } = require("./gallery_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText") @@ -69,20 +70,17 @@ const renderAudioPlayer = (audioObj, opts = {}) => const renderAudioOwnerActions = (filter, audioObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(audioObj.author) === String(userId); - const hasOpinions = Object.keys(audioObj.opinions || {}).length > 0; if (!isAuthor) return []; const items = []; - if (!hasOpinions) { - items.push( - form( - { method: "GET", action: `/audios/edit/${encodeURIComponent(audioObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.audioUpdateButton) - ) - ); - } + items.push( + form( + { method: "GET", action: `/audios/edit/${encodeURIComponent(audioObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.audioUpdateButton) + ) + ); return items; }; @@ -95,6 +93,15 @@ const renderAudioCommentsSection = (audioId, comments = [], returnTo = null) => }); }; +const renderAudioUpdated = (audioObj) => { + const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; + const updatedTs = audioObj.updatedAt ? new Date(audioObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return showUpdated + ? span({ class: "votations-comment-date" }, `${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; +}; + const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); @@ -102,53 +109,37 @@ const renderAudioList = exports.renderAudioList = (audios, filter, params = {}) ? audios.map((audioObj) => { const commentCount = typeof audioObj.commentCount === "number" ? audioObj.commentCount : 0; const title = safeText(audioObj.title); + const href = `/audios/${encodeURIComponent(audioObj.key)}`; const isOwn = audioObj.author && String(audioObj.author) === String(userId); - const headerActions = typeof params.headerActions === "function" - ? params.headerActions(audioObj) + const byline = { + author: audioObj.author, + date: audioObj.createdAt, + chips: [ + audioObj.tribeOrigin ? renderTribeOriginChip(audioObj.tribeOrigin) : audioObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('audios', audioObj.title, audioObj.key)) : null, + renderLicenseChip(audioObj.license), + ...(typeof params.titleChips === "function" ? params.titleChips(audioObj) : []), + renderLifespanChip(audioObj.lifetime, i18n) + ], + bylineExtra: [renderAudioUpdated(audioObj)] + }; + const header = typeof params.headerActions === "function" + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), params.headerActions(audioObj)) : audioObj.tribeOrigin - ? renderContentActions(null, audioObj.tribeOrigin.href) - : renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, author: audioObj.author, favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, isFavorite: audioObj.isFavorite, reportTitle: audioObj.title, deleteAction: isOwn ? contentDeleteAction('audio', audioObj.key) : undefined, returnTo }); - const titleHref = typeof params.headerActions === "function" ? null : audioObj.tribeOrigin ? audioObj.tribeOrigin.href : `/audios/${encodeURIComponent(audioObj.key)}`; - const engagement = typeof params.engagement === "function" - ? params.engagement(audioObj) - : audioObj.tribeOrigin ? null : renderEngagement(audioObj.key, - renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderCommentsLink({ href: `/audios/${encodeURIComponent(audioObj.key)}`, count: commentCount }) - ); + ? renderCardTop(audioObj.key, audioObj.tribeOrigin.href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || null, reportTitle: audioObj.title }) + : renderCardTop(audioObj.key, href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(audioObj.key)) || params.spreads || null, favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, isFavorite: audioObj.isFavorite, reportTitle: audioObj.title, deleteAction: isOwn ? contentDeleteAction('audio', audioObj.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : audioObj.tribeOrigin ? audioObj.tribeOrigin.href : href; + const engagement = typeof params.engagement === "function" ? params.engagement(audioObj) : null; return div( { class: "trending-card audio-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + header, div( { class: "card-section audio-card-body" }, - div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, audioObj.tribeOrigin ? renderTribeOriginChip(audioObj.tribeOrigin) : audioObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('audios', audioObj.title, audioObj.key)) : null, renderLicenseChip(audioObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(audioObj) : [])), - audioObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(audioObj.lifetime, i18n)) : null, + title ? div({ class: "shop-title-row" }, h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title)) : null, renderAudioPlayer(audioObj), ...(typeof params.bodyExtra === "function" ? params.bodyExtra(audioObj) : []), - engagement, renderMapLocationVisitLabel(audioObj.mapUrl), - br(), - (() => { - const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; - const updatedTs = audioObj.updatedAt ? new Date(audioObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(audioObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })() + engagement ) ); }) @@ -169,7 +160,7 @@ const renderAudioForm = (filter, audioId, audioToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), span(i18n.audioFileLabel), br(), - input({ type: "file", name: "audio", accept: "audio/*", required: filter !== "edit" }), + (filter === "edit" && renderMediaLockNote(audioToEdit)) || input({ type: "file", name: "audio", accept: "audio/*", required: filter !== "edit" }), br(), br(), span(i18n.audioTitleLabel), @@ -294,6 +285,7 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(audioObj.title); + const href = `/audios/${encodeURIComponent(audioObj.key)}`; const isAuthor = String(audioObj.author) === String(userId); const isClearnet = !!audioObj.clearnet; @@ -314,26 +306,28 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params const tagsNode = renderTags(audioObj.tags); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(audioObj.key, `/audios/${encodeURIComponent(audioObj.key)}`, { + const audioSide = div({ class: "tribe-side" }, + renderCardTop(audioObj.key, href, { arrow: false, author: audioObj.author, + date: audioObj.createdAt, + chips: [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('audios', audioObj.title, audioObj.key) : null), + renderLicenseChip(audioObj.license), + ...chips + ], + bylineExtra: [renderEditedChip(audioObj.key, audioObj.rootId), renderAudioUpdated(audioObj)], favKind: 'audios', torrentFrom: { blobId: audioObj.url, name: audioObj.title }, isFavorite: audioObj.isFavorite, spread: params.spreads || null, returnTo, reportTitle: audioObj.title, deleteAction: isAuthor ? contentDeleteAction('audio', audioObj.key) : undefined - }) - ); - - const audioSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('audios', audioObj.title, audioObj.key) : null), - isAuthor ? renderClearnetSwitch('audios', audioObj.rootId || audioObj.key, isClearnet) : null, - renderLicenseChip(audioObj.license) + isAuthor ? renderClearnetSwitch('audios', audioObj.rootId || audioObj.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(audioObj.key, href, audioObj.title), safeText(audioObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(audioObj.description)) : null, @@ -343,29 +337,11 @@ exports.singleAudioView = async (audioObj, filter = "all", comments = [], params ); const audioMain = div({ class: "tribe-main" }, - detailActions, renderAudioPlayer(audioObj), - (() => { - const createdTs = audioObj.createdAt ? new Date(audioObj.createdAt).getTime() : NaN; - const updatedTs = audioObj.updatedAt ? new Date(audioObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(audioObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(audioObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.audioUpdatedAt}: ${moment(audioObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), + audioObj?.url ? renderDownloadButton(blobUrlOf(audioObj.url), audioObj.title || "") : null, renderEngagement(audioObj.key, renderOpinionsVoting('/audios/opinions', audioObj.key, audioObj.opinions, returnTo, audioObj.opinions_inhabitants), - renderAudioCommentsSection(audioObj.key, comments, returnTo) - ) + renderAudioCommentsSection(audioObj.key, comments, returnTo)) ); return template( @@ -419,15 +395,10 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload ) ), div({ class: "bookmark-item card" }, + renderCardTop(audio.key, `/audios/${encodeURIComponent(audio.key)}`, { author: audio.author, date: audio.createdAt, chips: [itemSize ? renderEcoTax(itemSize, audio.key) : null], reportTitle: audio.title }), audio.title ? h2(audio.title) : null, renderAudioPlayer(audio, { skipTranscode: true }), - p({ class: "transcode-meta card-footer" }, - userLink(audio.author), - span({ class: "melody-meta-sep" }, " ยท "), - span({ class: "card-value" }, moment(audio.createdAt).format("YYYY/MM/DD HH:mm")), - itemSize ? span({ class: "melody-meta-sep" }, " ยท ") : null, - itemSize ? renderEcoTax(itemSize, audio.key) : null - ), + renderDetailShare(audio.key, `/audios/${encodeURIComponent(audio.key)}`, audio.title), safeText(audio.description) ? p({ class: "melody-bcs-desc" }, audio.description) : null, renderTags(audio.tags), form({ method: "POST", action: `/melody/transcode/${encodeURIComponent(audio.key)}`, class: "audio-transcode-run-form audio-transcode-form" }, @@ -464,5 +435,4 @@ exports.audioTranscodeDetailView = async ({ audio, decoded = false, stegoPayload ); }; -exports.audiosTranscodeView = exports.audioTranscodeDetailView; diff --git a/src/views/blockchain_view.js b/src/views/blockchain_view.js index 2c9ae2e..e77200b 100644 --- a/src/views/blockchain_view.js +++ b/src/views/blockchain_view.js @@ -15,9 +15,12 @@ const FILTER_LABELS = { map: i18n.typeMap, shop: i18n.typeShop, shopProduct: i18n.typeShopProduct || 'Shop Product', pad: i18n.typePad || 'PAD', room: i18n.typeRoom || 'ROOM', roomMember: i18n.typeRoomMember || 'ROOM PARTICIPANT', wikiPage: i18n.typeWiki || 'WIKI', emergency: i18n.typeEmergency || 'EMERGENCY', emergencyConfirm: i18n.typeEmergencyConfirm || 'EMERGENCY CONFIRM', emergencyUpdate: i18n.typeEmergencyUpdate || 'EMERGENCY UPDATE', mailingList: i18n.typeMailingList || 'MAILING LIST', logisticsRoute: i18n.typeLogisticsRoute || 'ROUTE', logisticsRating: i18n.typeLogisticsRating || 'ROUTE RATING', podcast: i18n.typePodcast || 'PODCAST', podcastEpisode: i18n.typePodcastEpisode || 'EPISODE', podcastOpinion: i18n.typePodcastOpinion || 'EPISODE OPINION', podcastPlay: i18n.typePodcastPlay || 'EPISODE PLAY', campaign: i18n.typeCampaign || 'CAMPAIGN', campaignSignature: i18n.typeCampaignSignature || 'SIGNATURE', campaignUpdate: i18n.typeCampaignUpdate || 'CAMPAIGN UPDATE', chat: i18n.typeChat || 'CHAT', gameScore: i18n.typeGameScore || 'GAME SCORE', calendar: i18n.typeCalendar || 'CALENDAR', torrent: i18n.typeTorrent, file: i18n.typeFile, - school: i18n.typeSchool || 'SCHOOL' + school: i18n.typeSchool || 'SCHOOL', comment: i18n.typeComment }; +const blockKind = (block) => block && block.type === 'post' && block.content && typeof block.content.root === 'string' ? 'comment' : block && block.type; +const blockLabel = (block) => FILTER_LABELS[blockKind(block)] || blockKind(block); + const BASE_FILTERS = ['all', 'recent', 'mine', 'tombstone', 'logs']; const CAT_BLOCK1 = ['votes', 'event', 'task', 'report', 'calendar', 'school', 'parliament', 'courts']; const CAT_BLOCK2 = ['pub', 'tribe', 'about', 'contact', 'curriculum', 'vote', 'aiExchange']; @@ -78,7 +81,7 @@ const filterBlocks = (blocks, filter, userId) => { if (filter === 'shop') return blocks.filter(b => b.type === 'shop' || b.type === 'shopProduct'); if (filter === 'school') return blocks.filter(b => ['schoolCourse', 'schoolLesson', 'schoolEnroll', 'schoolCertificate'].includes(b.type)); if (filter === 'logs') return blocks.filter(b => b.type === 'log' && b.author === userId); - return blocks.filter(b => b.type === filter); + return blocks.filter(b => blockKind(b) === filter); }; const computeStats = (blocks) => { @@ -87,7 +90,7 @@ const computeStats = (blocks) => { const byAuthor = new Map(); for (const b of arr) { if (!b || !b.type) continue; - byType.set(b.type, (byType.get(b.type) || 0) + 1); + byType.set(blockKind(b), (byType.get(blockKind(b)) || 0) + 1); if (b.author) byAuthor.set(b.author, (byAuthor.get(b.author) || 0) + 1); } const typeBreakdown = Array.from(byType.entries()) @@ -252,7 +255,7 @@ const renderBlockDiagram = (blocks, qs) => { h3({ class: 'block-diagram-title' }, i18n.blockchainLatestDatagram || 'Latest Datagram'), ...last2.map(block => { const ts = moment(block.ts).format('YYYY-MM-DD HH:mm:ss'); - const typeLabel = (FILTER_LABELS[block.type] || block.type).toUpperCase(); + const typeLabel = blockLabel(block).toUpperCase(); const color = TYPE_COLORS[block.type] || '#95a5a6'; const shortId = block.id.length > 20 ? block.id.slice(0, 10) + 'โ€ฆ' + block.id.slice(-8) : block.id; const shortAuthor = block.author.length > 20 ? block.author.slice(0, 10) + 'โ€ฆ' + block.author.slice(-8) : block.author; @@ -345,7 +348,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi ), div({ class: 'block-row block-row--meta' }, span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`), - span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase()) + span({ class: 'blockchain-card-value' }, blockLabel(block).toUpperCase()) ), div({ class: 'block-row block-row--meta' }, span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockTimestamp}:`), @@ -366,7 +369,7 @@ const renderSingleBlockView = (block, filter = 'recent', userId, search = {}, vi ), div({ class: 'block-row block-row--meta' }, span({ class: 'blockchain-card-label' }, `${i18n.blockchainBlockType}:`), - span({ class: 'blockchain-card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase()) + span({ class: 'blockchain-card-value' }, blockLabel(block).toUpperCase()) ), div({ class: 'block-row block-row--meta' }, span({ class: 'blockchain-card-label' }, `${i18n.oasisIdLabel || 'OasisID'}:`), @@ -550,7 +553,7 @@ const renderBlockchainView = (blocks, filter, userId, search = {}, extras = {}) table({ class:'block-info-table' }, tr(td({ class:'card-label' }, i18n.blockchainBlockTimestamp), td({ class:'card-value' }, moment(block.ts).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))), tr(td({ class:'card-label' }, i18n.blockchainBlockID), td({ class:'card-value' }, block.id)), - tr(td({ class:'card-label' }, i18n.blockchainBlockType), td({ class:'card-value' }, (FILTER_LABELS[block.type]||block.type).toUpperCase())), + tr(td({ class:'card-label' }, i18n.blockchainBlockType), td({ class:'card-value' }, blockLabel(block).toUpperCase())), tr(td({ class:'card-label' }, i18n.blockchainBlockAuthor), td({ class:'card-value' }, a({ href:`/author/${encodeURIComponent(block.author)}`, class:'block-author user-link' }, block.author))), tr(td({ class:'card-label' }, i18n.blockchainBlockCarbon || 'Carbon footprint'), td({ class:'card-value' }, formatCarbon(block.size))) ) diff --git a/src/views/blog_view.js b/src/views/blog_view.js index d9bbfe1..db65c3d 100644 --- a/src/views/blog_view.js +++ b/src/views/blog_view.js @@ -1,10 +1,9 @@ -const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, input, label, span, textarea, br } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); const { TEXT_CAP } = require('../backend/long_text'); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { clearnetItemHref, template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, contentDeleteAction, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderCardTop, contentDeleteAction, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged, renderDetailShare } = require("./main_views"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); -const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledHtml } = require("../backend/renderStyledText"); const { sanitizeHtml } = require("../backend/sanitizeHtml"); @@ -73,22 +72,16 @@ const renderBlogCard = (blog, filter, spreadInfo) => { const href = `/blogs/${encodeURIComponent(blog.id)}`; const isOwn = String(blog.author) === String(userId); return div({ class: "trending-card blog-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - renderContentActions(blog.id, href, { spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, deleteAction: isOwn ? contentDeleteAction('blog', blog.id) : undefined }) - ), + renderCardTop(blog.id, href, { date: blog.createdAt, chips: blog.clearnet === true ? [renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))] : [], spread: spreadInfo || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, deleteAction: isOwn ? contentDeleteAction('blog', blog.id) : undefined }), div({ class: "card-section blog-card-body" }, blog.subject ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, blog.subject)) ) : null, - blog.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("blog", blog.subject || blog.text, blog.id))) : null, div({ class: "blog-card-text", [RAW_HTML]: sanitizeHtml(renderStyledHtml(excerpt(blog.text))) }), renderBlogTags(blog.tags), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(blog.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(blog.author) - ) + null ) ); }; @@ -184,23 +177,12 @@ exports.singleBlogView = async (blog, comments = [], params = {}) => { const isClearnet = !!blog.clearnet; const blogSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(blog.id, href, { spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, returnTo: href, deleteAction: isAuthor ? contentDeleteAction('blog', blog.id) : undefined }) - ), + renderCardTop(blog.id, href, { arrow: false, date: blog.createdAt, chips: [renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("blog", blog.subject || blog.text, blog.id) : null)], spread: params.spreads || null, author: blog.author, favKind: 'blogs', isFavorite: blog.isFavorite, reportTitle: blog.subject || blog.text, returnTo: href, deleteAction: isAuthor ? contentDeleteAction('blog', blog.id) : undefined }), div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("blog", blog.subject || blog.text, blog.id) : null) + h2({ class: "tribe-card-title" }, blog.subject || i18n.blogTitle) ), + renderDetailShare(blog.id, href, blog.subject || blog.text), isAuthor ? renderClearnetSwitch("posts", blog.rootId || blog.id, isClearnet) : null, - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, moment(blog.createdAt).format("YYYY/MM/DD HH:mm")) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(blog.author)) - ) - ), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.blogComments}: ${blog.commentCount || 0}`) ), @@ -223,8 +205,7 @@ exports.singleBlogView = async (blog, comments = [], params = {}) => { ), renderEngagement(blog.id, renderOpinionsVoting('/blogs/opinions', blog.id, blog.opinions, href, blog.opinions_inhabitants), - renderCommentsSection(blog, comments) - ) + renderCommentsSection(blog, comments)) ); return template( diff --git a/src/views/bookmark_view.js b/src/views/bookmark_view.js index 8b6d2eb..3e386f2 100644 --- a/src/views/bookmark_view.js +++ b/src/views/bookmark_view.js @@ -2,7 +2,7 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderEcoTax, renderLifespanChip, renderCardTop, renderCardByline, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); @@ -60,6 +60,15 @@ const renderTags = (tags) => { : null; }; +const renderBookmarkUpdated = (bookmark) => { + const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; + const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return showUpdated + ? span({ class: "votations-comment-date" }, `${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; +}; + const renderBookmarkList = exports.renderBookmarkList = (filteredBookmarks, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); @@ -78,53 +87,32 @@ const renderBookmarkList = exports.renderBookmarkList = (filteredBookmarks, filt : i18n.noUrl; const isOwn = bookmark.author && String(bookmark.author) === String(userId); + const href = `/bookmarks/${encodeURIComponent(bookmark.id)}`; const reachChip = bookmark.tribeOrigin ? renderTribeOriginChip(bookmark.tribeOrigin) : bookmark.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id)) : null; const extraChips = typeof params.titleChips === "function" ? params.titleChips(bookmark).filter(Boolean) : []; - const headerActions = typeof params.headerActions === "function" - ? params.headerActions(bookmark) + const byline = { + author: bookmark.author, + date: bookmark.createdAt, + chips: [renderLifespanChip(bookmark.lifetime, i18n), reachChip, ...extraChips], + bylineExtra: [renderBookmarkUpdated(bookmark)] + }; + const header = typeof params.headerActions === "function" + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), params.headerActions(bookmark)) : bookmark.tribeOrigin - ? renderContentActions(null, bookmark.tribeOrigin.href) - : renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, author: bookmark.author, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title, deleteAction: isOwn ? contentDeleteAction('bookmark', bookmark.id) : undefined, returnTo }); - const titleHref = typeof params.headerActions === "function" ? null : bookmark.tribeOrigin ? bookmark.tribeOrigin.href : `/bookmarks/${encodeURIComponent(bookmark.id)}`; - const engagement = typeof params.engagement === "function" - ? params.engagement(bookmark) - : bookmark.tribeOrigin ? null : renderEngagement(bookmark.id, - renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderCommentsLink({ href: `/bookmarks/${encodeURIComponent(bookmark.id)}`, count: commentCount }) - ); + ? renderCardTop(bookmark.id, bookmark.tribeOrigin.href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || null, reportTitle: bookmark.title || bookmark.url }) + : renderCardTop(bookmark.id, href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(bookmark.id)) || params.spreads || null, favKind: 'bookmarks', isFavorite: bookmark.isFavorite, reportTitle: bookmark.title, deleteAction: isOwn ? contentDeleteAction('bookmark', bookmark.id) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : bookmark.tribeOrigin ? bookmark.tribeOrigin.href : href; + const engagement = typeof params.engagement === "function" ? params.engagement(bookmark) : null; return div( { class: "trending-card bookmark-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + header, div( { class: "card-section bookmark-card-body" }, h2({ class: "bookmark-title" }, titleHref ? a({ href: titleHref, class: "card-title-link" }, bookmark.title || bookmark.url || "") : (bookmark.url ? urlLink : (bookmark.title || ""))), - bookmark.lifetime || reachChip || extraChips.length ? div({ class: "card-chips-row" }, renderLifespanChip(bookmark.lifetime, i18n), reachChip, ...extraChips) : null, titleHref ? (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, urlLink) : null) : (bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null), params.hideLastVisit ? null : renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), ...(typeof params.bodyExtra === "function" ? params.bodyExtra(bookmark) : []), - br, - engagement, - (() => { - const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; - const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(bookmark.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })() + engagement ) ); }) @@ -148,6 +136,11 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { form( { action: filter === "edit" ? `/bookmarks/update/${encodeURIComponent(bookmarkId)}` : "/bookmarks/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: returnTo }), + label(i18n.bookmarkTitleLabel), + br(), + input({ type: "text", name: "title", maxlength: "100", placeholder: i18n.bookmarkDescriptionPlaceholder, value: filter === "edit" ? bookmarkToEdit.title || "" : "" }), + br(), + br(), label(i18n.bookmarkUrlLabel), br(), input({ @@ -156,14 +149,16 @@ const renderBookmarkForm = (filter, bookmarkId, bookmarkToEdit, tags, params = { id: "url", required: true, placeholder: i18n.bookmarkUrlPlaceholder, - value: filter === "edit" ? bookmarkToEdit.url || "" : "" + value: filter === "edit" ? bookmarkToEdit.url || "" : "", + ...(filter === "edit" && renderMediaLockNote(bookmarkToEdit) ? { readonly: true } : {}) }), + filter === "edit" ? renderMediaLockNote(bookmarkToEdit) : null, br(), br(), label(i18n.bookmarkDescriptionLabel), br(), textarea( - { name: "description", id: "description", placeholder: i18n.bookmarkDescriptionPlaceholder, rows: "4" }, + { name: "description", id: "description", maxlength: "5000", placeholder: i18n.bookmarkDescriptionPlaceholder, rows: "4" }, filter === "edit" ? bookmarkToEdit.description || "" : "" ), br(), @@ -275,7 +270,6 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par const returnTo = params.returnTo || buildReturnTo(filter, { q, sort }); const isAuthor = String(bookmark.author) === String(userId); - const hasOpinions = Object.keys(bookmark.opinions || {}).length > 0; const isClearnet = !!bookmark.clearnet; const lastVisit = bookmark.lastVisit ? moment(bookmark.lastVisit) : null; @@ -294,8 +288,7 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ].filter(Boolean); const sideActions = []; - sideActions.push(renderPMButton(bookmark.author)); - if (isAuthor && !hasOpinions) { + if (isAuthor) { sideActions.push(form( { method: "GET", action: `/bookmarks/edit/${encodeURIComponent(bookmark.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -304,27 +297,30 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par } const tagsNode = renderTags(bookmark.tags); + const href = `/bookmarks/${encodeURIComponent(bookmark.id)}`; - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(bookmark.id, `/bookmarks/${encodeURIComponent(bookmark.id)}`, { + const bookmarkSide = div({ class: "tribe-side" }, + renderCardTop(bookmark.id, href, { arrow: false, author: bookmark.author, + date: bookmark.createdAt, + chips: [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id) : null), + ...chips + ], + bylineExtra: [renderEditedChip(bookmark.id, bookmark.rootId), renderBookmarkUpdated(bookmark)], favKind: 'bookmarks', isFavorite: bookmark.isFavorite, spread: params.spreads || null, returnTo, reportTitle: bookmark.title, deleteAction: isAuthor ? contentDeleteAction('bookmark', bookmark.id) : undefined - }) - ); - - const bookmarkSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, bookmark.url ? urlLink : (bookmark.title || "")), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("bookmarks", bookmark.title || bookmark.url, bookmark.id) : null) + h2({ class: "tribe-card-title" }, bookmark.title || urlLink || "") ), + renderDetailShare(bookmark.id, href, bookmark.title), isAuthor ? renderClearnetSwitch("bookmarks", bookmark.rootId || bookmark.id, isClearnet) : null, - bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, bookmark.title) : null, - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + bookmark.title && bookmark.url ? p({ class: "bookmark-subtitle" }, urlLink) : null, safeText(bookmark.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(bookmark.description)) : null, @@ -333,28 +329,10 @@ exports.singleBookmarkView = async (bookmark, filter = "all", comments = [], par ); const bookmarkMain = div({ class: "tribe-main" }, - detailActions, renderCardField(i18n.bookmarkLastVisitLabel + ":", lastVisitTxt), - (() => { - const createdTs = bookmark.createdAt ? new Date(bookmark.createdAt).getTime() : NaN; - const updatedTs = bookmark.updatedAt ? new Date(bookmark.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(bookmark.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(bookmark.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.bookmarkUpdatedAt}: ${moment(bookmark.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), renderEngagement(bookmark.id, renderOpinionsVoting('/bookmarks/opinions', bookmark.id, bookmark.opinions, returnTo, bookmark.opinions_inhabitants), - renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo) - ) + renderBookmarkCommentsSection(bookmark.id, bookmark.rootId, comments, returnTo)) ); return template( diff --git a/src/views/calendars_view.js b/src/views/calendars_view.js index f1990c4..b43452d 100644 --- a/src/views/calendars_view.js +++ b/src/views/calendars_view.js @@ -1,6 +1,6 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, hr, textarea, input, label, select, option, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe") +const { div, h2, h4, p, section, button, form, a, span, br, hr, textarea, input, label, select, option, table, tr, td } = require("../server/node_modules/hyperaxe") const { renderStyledText } = require("../backend/renderStyledText") -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadEditWarning, renderCardTop, renderEngagement, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderDetailShare } = require("./main_views") const { renderMapEmbed } = require("./maps_view") const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") @@ -58,16 +58,13 @@ const renderCalendarCard = exports.renderCalendarCard = (cal, spreadInfo) => { : (cal.subscriptionIn === false ? renderStateChip("closed", "โœ‰", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(cal.rootId, href, origin - ? { author: cal.author, reportTitle: cal.title } - : { spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title, deleteAction: String(cal.author) === String(userId) ? contentDeleteAction('calendar', cal.rootId) : undefined }) - ), + renderCardTop(cal.rootId, href, origin + ? { author: cal.author, date: cal.createdAt, chips, reportTitle: cal.title } + : { date: cal.createdAt, chips, spread: spreadInfo || null, author: cal.author, favKind: 'calendars', isFavorite: cal.isFavorite, reportTitle: cal.title, deleteAction: String(cal.author) === String(userId) ? contentDeleteAction('calendar', cal.rootId) : undefined }), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, cal.title || "\u2014")) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, cal.deadline ? div({ class: "card-field card-field-mb" }, span({ class: "card-label" }, `${String(i18n.calendarDeadlineLabel).toUpperCase()}: `), @@ -81,7 +78,8 @@ const renderCalendarCard = exports.renderCalendarCard = (cal, spreadInfo) => { ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count calendar-participants-count" }, `${i18n.calendarParticipantsLabel || "Participants"}: ${cal.participants.length}`) - ) + ), + null ) ) } @@ -286,21 +284,30 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { renderEncryptedChip(i18n), renderLifespanChip(calendar.lifetime, i18n), canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? calClearnetHref(calendar) : null) : null, - isAuthor && canClearnet ? renderClearnetSwitch("calendars", calendar.rootId, isClearnet) : null, (isAuthor || isParticipant) ? renderStateChip(subscriptionIn ? "mutuals" : "closed", "โœ‰", subscriptionIn ? i18n.subscriptionOn : i18n.subscriptionOff) : null ].filter(Boolean) const calSide = div({ class: "tribe-side" }, + renderCardTop(calendar.rootId, shareUrl, { arrow: false, + date: calendar.createdAt, + chips: detailChips, + author: calendar.author, + favKind: 'calendars', + isFavorite: calendar.isFavorite, + spread: (params && params.spreads) || null, + returnTo: shareUrl, + reportTitle: calendar.title, + deleteAction: isAuthor ? contentDeleteAction('calendar', calendar.rootId) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, calendar.title || "\u2014") ), - detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, + renderDetailShare(calendar.rootId, shareUrl, calendar.title), + isAuthor && canClearnet ? renderClearnetSwitch("calendars", calendar.rootId, isClearnet) : null, table({ class: "tribe-info-table" }, - tr(td({ class: "tribe-info-label" }, i18n.calendarCreated || "Created"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.createdAt).format("YYYY/MM/DD HH:mm"))), calendar.deadline ? tr(td({ class: "tribe-info-label" }, i18n.calendarDeadlineLabel || "Deadline"), td({ class: "tribe-info-value", colspan: "3" }, moment(calendar.deadline).format("YYYY/MM/DD HH:mm"))) : null, - tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))), - tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(calendar.author))) + tr(td({ class: "tribe-info-label" }, i18n.calendarStatusLabel || "Status"), td({ class: "tribe-info-value", colspan: "3" }, renderStatus(calendar))) ), tags, renderMapEmbed(params && params.mapData, calendar.mapUrl), @@ -364,7 +371,8 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { ) ) : null, - renderDocumentActions('calendars', calendar.rootId) + renderDocumentActions('calendars', calendar.rootId), + null ) const minDate = now.add(1, "minute").format("YYYY-MM-DDTHH:mm") @@ -456,18 +464,6 @@ exports.singleCalendarView = async (calendar, dates, notesByDate, params) => { : null const calMain = div({ class: "tribe-main" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(calendar.rootId, shareUrl, { - author: calendar.author, - favKind: 'calendars', - isFavorite: calendar.isFavorite, - spread: (params && params.spreads) || null, - returnTo: shareUrl, - reportTitle: calendar.title, - deleteAction: isAuthor ? contentDeleteAction('calendar', calendar.rootId) : undefined - }) - ), - calNav, grid, dayNotesSection, @@ -504,8 +500,7 @@ exports.clearnetCalendarView = async (calendar, dates, notesByDate = {}) => { const entriesHtml = entries.map(e => { const notes = (notesByDate[e.key] || []).filter(n => n && String(n.text || "").trim()) return `
- ${e.label ? `` : ""} -
${e.when.map(w => `๐Ÿ“… ${fmt(w)}`).join("")}
+
${e.label ? `` : ""}
${e.when.map(w => `๐Ÿ“… ${fmt(w)}`).join("")}
${notes.map(n => `

${renderRichText(n.text)}

`).join("")}
` }).join("") @@ -513,7 +508,8 @@ exports.clearnetCalendarView = async (calendar, dates, notesByDate = {}) => { .cn-cal-title{color:var(--fg);margin:0 0 16px 0;font-size:32px;font-weight:700;word-break:break-word} .cn-cal-meta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px} .cn-cal-entry{border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin:12px 0;background:var(--bg-elev)} -.cn-cal-entry-label{color:var(--fg);font-weight:600;font-size:16px;margin-bottom:8px;word-break:break-word} +.cn-cal-entry-head{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px;margin-bottom:8px} +.cn-cal-entry-label{color:var(--fg);font-weight:600;font-size:16px;word-break:break-word} .cn-cal-dates{display:flex;flex-wrap:wrap;gap:6px} .cn-cal-note{color:var(--fg-soft);line-height:1.5;margin:10px 0 0 0;word-break:break-word} .cn-cal-empty{color:var(--fg-dim)} diff --git a/src/views/campaigns_view.js b/src/views/campaigns_view.js index d3758fb..407b5fb 100644 --- a/src/views/campaigns_view.js +++ b/src/views/campaigns_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, progress, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderCardTop, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); const { renderCommentsSection } = require("./comments_view"); const { renderMapEmbed } = require("./maps_view"); @@ -56,16 +56,13 @@ exports.campaignChipFor = campaignChipFor; const renderCampaignCard = (cp, params = {}) => div({ class: `tribe-card campaign-card campaign-${cp.status.toLowerCase()}` }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(cp.id, campaignHref(cp), { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: (params.spreadMap && params.spreadMap.get(cp.id)) || null, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) - ), + renderCardTop(cp.id, campaignHref(cp), { date: cp.createdAt, chips: [statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.clearnet === true && clearnetEligible(cp) ? renderReachChip(true, i18n, clearnetItemHref("campaigns", cp.title, cp.id)) : null], author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: (params.spreadMap && params.spreadMap.get(cp.id)) || null, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }), div({ class: "tribe-card-body" }, renderCover(cp), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: campaignHref(cp) }, cp.title || "โ€”"))), - div({ class: "card-chips-row" }, statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.clearnet === true && clearnetEligible(cp) ? renderReachChip(true, i18n, clearnetItemHref("campaigns", cp.title, cp.id)) : null), renderProgress(cp), - cp.deadline ? p({ class: "job-meta-line" }, `${i18n.campaignDeadlineLabel}: ${fmt(cp.deadline)}`) : null + cp.deadline ? p({ class: "job-meta-line" }, `${i18n.campaignDeadlineLabel}: ${fmt(cp.deadline)}`) : null, + null ) ); @@ -95,7 +92,7 @@ const renderForm = (cp) => label(i18n.campaignTextLabel), br(), textarea({ name: "text", rows: 10, maxlength: "8000", placeholder: i18n.campaignTextPlaceholder }, cp ? cp.text : ""), br(), label(i18n.uploadMedia), br(), - input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + (cp && renderMediaLockNote(cp)) || input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), label(i18n.campaignCategoryLabel), br(), select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((cp ? cp.category : "OTHER") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), label(i18n.campaignGoalLabel), br(), @@ -155,25 +152,22 @@ const renderSignatures = (cp) => const renderUpdate = (cp, u, href, editing) => div({ class: "campaign-update", id: `update-${encodeURIComponent(u.id)}` }, - div({ class: "emergency-update-head campaign-update-head" }, - div({ class: "card-chips-row" }, - u.edited ? renderStateChip("neutral", "", i18n.campaignUpdateEdited) : null - ), - cp.isOwner - ? div({ class: "tribe-side-actions emergency-update-actions" }, + renderCardTop(null, null, { author: u.author, date: u.createdAt, chips: [u.edited ? renderStateChip("neutral", "", i18n.campaignUpdateEdited) : null] }), + cp.isOwner + ? div({ class: "emergency-update-head campaign-update-head" }, + div({ class: "tribe-side-actions emergency-update-actions" }, editing ? null : form({ method: "GET", action: `${href}#update-${encodeURIComponent(u.id)}` }, input({ type: "hidden", name: "editUpdate", value: u.id }), button({ type: "submit", class: "update-btn" }, i18n.campaignUpdate)), form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/delete` }, button({ type: "submit", class: "delete-btn" }, i18n.campaignDelete)) ) - : null - ), + ) + : null, editing ? form({ method: "POST", action: `/campaigns/updates/${encodeURIComponent(u.id)}/edit`, enctype: "multipart/form-data", class: "campaign-update-form" }, textarea({ name: "text", rows: 3, maxlength: "3000", required: true, autofocus: true }, u.text), br(), input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.campaignUpdate) ) - : div({ class: "campaign-update-text" }, ...renderStyledText(u.text)), - p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(u.createdAt)}`), userLink(u.author)) + : div({ class: "campaign-update-text" }, ...renderStyledText(u.text)) ); const renderUpdates = (cp, href, editUpdate) => @@ -203,34 +197,30 @@ exports.singleCampaignView = async (cp, params = {}) => { ].filter(Boolean) : []; const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(cp.id, href, { author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: params.spread || null, returnTo: href, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined }) - ), + renderCardTop(cp.id, href, { arrow: false, + date: cp.createdAt, + chips: [ + statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.proposalId ? renderStateChip("neutral", "โš–", String(i18n.campaignElevated).toUpperCase()) : null, + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("campaigns", cp.title, cp.id) : null) + ], + bylineExtra: [renderEditedChip(cp.tipId, cp.rootId)], + author: cp.author, favKind: "campaigns", isFavorite: cp.isFavorite, reportTitle: cp.title, spread: params.spread || null, returnTo: href, deleteAction: cp.isOwner ? contentDeleteAction("campaign", cp.id) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, cp.title)), - div({ class: "card-chips-row" }, - statusChip(cp), outcomeChip(cp), categoryChip(cp), cp.proposalId ? renderStateChip("neutral", "โš–", String(i18n.campaignElevated).toUpperCase()) : null, - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("campaigns", cp.title, cp.id) : null), - cp.isOwner && canClearnet ? renderClearnetSwitch("campaigns", cp.rootId || cp.id, isClearnet) : null - ), - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.createdAt)) - ), - cp.deadline - ? tr( + cp.isOwner && canClearnet ? renderClearnetSwitch("campaigns", cp.rootId || cp.id, isClearnet) : null, + cp.deadline + ? table({ class: "tribe-info-table jobs-info-table" }, + tr( td({ class: "tribe-info-label" }, i18n.campaignDeadlineLabel), td({ class: "tribe-info-value", colspan: "3" }, fmt(cp.deadline)) ) - : null, - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(cp.author)) - ) - ), + ) + : null, div({ class: "campaign-qr" }, img({ src: `${href}/qr.png`, alt: i18n.campaignQrAlt, class: "campaign-qr-img" }), p({ class: "campaign-qr-caption" }, i18n.campaignQrCaption) ), + renderDetailShare(cp.id, href, cp.title), renderProgress(cp), renderMapEmbed(params.mapData, cp.mapUrl), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, @@ -275,8 +265,7 @@ exports.singleCampaignView = async (cp, params = {}) => { renderSignatures(cp), renderEngagement(cp.id, renderOpinionsVoting("/campaigns/opinions", cp.id, cp.opinions, href, cp.opinions_inhabitants), - renderCommentsSection({ action: `/campaigns/${encodeURIComponent(cp.id)}/comments`, comments: params.comments || [], returnTo: href }) - ) + renderCommentsSection({ action: `/campaigns/${encodeURIComponent(cp.id)}/comments`, comments: params.comments || [], returnTo: href })) ); return template( cp.title, diff --git a/src/views/chats_view.js b/src/views/chats_view.js index bb8202f..721874a 100644 --- a/src/views/chats_view.js +++ b/src/views/chats_view.js @@ -1,5 +1,5 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, ul, li, details, summary, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, userLinkLabel, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, paged } = require("./main_views") +const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, td, details, summary, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, renderContentActions, userLinkLabel, renderStateChip, renderLifespanChip, renderCardTop, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, contentDeleteAction, paged } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") const { renderResults, renderBallot, outcomeOf } = require("./polls_view") const moment = require("../server/node_modules/moment") @@ -84,17 +84,17 @@ const renderChatCard = exports.renderChatCard = (chat, filter, params = {}) => { const href = `/chats/${encodeURIComponent(chat.key)}` return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(chat.key, href, { - spread: params.spread || null, - author: chat.author, - favKind: 'chats', - isFavorite: chat.isFavorite, - returnTo: buildReturnTo(filter, params), - reportTitle: chat.title, - deleteAction: String(chat.author) === String(userId) ? contentDeleteAction('chat', chat.key) : undefined - }) - ), + renderCardTop(chat.key, href, { + date: chat.createdAt, + chips, + spread: params.spread || null, + author: chat.author, + favKind: 'chats', + isFavorite: chat.isFavorite, + returnTo: buildReturnTo(filter, params), + reportTitle: chat.title, + deleteAction: String(chat.author) === String(userId) ? contentDeleteAction('chat', chat.key) : undefined + }), div({ class: "tribe-card-image-wrapper" }, a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, renderMediaBlob(chat.image, "/assets/images/default-avatar.png", { class: "tribe-card-hero-image" }) @@ -106,11 +106,11 @@ const renderChatCard = exports.renderChatCard = (chat, filter, params = {}) => { a({ href: `/chats/${encodeURIComponent(chat.key)}#chat-latest` }, chat.title || i18n.chatUntitled) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, chat.description ? p({ class: "tribe-card-description" }, chat.description) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.chatParticipants}: ${safeArr(chat.members).length}`) - ) + ), + null ) ) } @@ -406,8 +406,8 @@ exports.singleChatView = async (chat, filter, messages = [], params = {}) => { : null ].filter(Boolean) const chatSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(chat.key, `/chats/${encodeURIComponent(chat.key)}`, { spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title, deleteAction: isAuthor ? contentDeleteAction('chat', chat.key) : undefined }) + div({ class: "card-header activity-card-header card-top" }, + renderContentActions(chat.key, `/chats/${encodeURIComponent(chat.key)}`, { menu: true, arrow: false, spread: params.spreads || null, author: chat.author, favKind: 'chats', isFavorite: chat.isFavorite, returnTo: params.returnTo, reportTitle: chat.title, deleteAction: isAuthor ? contentDeleteAction('chat', chat.key) : undefined }) ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, chat.title || i18n.chatUntitled) diff --git a/src/views/clearnet_view.js b/src/views/clearnet_view.js index a54687d..792de5d 100644 --- a/src/views/clearnet_view.js +++ b/src/views/clearnet_view.js @@ -163,7 +163,7 @@ const LICENSES = [ { id: 'LAL-1.3', label: 'Free Art License 1.3', name: 'Free Art License 1.3' } ]; const DEFAULT_LICENSE = LICENSES[0].id; -const LICENSED_KINDS = new Set(['wiki', 'audio', 'video', 'image', 'document']); +const LICENSED_KINDS = new Set(['wiki', 'audio', 'video', 'image', 'document', 'file', 'torrent']); const licenseInfo = (id) => { const v = String(id || '').trim() || DEFAULT_LICENSE; return LICENSES.find(l => l.id === v) || null; @@ -366,6 +366,8 @@ const getCurrentPalette = () => { }; const buildBaseCss = (p) => ` +.cn-pdf-frame{display:block;width:100%;max-width:100%;height:80vh;min-height:520px;border:0;margin:0 0 1rem 0;background:transparent} +.pending-media{display:flex;align-items:center;justify-content:center;min-height:120px;max-width:480px;padding:16px;border:1px dashed #8a8a8a;border-radius:10px;background:rgba(128,128,128,0.15);color:#a0a0a0;text-align:center;box-sizing:border-box;margin:8px 0} :root{ --bg:${p.bg}; --bg-elev:${p.bgElev}; --bg-sub:${p.bgSub}; --fg:${p.fg}; --fg-soft:${p.fgSoft}; --fg-dim:${p.fgDim}; @@ -390,10 +392,12 @@ footer.cn-footer .cn-footer-logo{width:56px;height:auto;display:block;margin:0 a footer.cn-footer .cn-footer-line{margin:3px 0} `; +const renderRssLink = (href) => ``; + const renderClearnetHubSearch = ({ type = '', query = '' } = {}) => ``; -const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null }) => { +const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null, extraCss = '', body, headerExtra = null, hubFeedId = null, feed = '' }) => { const safeTitle = escapeHtml(title || 'Oasis'); const safeOgTitle = escapeHtml(ogTitle || title || 'Oasis'); const safeOgDesc = escapeHtml(ogDescription || ''); @@ -415,12 +419,15 @@ const renderClearnetPage = ({ title, ogTitle, ogDescription = '', ogImage = null + ${feed ? `` : ''} + +${clearnetPdfViewer(state, { title: t, back, embed: true })} +`; +}; +const renderClearnetPdfView = (state, { title: t = '', back = '' } = {}) => renderClearnetPage({ + title: t || 'PDF', + extraCss: CLEARNET_PDF_CSS, + body: `${back ? `

โ† ${escapeHtml(cnText('goBack', 'Go back'))}

` : ''}

${escapeHtml(t || 'PDF')}

${clearnetPdfViewer(state, { title: t, back })}` +}); + const renderClearnetMediaView = ({ kind, item }) => { const blob = blobUrl(item.url); const title = escapeHtml(item.title || cnText('cnUntitled', 'Untitled')); @@ -501,7 +565,7 @@ const renderClearnetMediaView = ({ kind, item }) => { .cn-media-frame audio,.cn-media-frame video{width:100%;max-width:100%;display:block;border-radius:6px;background:#000} .cn-media-frame .cn-media-doc{display:inline-block;max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 18px;color:var(--fg);text-decoration:none} .cn-media-frame .cn-media-doc:hover{border-color:var(--fg)} -.cn-media-frame .cn-media-docs{display:flex;flex-direction:column;align-items:flex-start;gap:10px} +.cn-media-frame .cn-media-docs{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:10px} `; const fileKind = kind === 'document' || kind === 'torrent' || kind === 'file'; let mediaHtml = ''; @@ -514,14 +578,16 @@ const renderClearnetMediaView = ({ kind, item }) => { mediaHtml = ``; } else if (kind === 'file' || kind === 'torrent') { const dlName = (v) => String(v || '').replace(/[\\/:*?"<>|\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120); - const dlLink = (href, name) => `โ‡ฉ ${escapeHtml(name)}`; + const extLabel = (name) => { const m = String(name || '').match(/\.([A-Za-z0-9]{1,8})$/); return m ? m[1].toUpperCase() : 'FILE'; }; + const dlLink = (href, name, label) => `\u2B07 ${escapeHtml(label)}`; const original = kind === 'file' ? blob : blobUrl(item.source); const originalName = dlName(kind === 'file' ? item.fileName : item.sourceName) || dlName(item.title) || 'file'; const torrentBlob = kind === 'file' ? blobUrl(item.torrentUrl) : blob; const torrentName = `${kind === 'file' ? originalName : (dlName(item.title) || 'download').replace(/\.torrent$/i, '')}.torrent`; - mediaHtml = `
${[original ? dlLink(original, originalName) : '', torrentBlob ? dlLink(torrentBlob, torrentName) : ''].filter(Boolean).join('')}
`; + const magnet = typeof item.magnet === 'string' && item.magnet.startsWith('magnet:?') ? `\u2B07 ${escapeHtml(cnText('torrentMagnet', 'MAGNET'))}` : ''; + mediaHtml = `
${[original ? dlLink(original, originalName, extLabel(originalName)) : '', torrentBlob ? dlLink(torrentBlob, torrentName, 'TORRENT') : '', magnet].filter(Boolean).join('')}
`; } else if (kind === 'document') { - mediaHtml = `โ‡ฉ ${title}`; + mediaHtml = renderClearnetPdfFrame(blob, item.title); } else { mediaHtml = `${title}`; } @@ -546,13 +612,13 @@ const renderClearnetMediaView = ({ kind, item }) => { ogTitle: item.title || (kind ? kindLabel(kind) : 'Oasis'), ogDescription: item.description || '', ogImage: (kind === 'image') ? blob : null, - extraCss, + extraCss: kind === 'document' ? extraCss + CLEARNET_PDF_CSS : extraCss, body, hubFeedId: item.author || null }); }; -const renderClearnetPodcastView = ({ channel }) => { +const renderClearnetPodcastView = ({ channel, feed = '' }) => { const title = escapeHtml(channel.title || cnText('cnUntitled', 'Untitled')); const desc = renderRichText(channel.description || ''); const cover = channel.cover && channel.cover.blobId ? blobUrl(channel.cover.blobId) : null; @@ -579,7 +645,7 @@ const renderClearnetPodcastView = ({ channel }) => { return `

#${Number(ep.number) || 0} ยท ${escapeHtml(ep.title || '')}

${epDate}
${player}${ep.description ? `

${renderRichText(ep.description)}

` : ''}
`; }).join(''); const body = ` -
${renderKindTag('podcast')}${dateStr ? `๐Ÿ“… ${dateStr}` : ''}๐ŸŽง ${episodes.length}
+
${renderKindTag('podcast')}${dateStr ? `๐Ÿ“… ${dateStr}` : ''}๐ŸŽง ${episodes.length}${renderClearnetLicense(channel.license)}${feed ? `RSS` : ''}

${title}

${cover ? (coverIsVideo ? `` : `${title}`) : ''} ${desc ? `

${desc}

` : ''} @@ -592,6 +658,7 @@ const renderClearnetPodcastView = ({ channel }) => { ogImage: cover && !coverIsVideo ? cover : null, extraCss, body, + feed, hubFeedId: channel.author || null }); }; @@ -629,5 +696,9 @@ module.exports = { CLEARNET_PAGER_CSS, paginateClearnet, renderClearnetPager, - renderClearnetMediaView + renderClearnetMediaView, + renderClearnetPdfEmbed, + renderClearnetPdfView, + renderClearnetPdfFrame, + CLEARNET_PDF_CSS }; diff --git a/src/views/comments_view.js b/src/views/comments_view.js index b71f73f..74846f8 100644 --- a/src/views/comments_view.js +++ b/src/views/comments_view.js @@ -68,7 +68,7 @@ const renderCommentsSection = ({ action, comments = [], returnTo = null, closedN h2({ class: "comment-form-title" }, i18n.voteNewCommentLabel), form({ method: "POST", action, class: "comment-form", enctype: "multipart/form-data" }, returnTo ? input({ type: "hidden", name: "returnTo", value: returnTo }) : null, - textarea({ name: "text", rows: 4, maxlength: String(TEXT_CAP), class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder, required: true }), + textarea({ name: "text", rows: 4, maxlength: String(TEXT_CAP), class: "comment-textarea", placeholder: i18n.voteNewCommentPlaceholder }), div({ class: "comment-file-upload" }, label(i18n.uploadMedia), input({ type: "file", name: "blob" })), br(), button({ type: "submit", class: "comment-submit-btn" }, i18n.voteNewCommentButton) diff --git a/src/views/courts_view.js b/src/views/courts_view.js index 5f9fa9d..d0e18ad 100644 --- a/src/views/courts_view.js +++ b/src/views/courts_view.js @@ -1,6 +1,6 @@ -const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); +const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, img } = require('../server/node_modules/hyperaxe'); const moment = require('../server/node_modules/moment'); -const { template, i18n, userLink, renderStateChip, renderModuleStatsBy, paged } = require('./main_views'); +const { template, i18n, userLink, renderStateChip, renderModuleStatsBy, paged, renderCardTop, renderEngagement } = require('./main_views'); const { renderStyledText, safeExternalHref } = require('../backend/renderStyledText'); const CourtsE2EChip = () => renderStateChip('encrypted', '๐Ÿ”’', i18n.encryptedChipLabel || 'E2E'); @@ -44,6 +44,9 @@ const PRESET_KEYS = { const presetLabel = (t) => i18n[PRESET_KEYS[t]] || t; const caseTitle = (title) => CASE_TITLE_PRESETS.reduce((acc, t) => acc.split(t).join(presetLabel(t)), String(title || '')); const statusLabel = (status) => String(i18n[`courtsStatus${String(status || '').toUpperCase()}`] || status || '').toUpperCase(); +const caseHref = (c) => `/courts/cases/${encodeURIComponent(c.id)}`; +const caseTop = (c, chips = [CourtsE2EChip()]) => renderCardTop(null, caseHref(c), { author: c.accuser, date: c.createdAt || c.openedAt, chips }); +const caseShare = (c) => renderEngagement(c.id, null, null, { href: caseHref(c), title: caseTitle(c.title) }); const FILTERS = [ { value: 'cases', key: 'courtsFilterCases' }, @@ -463,38 +466,32 @@ const CaseCard = (c) => { (c.isAccuser || c.isRespondent); return div( { class: 'card' }, + caseTop(c), + h2(caseTitle(c.title)), div( - { class: 'card-header' }, - div( - { class: 'card-header__meta' }, - h2(caseTitle(c.title)), - div({ class: 'card-chips-row' }, CourtsE2EChip()), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '), - span({ class: 'card-value' }, methodLabel(c.method)) - ), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, statusLabel(c.status)) - ), - c.answerBy - ? div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '), - span({ class: 'card-value' }, fmt(c.answerBy)) - ) - : null, - c.evidenceBy - ? div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '), - span({ class: 'card-value' }, fmt(c.evidenceBy)) - ) - : null - ) + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '), + span({ class: 'card-value' }, methodLabel(c.method)) ), + div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThStatus + ': '), + span({ class: 'card-value' }, statusLabel(c.status)) + ), + c.answerBy + ? div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '), + span({ class: 'card-value' }, fmt(c.answerBy)) + ) + : null, + c.evidenceBy + ? div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '), + span({ class: 'card-value' }, fmt(c.evidenceBy)) + ) + : null, div( { class: 'table-wrap mt-2' }, applyEl(table, { class: 'table table--centered' }, [ @@ -681,13 +678,12 @@ const MyCaseCard = (c) => { return div( { class: 'card' }, - br(), + caseTop(c), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThCase + ': '), span({ class: 'card-value' }, caseTitle(c.title)) ), - div({ class: 'card-chips-row' }, CourtsE2EChip()), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus + ': '), @@ -942,7 +938,7 @@ const HistoryList = (rows = []) => { const canShowDescription = hh.mine || hh.publicDetails; return div( { class: 'card' }, - br(), + caseTop(hh, []), hh.method ? div( { class: 'card-field' }, @@ -963,14 +959,7 @@ const HistoryList = (rows = []) => { span({ class: 'card-value' }, String(hh.status || '')) ), h2(caseTitle(hh.title)), - canShowDescription ? RichTextBlock(hh.description || '') : null, - form( - { - method: 'GET', - action: `/courts/cases/${encodeURIComponent(hh.id)}` - }, - button({ type: 'submit', class: 'create-button' }, i18n.courtsViewDetails) - ) + canShowDescription ? RichTextBlock(hh.description || '') : null ); }); return div({ class: 'cards' }, ...cards); @@ -1070,46 +1059,32 @@ const CaseDetailsBlock = (c) => { return div( { class: 'card case-details-card' }, - br(), + caseTop(c), + h2(caseTitle(c.title)), + c.method + ? div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '), + span({ class: 'card-value' }, methodLabel(c.method)) + ) + : null, div( - { class: 'card-header' }, - div( - { class: 'card-header__meta' }, - h2(caseTitle(c.title)), - div({ class: 'card-chips-row' }, CourtsE2EChip()), - c.method - ? div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '), - span({ class: 'card-value' }, methodLabel(c.method)) - ) - : null, - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThJudge + ': '), - span({ class: 'card-value' }, judgeValue) - ), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThStatus + ': '), - span({ class: 'card-value' }, statusLabel(c.status)) - ), - c.createdAt - ? div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThCreatedAt + ': '), - span({ class: 'card-value' }, fmt(c.createdAt)) - ) - : null, - c.decidedAt - ? div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '), - span({ class: 'card-value' }, fmt(c.decidedAt)) - ) - : null - ) + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThJudge + ': '), + span({ class: 'card-value' }, judgeValue) ), + div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThStatus + ': '), + span({ class: 'card-value' }, statusLabel(c.status)) + ), + c.decidedAt + ? div( + { class: 'card-field' }, + span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '), + span({ class: 'card-value' }, fmt(c.decidedAt)) + ) + : null, div( { class: 'table-wrap mt-2' }, applyEl(table, { class: 'table table--centered' }, [ @@ -1284,7 +1259,8 @@ const CaseDetailsBlock = (c) => { ) ) ) - : null + : null, + caseShare(c) ); }; diff --git a/src/views/document_view.js b/src/views/document_view.js index e609e1a..f991dfe 100644 --- a/src/views/document_view.js +++ b/src/views/document_view.js @@ -4,7 +4,7 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } const moment = require("../server/node_modules/moment"); const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderEcoTax, renderLifespanChip, renderCardTop, renderCardByline, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf, renderPdfDocument, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText"); @@ -23,7 +23,6 @@ const buildReturnTo = (filter, params = {}) => { return `/documents?${parts.join("&")}`; }; -const safeDomId = (prefix, key) => `${prefix}${String(key || "").replace(/[^A-Za-z0-9_-]/g, "_")}`; const renderTags = (tags) => { const list = safeArr(tags).map((t) => String(t || "").trim()).filter(Boolean); @@ -43,6 +42,15 @@ const renderDocumentCommentsSection = (documentKey, rootId, comments = [], retur }); }; +const renderDocumentUpdated = (doc) => { + const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; + const updatedTs = doc.updatedAt ? new Date(doc.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return showUpdated + ? span({ class: "votations-comment-date" }, `${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; +}; + const renderDocumentList = exports.renderDocumentList = (documents, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); @@ -50,55 +58,38 @@ const renderDocumentList = exports.renderDocumentList = (documents, filter, para ? documents.map((doc) => { const commentCount = typeof doc.commentCount === "number" ? doc.commentCount : 0; const title = safeText(doc.title); - const pdfId = safeDomId("pdf-container-", doc.key); + const href = `/documents/${encodeURIComponent(doc.key)}`; const isOwn = doc.author && String(doc.author) === String(userId); - const headerActions = typeof params.headerActions === "function" - ? params.headerActions(doc) + const byline = { + author: doc.author, + date: doc.createdAt, + chips: [ + doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, + renderLicenseChip(doc.license), + ...(typeof params.titleChips === "function" ? params.titleChips(doc) : []), + renderLifespanChip(doc.lifetime, i18n) + ], + bylineExtra: [renderDocumentUpdated(doc)] + }; + const header = typeof params.headerActions === "function" + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), params.headerActions(doc)) : doc.tribeOrigin - ? renderContentActions(null, doc.tribeOrigin.href) - : renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, author: doc.author, favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, isFavorite: doc.isFavorite, reportTitle: doc.title, deleteAction: isOwn ? contentDeleteAction('document', doc.key) : undefined, returnTo }); - const titleHref = typeof params.headerActions === "function" ? null : doc.tribeOrigin ? doc.tribeOrigin.href : `/documents/${encodeURIComponent(doc.key)}`; - const engagement = typeof params.engagement === "function" - ? params.engagement(doc) - : doc.tribeOrigin ? null : renderEngagement(doc.key, - renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderCommentsLink({ href: `/documents/${encodeURIComponent(doc.key)}`, count: commentCount }) - ); + ? renderCardTop(doc.key, doc.tribeOrigin.href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(doc.key)) || null, reportTitle: doc.title }) + : renderCardTop(doc.key, href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(doc.key)) || params.spreads || null, favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, isFavorite: doc.isFavorite, reportTitle: doc.title, deleteAction: isOwn ? contentDeleteAction('document', doc.key) : undefined, returnTo }); + const titleHref = typeof params.headerActions === "function" ? null : doc.tribeOrigin ? doc.tribeOrigin.href : href; + const engagement = typeof params.engagement === "function" ? params.engagement(doc) : null; return div( { class: "trending-card document-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + header, div( { class: "card-section document-card-body" }, - div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, doc.tribeOrigin ? renderTribeOriginChip(doc.tribeOrigin) : doc.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('documents', doc.title, doc.key)) : null, renderLicenseChip(doc.license), ...(typeof params.titleChips === "function" ? params.titleChips(doc) : [])), - doc.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(doc.lifetime, i18n)) : null, + title ? div({ class: "shop-title-row" }, h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title)) : null, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) + ? renderPdfDocument(blobUrlOf(doc.url), { title: doc.title }) : p(i18n.documentNoFile), ...(typeof params.bodyExtra === "function" ? params.bodyExtra(doc) : []), - engagement, - br(), - (() => { - const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; - const updatedTs = doc.updatedAt ? new Date(doc.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(doc.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })() + engagement ) ); }) @@ -122,7 +113,7 @@ const renderDocumentForm = (filter, documentId, docToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.documentFileLabel), br(), - input({ type: "file", name: "document", accept: "application/pdf", required: filter !== "edit" }), + (filter === "edit" && renderMediaLockNote(docToEdit)) || input({ type: "file", name: "document", accept: "application/pdf", required: filter !== "edit" }), br(), br(), label(i18n.documentTitleLabel), @@ -223,7 +214,7 @@ exports.documentView = async (documents, filter = "all", documentId = null, para ) ); - return `${tpl}`; + return tpl; }; exports.singleDocumentView = async (doc, filter = "all", comments = [], params = {}) => { @@ -233,9 +224,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = const returnTo = safeText(params.returnTo) || buildReturnTo(filter, { q, sort }); const title = safeText(doc.title); - const pdfId = safeDomId("pdf-container-", doc.key); const isAuthor = String(doc.author) === String(userId); - const hasOpinions = Object.keys(doc.opinions || {}).length > 0; const isClearnet = !!doc.clearnet; const chips = [ @@ -244,7 +233,7 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ].filter(Boolean); const sideActions = []; - if (isAuthor && !hasOpinions) { + if (isAuthor) { sideActions.push(form( { method: "GET", action: `/documents/edit/${encodeURIComponent(doc.key)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), @@ -253,27 +242,30 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = } const tagsNode = renderTags(doc.tags); + const href = `/documents/${encodeURIComponent(doc.key)}`; - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(doc.key, `/documents/${encodeURIComponent(doc.key)}`, { + const docSide = div({ class: "tribe-side" }, + renderCardTop(doc.key, href, { arrow: false, author: doc.author, + date: doc.createdAt, + chips: [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('documents', doc.title, doc.key) : null), + renderLicenseChip(doc.license), + ...chips + ], + bylineExtra: [renderEditedChip(doc.key, doc.rootId), renderDocumentUpdated(doc)], favKind: 'documents', torrentFrom: { blobId: doc.url, name: doc.title }, isFavorite: doc.isFavorite, spread: params.spreads || null, returnTo: buildReturnTo(filter, { q, sort }), reportTitle: doc.title, deleteAction: isAuthor ? contentDeleteAction('document', doc.key) : undefined - }) - ); - - const docSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('documents', doc.title, doc.key) : null), - isAuthor ? renderClearnetSwitch('documents', doc.rootId || doc.key, isClearnet) : null, - renderLicenseChip(doc.license) + isAuthor ? renderClearnetSwitch('documents', doc.rootId || doc.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(doc.key, href, doc.title), safeText(doc.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(doc.description)) : null, @@ -282,31 +274,12 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ); const docMain = div({ class: "tribe-main" }, - detailActions, doc?.url - ? div({ id: pdfId, class: "pdf-viewer-container", "data-pdf-url": blobUrlOf(doc.url) }) + ? renderPdfDocument(blobUrlOf(doc.url), { title: doc.title, preview: true }) : p(i18n.documentNoFile), - (() => { - const createdTs = doc.createdAt ? new Date(doc.createdAt).getTime() : NaN; - const updatedTs = doc.updatedAt ? new Date(doc.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(doc.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(doc.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.documentUpdatedAt}: ${moment(doc.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), renderEngagement(doc.key, renderOpinionsVoting('/documents/opinions', doc.key, doc.opinions, returnTo, doc.opinions_inhabitants), - renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo) - ) + renderDocumentCommentsSection(doc.key, doc.rootId || doc.key, comments, returnTo)) ); const tpl = template( @@ -336,6 +309,6 @@ exports.singleDocumentView = async (doc, filter = "all", comments = [], params = ) ); - return `${tpl}`; + return tpl; }; diff --git a/src/views/emergencies_view.js b/src/views/emergencies_view.js index b5c00e2..a90689e 100644 --- a/src/views/emergencies_view.js +++ b/src/views/emergencies_view.js @@ -1,5 +1,5 @@ const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, video: videoHyperaxe, table, tr, td } = require("../server/node_modules/hyperaxe"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderStateChip, renderCardTop, renderCardByline, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderEngagement, renderDetailShare } = require("./main_views"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); const { renderCommentsSection } = require("./comments_view"); const { renderMapEmbed } = require("./maps_view"); @@ -32,16 +32,13 @@ const emergencyHref = (emergency) => `/emergencies/${encodeURIComponent(emergenc const renderEmergencyCard = (emergency, params = {}) => div({ class: `tribe-card emergency-card emergency-card-${String(emergency.severity || "UNVERIFIED").toLowerCase()}` }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(emergency.id, emergencyHref(emergency), { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: (params.spreadMap && params.spreadMap.get(emergency.id)) || null, deleteAction: String(emergency.author) === String(userId) ? contentDeleteAction("emergency", emergency.id) : undefined }) - ), + renderCardTop(emergency.id, emergencyHref(emergency), { date: emergency.createdAt, chips: [severityChip(emergency), statusChip(emergency), categoryChip(emergency), emergency.clearnet === true && clearnetEligible(emergency) ? renderReachChip(true, i18n, clearnetItemHref("emergencies", emergency.title, emergency.id)) : null], author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: (params.spreadMap && params.spreadMap.get(emergency.id)) || null, deleteAction: String(emergency.author) === String(userId) ? contentDeleteAction("emergency", emergency.id) : undefined }), div({ class: "tribe-card-body" }, emergency.media && emergency.media.kind === "image" ? a({ href: emergencyHref(emergency) }, img({ loading: 'lazy', class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, alt: emergency.title || "" })) : null, emergency.media && emergency.media.kind === "video" ? videoHyperaxe({ class: "emergency-card-cover", src: `/blob/${encodeURIComponent(emergency.media.blobId)}`, controls: true, preload: "metadata" }) : null, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: emergencyHref(emergency) }, emergency.title || "โ€”"))), - div({ class: "card-chips-row" }, severityChip(emergency), statusChip(emergency), categoryChip(emergency), emergency.clearnet === true && clearnetEligible(emergency) ? renderReachChip(true, i18n, clearnetItemHref("emergencies", emergency.title, emergency.id)) : null), - emergency.expiresAt ? p({ class: "job-meta-line" }, `${i18n.emergencyExpiresAt}: ${moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")}`) : null + emergency.expiresAt ? p({ class: "job-meta-line" }, `${i18n.emergencyExpiresAt}: ${moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")}`) : null, + null ) ); @@ -155,30 +152,18 @@ exports.singleEmergencyView = async (emergency, params = {}) => { : []; const updates = Array.isArray(emergency.updates) ? emergency.updates : []; const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(emergency.id, href, { author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("emergency", emergency.id) : undefined }) - ), + renderCardTop(emergency.id, href, { arrow: false, date: emergency.createdAt, chips: [severityChip(emergency), statusChip(emergency), categoryChip(emergency), renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("emergencies", emergency.title, emergency.id) : null)], author: emergency.author, favKind: "emergencies", isFavorite: emergency.isFavorite, reportTitle: emergency.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("emergency", emergency.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, emergency.title)), - div({ class: "card-chips-row" }, - severityChip(emergency), statusChip(emergency), categoryChip(emergency), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("emergencies", emergency.title, emergency.id) : null), - isAuthor && canClearnet ? renderClearnetSwitch("emergencies", emergency.rootId || emergency.id, isClearnet) : null - ), - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.createdAt).format("YYYY/MM/DD HH:mm")) - ), - emergency.expiresAt - ? tr( + renderDetailShare(emergency.id, href, emergency.title), + isAuthor && canClearnet ? renderClearnetSwitch("emergencies", emergency.rootId || emergency.id, isClearnet) : null, + emergency.expiresAt + ? table({ class: "tribe-info-table jobs-info-table" }, + tr( td({ class: "tribe-info-label" }, i18n.emergencyExpiresAt), td({ class: "tribe-info-value", colspan: "3" }, moment(emergency.expiresAt).format("YYYY/MM/DD HH:mm")) ) - : null, - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(emergency.author)) - ) - ), + ) + : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.emergencyConfirmations}: ${emergency.confirmationCount || 0}`) ), @@ -215,10 +200,10 @@ exports.singleEmergencyView = async (emergency, params = {}) => { const editing = params.editUpdate && params.editUpdate === u.id; return div({ class: "emergency-update", id: `update-${encodeURIComponent(u.id)}` }, div({ class: "emergency-update-head" }, - div({ class: "card-chips-row" }, + renderCardByline({ author: u.author, date: u.createdAt, chips: [ renderStateChip(u.confirmationCount > 0 ? "mutuals" : "neutral", "โœ“", `${i18n.emergencyConfirmations} ยท ${u.confirmationCount || 0}`), u.edited ? renderStateChip("neutral", "", i18n.emergencyUpdateEdited) : null - ), + ] }), div({ class: "tribe-side-actions emergency-update-actions" }, !mineUpdate && !confirmedUpdate && emergency.status === "ACTIVE" ? form({ method: "POST", action: `/emergencies/updates/${encodeURIComponent(u.id)}/confirm` }, input({ type: "hidden", name: "emergency", value: emergency.id }), button({ type: "submit", class: "tribe-action-btn" }, `โœ“ ${String(i18n.emergencyConfirmButton).toUpperCase()}`)) @@ -238,8 +223,7 @@ exports.singleEmergencyView = async (emergency, params = {}) => { input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), button({ type: "submit", class: "create-button" }, i18n.emergencyUpdate) ) - : div({ class: "emergency-update-text" }, ...renderStyledText(u.text)), - p({ class: "card-footer" }, span({ class: "date-link" }, `${moment(u.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(u.author)) + : div({ class: "emergency-update-text" }, ...renderStyledText(u.text)) ); })) : p(i18n.emergencyNoUpdates), @@ -251,7 +235,7 @@ exports.singleEmergencyView = async (emergency, params = {}) => { ) : null ), - renderCommentsSection({ action: `/emergencies/${encodeURIComponent(emergency.id)}/comments`, comments: params.comments || [], returnTo: href }) + renderEngagement(emergency.id, null, renderCommentsSection({ action: `/emergencies/${encodeURIComponent(emergency.id)}/comments`, comments: params.comments || [], returnTo: href })) ); return template( emergency.title, diff --git a/src/views/event_view.js b/src/views/event_view.js index 220277c..20e9e1b 100644 --- a/src/views/event_view.js +++ b/src/views/event_view.js @@ -1,12 +1,12 @@ const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, details, summary, ul, li, hr } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderOpenClosedChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderCardTop, renderCardByline, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views"); const { renderPhotoGallery, renderGalleryFields } = require("./gallery_view"); const { renderIntervalBlock } = require("./calendars_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); -const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view"); +const { renderMapEmbed } = require("./maps_view"); const { renderReachChip, renderEncryptedChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const userId = config.keys.id; @@ -140,22 +140,21 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo, opts = const dateText = e.date ? moment(e.date).format("YYYY/MM/DD HH:mm") : ""; const isOwn = e.organizer && String(e.organizer) === String(userId); - const headerActions = opts.headerActions !== undefined - ? opts.headerActions - : origin ? null : renderContentActions(e.id, `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`, { spread: spreadInfo || null, author: e.organizer || e.author, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title, deleteAction: isOwn ? contentDeleteAction('event', e.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }); + const byline = { author: e.organizer || e.author, date: e.createdAt, chips: chips.length ? [div({ class: "card-chips-row" }, ...chips)] : [] }; + const detailHref = `/events/${encodeURIComponent(e.id)}?filter=${encodeURIComponent(currentFilter)}`; + const cardTop = opts.headerActions !== undefined + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), opts.headerActions) + : origin + ? renderCardTop(e.id, href, { ...byline, spread: spreadInfo || null, reportTitle: e.title }) + : renderCardTop(e.id, detailHref, { ...byline, spread: spreadInfo || null, favKind: 'events', isFavorite: e.isFavorite, reportTitle: e.title, deleteAction: isOwn ? contentDeleteAction('event', e.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }); return div({ class: "trending-card event-card" + (isOwn ? " own-content" : "") }, - headerActions ? div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ) : null, + cardTop, div({ class: "card-section event-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, href ? a({ href }, e.title || i18n.eventsTitle) : (e.title || i18n.eventsTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, e.location && String(e.location).trim() ? div({ class: "card-field card-field-mb" }, span({ class: "card-label" }, `${String(i18n.eventLocation).toUpperCase()}: `), @@ -169,7 +168,8 @@ const renderEventItem = exports.renderEventItem = (e, filter, spreadInfo, opts = origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${attendees.length}`) ), - ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []), + null ) ); }; @@ -332,8 +332,7 @@ exports.eventView = async (events, filter, eventId, returnTo, params = {}) => { ), br(), ...renderGalleryFields(formData, currentFilter === "edit"), - label(i18n.uploadMedia), br(), - input({ type: "file", name: "blob" }), br(), br(), + ...(currentFilter === "edit" && renderMediaLockNote(formData) ? [] : [label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" }), br(), br()]), br(), label(i18n.eventDateLabel), br(), @@ -459,7 +458,6 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { renderPrivacyChip(isPrivate, i18n), renderEventStatusChip(event.status), isEncrypted ? renderEncryptedChip(i18n) : renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('events', event.title, event.id) : null), - isOrganizer && canClearnet ? renderClearnetSwitch('events', event.rootId || event.id, isClearnet) : null, isAttending ? renderStateChip("whole", "โ˜…", i18n.eventAttended) : null, renderLifespanChip(event.lifetime, i18n), renderEcoTax(event.msgSize, event.id), @@ -514,15 +512,14 @@ exports.singleEventView = async (event, filter, comments = [], params = {}) => { : null; const eventSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(event.id, returnToSelf, { spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title, deleteAction: isOrganizer ? contentDeleteAction('event', event.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }) - ), + renderCardTop(event.id, returnToSelf, { arrow: false, date: event.createdAt, chips, bylineExtra: [renderEditedChip(event.id, event.rootId)], spread: params.spreads || null, author: event.organizer || event.author, favKind: 'events', isFavorite: event.isFavorite, reportTitle: event.title, deleteAction: isOrganizer ? contentDeleteAction('event', event.id) : undefined, returnTo: `/events?filter=${encodeURIComponent(currentFilter)}` }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, event.title) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(event.id, returnToSelf, event.title), + isOrganizer && canClearnet ? renderClearnetSwitch('events', event.rootId || event.id, isClearnet) : null, renderPhotoGallery(event, 'event'), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(event.createdAt, event.organizer)), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), event.recurring && Array.isArray(event.occurrences) && event.occurrences.length > 1 ? details({ class: "event-occurrences" }, summary({ class: "chat-thread-summary" }, `${i18n.eventUpcomingDates} (${event.occurrences.length})`), diff --git a/src/views/favorites_view.js b/src/views/favorites_view.js index 4642562..8b9cd11 100644 --- a/src/views/favorites_view.js +++ b/src/views/favorites_view.js @@ -1,6 +1,6 @@ const { form, button, div, h2, p, section, input, a, span, img } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require("./main_views"); +const { template, i18n, renderCardTop, renderModuleStats, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); @@ -128,26 +128,26 @@ const renderFavoriteCard = (item, filter, spreadMap = null) => { const title = safeText(item.title) || safeText(item.name) || safeText(item.category) || safeText(item.url) || ""; - const ts = item.updatedAt || item.createdAt; - const absDate = ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; + const updated = item.updatedAt && item.createdAt && moment(item.updatedAt).valueOf() !== moment(item.createdAt).valueOf() + ? span({ class: "card-byline-time" }, `${i18n.jobsUpdatedAt}: ${moment(item.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; const isOwn = item.author && String(item.author) === String(userId); return div( { class: "trending-card favorites-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - div({ class: 'card-chips-row' }, + renderCardTop(item.favId, item.viewHref, { + date: item.createdAt || item.updatedAt, + bylineExtra: [updated], + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(item.kind || '').toUpperCase()) ) - ), - renderContentActions(item.favId, item.viewHref, { - author: item.author, - spread: (spreadMap && spreadMap.get(item.favId)) || null, - reportTitle: title, - ...(TORRENTABLE_KINDS.has(item.kind) && typeof item.url === "string" && item.url.startsWith("&") ? { torrentFrom: { blobId: item.url, name: item.title } } : {}) - }) - ), + )], + author: item.author, + spread: (spreadMap && spreadMap.get(item.favId)) || null, + reportTitle: title, + ...(TORRENTABLE_KINDS.has(item.kind) && typeof item.url === "string" && item.url.startsWith("&") ? { torrentFrom: { blobId: item.url, name: item.title } } : {}) + }), div( { class: "card-section favorites-card-body" }, title ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.viewHref ? a({ href: item.viewHref }, title) : title)) : null, @@ -156,11 +156,6 @@ const renderFavoriteCard = (item, filter, spreadMap = null) => { safeText(item.description) ? p(...renderStyledText(item.description)) : null, renderDetailFields(item), renderTags(item.tags), - p( - { class: "card-footer favorites-meta" }, - absDate ? span({ class: "date-link" }, `${absDate}`) : "", - item.author ? userLink(item.author) : "" - ), p( { class: "card-footer" }, form( diff --git a/src/views/feed_view.js b/src/views/feed_view.js index dc561ce..daf9d42 100644 --- a/src/views/feed_view.js +++ b/src/views/feed_view.js @@ -1,11 +1,9 @@ -const { div, h2, p, section, button, form, a, span, textarea, br, input, h1, label, img } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, span, textarea, input, h1, label, img } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } = require("./comments_view"); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, userLink, renderContentActions, renderEngagement, renderModuleStats, moduleIsEmpty, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, userLink, renderCardTop, renderCardByline, renderEngagement, renderModuleStats, moduleIsEmpty, paged, renderDetailShare } = require("./main_views"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); -const { renderStyledHtml, renderStyledText } = require("../backend/renderStyledText"); -const moment = require("../server/node_modules/moment"); -const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const { renderStyledText } = require("../backend/renderStyledText"); const FEED_TEXT_MIN = Number(config?.feed?.minLength ?? 1); const FEED_TEXT_MAX = Number(config?.feed?.maxLength ?? 280); @@ -21,10 +19,9 @@ const normalizeOptions = (opts) => { }; }; -const formatDate = (feed) => { - const ts = feed?.value?.timestamp || Date.parse(feed?.value?.content?.createdAt || "") || 0; - return ts ? moment(ts).format("YYYY/MM/DD HH:mm") : ""; -}; +const feedDateOf = (feed) => feed?.value?.timestamp || feed?.value?.content?.createdAt || null; + +const renderEditedBadge = (content) => content._textEdited ? span({ class: "edited-badge" }, ` ยท ${i18n.edited || "edited"}`) : null; const extractTags = (text) => { const list = (String(text || "").match(/#[A-Za-z0-9_]{1,32}/g) || []).map((t) => t.slice(1).toLowerCase()); @@ -85,7 +82,6 @@ const renderFeedComposer = ({ text = "", media = "", rows = 5, clearnet = false, { method: "POST", action: "/feed/create", enctype: "multipart/form-data" }, textarea({ name: "text", - required: true, minlength: String(FEED_TEXT_MIN), maxlength: String(FEED_TEXT_MAX), rows: String(rows), @@ -117,7 +113,6 @@ const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts = const voteEntries = Object.entries(content.opinions || {}); const totalCount = voteEntries.reduce((sum, [, count]) => sum + (Number(count) || 0), 0); - const createdAt = formatDate(feed); const me = config?.keys?.id; const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; @@ -129,63 +124,27 @@ const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts = const styledNodes = renderStyledText(safeText, { zoomImages: true, ...(typeof opts.hashtagHref === 'function' ? { hashtagHref: opts.hashtagHref } : {}) }); const refeedAction = opts.refeedAction || `/feed/refeed/${encodeURIComponent(feed.key)}`; const canRefeed = opts.canRefeed !== false && !(authorId && String(authorId) === String(me)); - const headerActions = opts.headerActions !== undefined - ? opts.headerActions + const feedHref = `/feed/${encodeURIComponent(feed.key)}`; + const byline = { author: authorId, date: feedDateOf(feed), bylineExtra: [renderEditedBadge(content)] }; + const cardTop = opts.headerActions !== undefined + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), opts.headerActions) : feed.tribeOrigin - ? renderContentActions(null, feed.tribeOrigin.href) - : renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { - author: authorId, + ? renderCardTop(feed.key, feed.tribeOrigin.href, { ...byline, chips: [renderTribeOriginChip(feed.tribeOrigin)], spread: (spreadMap && spreadMap.get(feed.key)) || null, reportTitle: safeText }) + : renderCardTop(feed.key, feedHref, { + ...byline, + chips: feed.clearnet === true ? [renderReachChip(true, i18n, clearnetItemHref("feed", "", feed.key))] : [], spread: (spreadMap && spreadMap.get(feed.key)) || null, reportTitle: safeText, ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) }); - const chipsRow = opts.chips !== undefined - ? opts.chips - : feed.tribeOrigin ? div({ class: "card-chips-row" }, renderTribeOriginChip(feed.tribeOrigin)) : feed.clearnet === true ? div({ class: "card-chips-row" }, renderReachChip(true, i18n, clearnetItemHref("feed", "", feed.key))) : null; + const chipsRow = opts.chips !== undefined ? opts.chips : null; const engagement = opts.engagement !== undefined ? opts.engagement - : feed.tribeOrigin ? null : renderEngagement(feed.key, - renderOpinionsVoting('/feed/opinions', feed.key, content.opinions, null, content.opinions_inhabitants), - renderCommentsLink({ href: `/feed/${encodeURIComponent(feed.key)}`, count: commentCount }) - ); - - if (process.env.OASIS_MOBILE === '1') { - return div( - { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, span(), headerActions), - div( - { class: "card-section feed-card-body" }, - div({ class: "feed-text" }, ...styledNodes), - chipsRow, - feed.tribeOrigin ? null : div( - { class: "refeed-column" }, - h1(String(refeedsNum)), - canRefeed - ? form( - { method: "POST", action: refeedAction }, - button({ class: alreadyRefeeded ? "refeed-btn active" : "refeed-btn", type: "submit", title: i18n.refeedButton, "aria-label": i18n.refeedButton, ...(alreadyRefeeded ? { disabled: true } : {}) }, "\u25b2") - ) - : null, - alreadyRefeeded ? p({ class: "muted" }, i18n.alreadyRefeeded) : null - ), - engagement, - p( - { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt}`), - userLink(authorId), - content._textEdited ? span({ class: "edited-badge" }, ` \u00b7 ${i18n.edited || "edited"}`) : null - ) - ) - ); - } + : null; return div( { class: "trending-card feed-card" + (authorId && String(authorId) === String(me) ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + cardTop, div( { class: "card-section feed-card-body" }, div( @@ -203,13 +162,7 @@ const renderFeedCard = exports.renderFeedCard = (feed, spreadMap = null, opts = div( { class: "feed-main" }, div({ class: "feed-text" }, ...styledNodes), - chipsRow, - p( - { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt}`), - userLink(authorId), - content._textEdited ? span({ class: "edited-badge" }, ` ยท ${i18n.edited || "edited"}`) : null - ) + chipsRow ) ), engagement @@ -355,7 +308,7 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { const safeText = rawText.trim(); const authorId = content.author || feed.value?.author || ""; const signerId = feed.value?.author || ""; - const createdAt = formatDate(feed); + const feedHref = `/feed/${encodeURIComponent(feed.key)}`; const styledNodes = renderStyledText(safeText, { zoomImages: true }); const me = config?.keys?.id; const alreadyRefeeded = Array.isArray(content.refeeds_inhabitants) && me ? content.refeeds_inhabitants.includes(me) : false; @@ -380,16 +333,15 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { ), div( { class: "bookmark-item card feed-detail-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(feed.key, `/feed/${encodeURIComponent(feed.key)}`, { - spread: params.spreads || null, - author: authorId, - reportTitle: safeText, - ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) - }) - ), - br, + renderCardTop(feed.key, feedHref, { arrow: false, + date: feedDateOf(feed), + bylineExtra: [renderEditedBadge(content)], + chips: [renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("feed", "", feed.key) : null)], + spread: params.spreads || null, + author: authorId, + reportTitle: safeText, + ...(((signerId && String(signerId) === String(me)) || (authorId && String(authorId) === String(me))) ? { deleteAction: `/feed/delete/${encodeURIComponent(feed.key)}` } : {}) + }), div( { class: "feed-row" }, div( @@ -411,15 +363,8 @@ exports.singleFeedView = (feed, comments = [], params = {}) => { tags.map((tag) => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)) ) : null, - div({ class: "card-chips-row" }, renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref("feed", "", feed.key) : null)), - authorId && String(authorId) === String(me) ? renderClearnetSwitch("feed", feed.rootId || feed.key, isClearnet) : null, - br, - p( - { class: "card-footer" }, - span({ class: "date-link" }, `${createdAt}`), - userLink(authorId), - content._textEdited ? span({ class: "edited-badge" }, ` ยท ${i18n.edited || "edited"}`) : null - ) + renderDetailShare(feed.key, feedHref, safeText), + authorId && String(authorId) === String(me) ? renderClearnetSwitch("feed", feed.rootId || feed.key, isClearnet) : null ) ) ), diff --git a/src/views/files_view.js b/src/views/files_view.js index 6fc9773..93c33a5 100644 --- a/src/views/files_view.js +++ b/src/views/files_view.js @@ -1,8 +1,8 @@ -const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require('./clearnet_view'); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseSelect, renderLicenseChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderEcoTax, renderLifespanChip, renderSpreadEditWarning, renderCardTop, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, formatSize, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledText, safeExternalHref } = require("../backend/renderStyledText"); @@ -35,9 +35,8 @@ const renderTags = (tags) => { const renderFileOwnerActions = (filter, fileObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(fileObj.author) === String(userId); - const hasOpinions = Object.keys(fileObj.opinions || {}).length > 0; - if (!isAuthor || hasOpinions) return []; + if (!isAuthor) return []; return [ form( @@ -62,13 +61,6 @@ const blobSha256Hex = (blobId) => { try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } }; -const formatSize = (bytes) => { - const n = Number(bytes) || 0; - if (n === 0) return "โ€”"; - if (n < 1024) return n + " B"; - if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; - return (n / (1024 * 1024)).toFixed(1) + " MB"; -}; const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; const renderFileSeeds = (t, spreadMap) => { @@ -141,6 +133,7 @@ const renderNextcloudForm = (returnTo) => div( input({ type: "text", name: "tags", placeholder: i18n.fileTagsPlaceholder }), br(), br(), + ...renderLicenseSelect(undefined, i18n), renderClearnetSelector(false, i18n), br(), button({ type: "submit" }, i18n.nextcloudUploadButton) ) @@ -163,7 +156,7 @@ const renderFileForm = (filter, fileId, fileToEdit, params = {}) => { tribeId ? input({ type: "hidden", name: "tribeId", value: tribeId }) : null, span(i18n.fileFileLabel), br(), - input({ type: "file", name: "file", required: filter !== "edit" }), + (filter === "edit" && renderMediaLockNote(fileToEdit)) || input({ type: "file", name: "file", required: filter !== "edit" }), br(), br(), span(i18n.fileTitleLabel), @@ -184,6 +177,7 @@ const renderFileForm = (filter, fileId, fileToEdit, params = {}) => { }), br(), br(), + ...renderLicenseSelect(fileToEdit?.license, i18n), ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!fileToEdit?.clearnet : false, i18n), br()] : []), button({ type: "submit" }, filter === "edit" ? i18n.fileUpdateButton : i18n.fileCreateButton) ) @@ -297,11 +291,26 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params = for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(fileObj.tags); + const href = `/files/${encodeURIComponent(fileObj.key)}`; + const createdTs = fileObj.createdAt ? new Date(fileObj.createdAt).getTime() : NaN; + const updatedTs = fileObj.updatedAt ? new Date(fileObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(fileObj.key, `/files/${encodeURIComponent(fileObj.key)}`, { - spreadTitle: i18n.seedAction, + const fileSide = div({ class: "tribe-side" }, + renderCardTop(fileObj.key, href, { arrow: false, author: fileObj.author, + date: fileObj.createdAt, + chips: [ + fileObj.tribeId && fileObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('files', fileObj.title, fileObj.key) : null) : null, + fileObj.tribeId ? null : renderLicenseChip(fileObj.license), + ...chips + ], + bylineExtra: [ + renderEditedChip(fileObj.key, fileObj.rootId), + showUpdated ? span({ class: "votations-comment-date" }, `${i18n.fileUpdatedAt}: ${moment(fileObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) : null + ], + spreadTitle: i18n.seedAction, deleteAction: String(fileObj.author) === String(userId) ? contentDeleteAction("file", fileObj.key) : null, favKind: 'files', ...(fileObj.url && fileObj.url.startsWith("&") ? { torrentFrom: { blobId: fileObj.url, name: fileObj.fileName || fileObj.title } } : {}), @@ -309,17 +318,12 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params = spread: params.spreads || null, returnTo, reportTitle: fileObj.title - }) - ); - - const fileSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - fileObj.tribeId && fileObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), - clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('files', fileObj.title, fileObj.key) : null) : null, clearnetAllowed && isAuthor ? renderClearnetSwitch('files', fileObj.rootId || fileObj.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(fileObj.key, href, fileObj.title), safeText(fileObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(fileObj.description)) : null, @@ -328,7 +332,6 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params = ); const fileMain = div({ class: "tribe-main" }, - detailActions, fileObj.url && fileObj.url.startsWith("&") ? div({ class: "torrent-download" }, div({ class: "torrent-oasis torrent-detail-section" }, @@ -368,27 +371,9 @@ exports.singleFileView = async (fileObj, filter = "all", comments = [], params = ) ) : p(i18n.fileNoFile), - (() => { - const createdTs = fileObj.createdAt ? new Date(fileObj.createdAt).getTime() : NaN; - const updatedTs = fileObj.updatedAt ? new Date(fileObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(fileObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(fileObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.fileUpdatedAt}: ${moment(fileObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), - fileObj.tribeId ? null : renderEngagement(fileObj.key, - renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants), - renderFileCommentsSection(fileObj.key, comments, returnTo) - ) + renderEngagement(fileObj.key, + fileObj.tribeId ? null : renderOpinionsVoting('/files/opinions', fileObj.key, fileObj.opinions, returnTo, fileObj.opinions_inhabitants), + fileObj.tribeId ? null : renderFileCommentsSection(fileObj.key, comments, returnTo)) ); return template( diff --git a/src/views/forum_view.js b/src/views/forum_view.js index 0f7be4b..0f91ed3 100644 --- a/src/views/forum_view.js +++ b/src/views/forum_view.js @@ -4,7 +4,7 @@ const { } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderStateChip, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged } = require('./main_views'); +const { template, i18n, userLink, renderSpreadButton, renderPrivacyChip, renderLifespanChip, renderContentActions, renderCardTop, renderEngagement, renderInviteQrCard, renderStateChip, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, paged, forumReplyAnchor } = require('./main_views'); const { renderEncryptedChip: renderForumEncryptedChip, renderTribeOriginChip } = require('./clearnet_view'); const { config } = require('../server/SSB_server.js'); const { renderStyledText } = require('../backend/renderStyledText'); @@ -110,7 +110,6 @@ const renderForumForm = (draft = null) => label(i18n.forumMessageLabel), br(), textarea({ maxlength: String(TEXT_CAP), name: 'text', - required: true, rows: 8, placeholder: i18n.forumMessagePlaceholder }, draft ? String(draft.text || '') : ''), br(), br(), @@ -148,7 +147,7 @@ const renderThread = (nodes, level = 0, forumId, bestKey = null) => { ].filter(Boolean).join(' '); const commentBox = div( - { class: classList, id: `msg-${String(m.key || '').replace(/[^a-zA-Z0-9]/g, '')}` }, + { class: classList, id: forumReplyAnchor(m.key) }, div({ class: 'comment-header' }, span({ class: 'date-link' }, `${moment(m.timestamp).format('YYYY/MM/DD HH:mm')}`), @@ -184,7 +183,6 @@ const renderThread = (nodes, level = 0, forumId, bestKey = null) => { textarea({ maxlength: String(TEXT_CAP), name: 'message', rows: 4, - required: true, placeholder: i18n.forumMessagePlaceholder, class: 'comment-textarea' }, draftFor(m.key)), @@ -212,34 +210,45 @@ const renderForumList = (forums, currentFilter, spreadMap = new Map()) => { ...paged(visibleForums).map(f => { if (f.tribeOrigin) { return li({ class: 'mailing-archive-item' }, - div({ class: 'emergency-update-head mailing-archive-head' }, - div({ class: 'mailing-archive-meta' }, - a({ href: f.tribeOrigin.href, class: 'user-link' }, f.title || 'โ€”'), + renderCardTop(f.key, f.tribeOrigin.href, { + spread: (spreadMap && spreadMap.get(f.key)) || null, + reportTitle: f.title, + author: f.author, + date: f.createdAt, + chips: [ ALL_CATS.includes(f.category) ? a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`) : (f.category ? span({ class: 'forum-category' }, `[${tribeCatLabel(f.category)}]`) : null), - span({ class: 'card-label activity-update-counts mailing-counts' }, `๐Ÿ‘ฅ: ${f.participants?.length || 1} ยท ๐Ÿ’ฌ ${Math.max(0, (f.messagesCount || 1) - 1)}`), renderLifespanChip(f.lifetime, i18n), renderTribeOriginChip(f.tribeOrigin) - ) - ) + ] + }), + div({ class: 'mailing-archive-meta' }, + a({ href: f.tribeOrigin.href, class: 'user-link' }, f.title || 'โ€”'), + span({ class: 'card-label activity-update-counts mailing-counts' }, `๐Ÿ‘ฅ: ${f.participants?.length || 1} ยท ๐Ÿ’ฌ ${Math.max(0, (f.messagesCount || 1) - 1)}`) + ), + null ); } return li({ class: 'mailing-archive-item' }, - div({ class: 'emergency-update-head mailing-archive-head' }, - div({ class: 'mailing-archive-meta' }, - a({ href: forumHref(f), class: 'user-link' }, f.title || 'โ€”'), + renderCardTop(f.key, forumHref(f), { + date: f.createdAt, + chips: [ a({ class: 'forum-category', href: `/forum?filter=${encodeURIComponent(f.category)}` }, `[${catLabel(f.category)}]`), - forumCounts(f), f.isPrivate ? renderPrivacyChip(true, i18n) : null, f.isPrivate ? renderForumEncryptedChip(i18n) : null, renderLifespanChip(f.lifetime, i18n), f.subscriptionIn === true ? renderStateChip('mutuals', 'โœ‰', i18n.subscriptionOn) : (f.subscriptionIn === false ? renderStateChip('closed', 'โœ‰', i18n.subscriptionOff) : null) - ), - renderContentActions(f.key, forumHref(f), { spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title, deleteAction: f.author === userId ? `/forum/delete/${encodeURIComponent(f.key)}` : null }) - ) + ], + spread: spreadMap.get(f.key) || null, author: f.author, favKind: 'forum', isFavorite: f.isFavorite, reportTitle: f.title, deleteAction: f.author === userId ? `/forum/delete/${encodeURIComponent(f.key)}` : null + }), + div({ class: 'mailing-archive-meta' }, + a({ href: forumHref(f), class: 'user-link' }, f.title || 'โ€”'), + forumCounts(f) + ), + null ); }) ); @@ -425,7 +434,6 @@ exports.singleForumView = async (forum, messagesData, currentFilter, replyId = n textarea({ maxlength: String(TEXT_CAP), name: 'message', rows: 8, - required: true, placeholder: i18n.forumMessagePlaceholder, class: 'new-message-textarea' }, draftFor(null)), br(), diff --git a/src/views/gallery_view.js b/src/views/gallery_view.js index 741661c..ed4b3e5 100644 --- a/src/views/gallery_view.js +++ b/src/views/gallery_view.js @@ -1,5 +1,5 @@ const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe") -const { i18n } = require("./main_views") +const { i18n, renderIcon, renderMediaLockNote } = require("./main_views") const { MAX_IMAGES } = require("../models/media_gallery") const safeArr = (v) => (Array.isArray(v) ? v : []) @@ -33,20 +33,31 @@ const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry) const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}` let zoomSeq = 0 -const renderZoomableImage = (src, { alt = "", imgClass = "", linkClass = "" } = {}) => { +const renderZoomableImage = (src, { alt = "", imgClass = "", linkClass = "", thumb = null, full = false } = {}) => { if (!src) return null zoomSeq = (zoomSeq + 1) % 1000000 const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}` return [ - a({ href: `#${id}`, id: `${id}-src`, class: linkClass ? `zoom-link ${linkClass}` : "zoom-link" }, img({ src, class: imgClass, alt })), - div({ id, class: "lightbox" }, + a({ href: `#${id}`, id: `${id}-src`, class: linkClass ? `zoom-link ${linkClass}` : "zoom-link" }, img({ src: thumb || src, class: imgClass, alt, loading: "lazy" })), + div({ id, class: full ? "lightbox live-lightbox" : "lightbox" }, a({ href: downloadHref(src), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "โค“"), a({ href: `#${id}-src`, class: "lightbox-close" }, "ร—"), - img({ src, class: "lightbox-image", alt }) - ) + full ? a({ href: `#${id}-full`, class: "live-fullscreen-btn", title: i18n.liveFullscreen }, renderIcon("fullscreen")) : null, + img({ src, class: "lightbox-image", alt, loading: "lazy" }) + ), + full + ? div({ id: `${id}-full`, class: "lightbox live-lightbox live-fullscreen" }, + a({ href: `#${id}`, class: "lightbox-close" }, "ร—"), + a({ href: `#${id}`, class: "lightbox-image-link" }, img({ src, class: "lightbox-image", alt, loading: "lazy" })) + ) + : null ] } +const renderDownloadButton = (src, name = "") => src + ? a({ href: `${downloadHref(src)}${name ? `&name=${encodeURIComponent(name)}` : ""}`, class: "filter-btn media-download-btn" }, `โค“ ${i18n.fileShareDownload || "Download"}`) + : null + const renderPhotoGallery = (item, scope = "media") => { const list = imagesOf(item) const clip = videoOf(item) @@ -79,16 +90,17 @@ const renderPhotoGallery = (item, scope = "media") => { const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => { const list = imagesOf(item) + const lock = isEdit ? renderMediaLockNote(item) : null return [ label(i18n.galleryImages), br(), ...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })), - list.length < maxImages + lock || (list.length < maxImages ? div({ class: "media-file-row" }, input({ type: "file", name: "images", accept: "image/*", multiple: true }), button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto) ) - : null, + : null), br(), list.length ? [ @@ -96,7 +108,7 @@ const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) list.map((entry, i) => span({ class: "gallery-item" }, renderMediaThumb(entry), - button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto) + lock ? null : button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto) ) ) ), @@ -117,5 +129,7 @@ module.exports = { renderMediaThumb, renderPhotoGallery, renderGalleryFields, - renderZoomableImage + renderZoomableImage, + renderDownloadButton, + downloadHref } diff --git a/src/views/graphos_view.js b/src/views/graphos_view.js index e291f6b..96dcc49 100644 --- a/src/views/graphos_view.js +++ b/src/views/graphos_view.js @@ -97,7 +97,7 @@ const buildGraphSvg = (me, peers, links = []) => { const zoomableGraph = (id, svg, stats = []) => [ div({ class: 'graphos-legend graphos-statsbar' }, ...stats.filter(Boolean)), div({ class: 'graphos-canvas' }, - a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, `\u2922 ${i18n.graphosZoom || 'Enlarge'}`), + a({ href: process.env.OASIS_MOBILE === '1' ? `#${id}-focus` : `#${id}`, id: `${id}-src`, class: 'tribe-action-btn graphos-zoom-btn' }, span({ class: 'graphos-zoom-icon' }, '\u2922'), ` ${String(i18n.graphosZoom || 'Enlarge').toUpperCase()}`), div({ class: 'graphos-canvas-svg', [RAW_HTML]: svg }) ), div({ id, class: 'lightbox graphos-lightbox' }, @@ -158,13 +158,13 @@ exports.graphosView = ({ filter, me, peers, links = [], kpis, focus = null, focu : null, ...zoomableGraph('graphos-zoom', buildGraphSvg(me, peers, links), [ kpi(i18n.graphosTotalNodes || 'Total nodes', kpis.total), - kpi(i18n.online || 'Online', kpis.online), + kpi(i18n.onlinePlural || i18n.online || 'Online', kpis.online), filter !== 'MINE' ? kpi(i18n.discovered || 'Discovered', kpis.discovered) : null, filter !== 'MINE' ? kpi(i18n.unknown || 'Unknown', kpis.unknown) : null ]), div({ class: 'graphos-legend' }, legendItem('me', i18n.graphosYou || 'You'), - legendItem('online', i18n.online || 'Online'), + legendItem('online', i18n.onlinePlural || i18n.online || 'Online'), filter !== 'MINE' ? legendItem('discovered', i18n.discovered || 'Discovered') : null, filter !== 'MINE' ? legendItem('unknown', i18n.unknown || 'Unknown') : null, span({ class: 'graphos-legend-item' }, span({ class: 'graphos-legend-line graphos-fed-line-mutual' }), span(i18n.graphosFedMutual)), diff --git a/src/views/housing_view.js b/src/views/housing_view.js index a0b5d45..c1e3e4a 100644 --- a/src/views/housing_view.js +++ b/src/views/housing_view.js @@ -1,8 +1,8 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, video, table, tr, td, hr } = require("../server/node_modules/hyperaxe") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") -const { clearnetItemHref, template, i18n, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") -const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderMediaThumb, renderPhotoGallery, renderGalleryFields } = require("./gallery_view") +const { clearnetItemHref, template, i18n, renderOpenClosedChip, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderCardTop, renderOpinionsVoting, renderEngagement, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderEditedChip, renderDetailShare } = require("./main_views") +const { blobUrl, blobIdOf, isVideoEntry, imagesOf, renderPhotoGallery, renderGalleryFields } = require("./gallery_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderStyledText } = require("../backend/renderStyledText") @@ -115,7 +115,7 @@ const renderInfoTable = (item) => { const mapNode = renderMapLocationVisitLabel(item.mapUrl) if (mapNode) pushRow(i18n.mapLocationTitle || "Map", mapNode) } - return table({ class: "tribe-info-table housing-info-table" }, ...rows, ...renderCreatedRows(item.createdAt, item.author)) + return table({ class: "tribe-info-table housing-info-table" }, ...rows) } const today = () => moment().format("YYYY-MM-DD") @@ -179,11 +179,9 @@ const renderHousingList = (items, filter, params = {}) => { item.clearnet === true && clearnetEligible(item) ? renderReachChip(true, i18n, housingClearnetHref(item)) : null ].filter(Boolean) + const itemHref = `/housing/${encodeURIComponent(item.id)}` return div({ class: "trending-card housing-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwn ? contentDeleteAction("housing", item.id) : null }) - ), + renderCardTop(item.id, itemHref, { date: item.createdAt, chips, spread: params.spreadMap && params.spreadMap.get(item.id) || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwn ? contentDeleteAction("housing", item.id) : null }), div({ class: "card-section housing-card-body" }, clip || (cover && isVideoEntry(cover)) ? div({ class: "tribe-card-image-wrapper housing-card-video" }, @@ -191,7 +189,7 @@ const renderHousingList = (items, filter, params = {}) => { ) : cover ? div({ class: "tribe-card-image-wrapper" }, - a({ href: `/housing/${encodeURIComponent(item.id)}` }, + a({ href: itemHref }, img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-card-hero-image", alt: "" }) ) ) @@ -199,10 +197,9 @@ const renderHousingList = (items, filter, params = {}) => { div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/housing/${encodeURIComponent(item.id)}` }, safeText(item.title) || i18n.housingTitle) + a({ href: itemHref }, safeText(item.title) || i18n.housingTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, renderStarRating(item.opinions, safeArr(item.opinions_inhabitants).length), div({ class: "price-chip" }, priceLabel(item)), safeText(item.place) @@ -219,7 +216,8 @@ const renderHousingList = (items, filter, params = {}) => { span({ class: "card-label" }, `${i18n.galleryPhotos}: `), span({ class: "card-value" }, String(imagesOf(item).length)) ) - : null + : null, + null ) ) ) @@ -441,8 +439,7 @@ exports.singleHousingView = async (item, filter = "ALL", comments = [], params = requested ? renderRequestedChip() : null, renderLifespanChip(item.lifetime, i18n), renderEcoTax(item.msgSize, item.id), - renderReachChip(isClearnet, i18n, isClearnet ? housingClearnetHref(item) : null), - isAuthor && canClearnet ? renderClearnetSwitch("housing", item.rootId || item.id, isClearnet) : null + renderReachChip(isClearnet, i18n, isClearnet ? housingClearnetHref(item) : null) ].filter(Boolean) const sideActions = [] @@ -469,15 +466,14 @@ exports.singleHousingView = async (item, filter = "ALL", comments = [], params = const cover = imagesOf(item)[0] const housingSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(item.id, `/housing/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("housing", item.id) : null }) - ), + renderCardTop(item.id, `/housing/${encodeURIComponent(item.id)}`, { arrow: false, date: item.createdAt, chips, bylineExtra: [renderEditedChip(item.id, item.rootId)], spread: params.spreads || null, author: item.author, favKind: 'housing', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("housing", item.id) : null }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(item.title) || i18n.housingTitle) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + isAuthor && canClearnet ? renderClearnetSwitch("housing", item.rootId || item.id, isClearnet) : null, renderStarRating(item.opinions, voters.length), cover && !isVideoEntry(cover) ? img({ src: blobUrl(blobIdOf(cover), 256), class: "tribe-detail-image", alt: "" }) : null, + renderDetailShare(item.id, `/housing/${encodeURIComponent(item.id)}`, item.title), renderInfoTable(item), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.housingRequests}: ${item.requestCount || 0}`) @@ -506,8 +502,7 @@ exports.singleHousingView = async (item, filter = "ALL", comments = [], params = !isAuthor && item.everRequestedByViewer ? renderOpinionsVoting("/housing/opinions", item.id, item.opinions, returnTo, voters) : null, - renderCommentsSection(item.id, returnTo, comments) - ) + renderCommentsSection(item.id, returnTo, comments)) ) return template( diff --git a/src/views/image_view.js b/src/views/image_view.js index 9547b7f..4befcbf 100644 --- a/src/views/image_view.js +++ b/src/views/image_view.js @@ -4,11 +4,13 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } const moment = require("../server/node_modules/moment"); const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderStateChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderEcoTax, renderLifespanChip, renderCardTop, renderCardByline, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); -const { renderZoomableImage } = require("./gallery_view"); +const { renderZoomableImage, renderDownloadButton, downloadHref } = require("./gallery_view"); +const LIST_THUMB = 640; +const thumbOf = (url) => String(url || "").startsWith("&") ? `/image/${LIST_THUMB}/${encodeURIComponent(url)}?fit=inside` : null; const userId = config.keys.id; @@ -41,7 +43,7 @@ const renderImageMedia = (imgObj, filter, params = {}) => { return imgObj?.url ? div( { class: "image-container image-container-row" }, - renderZoomableImage(src, { alt: imgObj.title || "", imgClass: "post-image" }) + renderZoomableImage(src, { alt: imgObj.title || "", imgClass: "post-image", thumb: thumbOf(imgObj.url) }) ) : p(i18n.imageNoFile); }; @@ -49,9 +51,8 @@ const renderImageMedia = (imgObj, filter, params = {}) => { const renderImageOwnerActions = (filter, imgObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(imgObj.author) === String(userId); - const hasOpinions = Object.keys(imgObj.opinions || {}).length > 0; - if (!isAuthor || hasOpinions) return []; + if (!isAuthor) return []; return [ form( @@ -62,61 +63,52 @@ const renderImageOwnerActions = (filter, imgObj, params = {}) => { ]; }; +const renderImageUpdated = (imgObj) => { + const createdTs = imgObj.createdAt ? new Date(imgObj.createdAt).getTime() : NaN; + const updatedTs = imgObj.updatedAt ? new Date(imgObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return showUpdated + ? span({ class: "votations-comment-date" }, `${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; +}; + const renderImageList = exports.renderImageList = (images, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); return images.length ? images.map((imgObj) => { const commentCount = typeof imgObj.commentCount === "number" ? imgObj.commentCount : 0; const title = safeText(imgObj.title); + const href = `/images/${encodeURIComponent(imgObj.key)}`; const isOwn = imgObj.author && String(imgObj.author) === String(userId); - const headerActions = typeof params.headerActions === "function" - ? params.headerActions(imgObj) + const byline = { + author: imgObj.author, + date: imgObj.createdAt, + chips: [ + imgObj.tribeOrigin ? renderTribeOriginChip(imgObj.tribeOrigin) : imgObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('images', imgObj.title, imgObj.key)) : null, + renderLicenseChip(imgObj.license), + ...(typeof params.titleChips === "function" ? params.titleChips(imgObj) : []), + renderLifespanChip(imgObj.lifetime, i18n) + ], + bylineExtra: [renderImageUpdated(imgObj)] + }; + const header = typeof params.headerActions === "function" + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), params.headerActions(imgObj)) : imgObj.tribeOrigin - ? renderContentActions(null, imgObj.tribeOrigin.href) - : renderContentActions(imgObj.key, `/images/${encodeURIComponent(imgObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, author: imgObj.author, favKind: 'images', torrentFrom: { blobId: imgObj.url, name: imgObj.title }, isFavorite: imgObj.isFavorite, reportTitle: imgObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction("image", imgObj.key) : null }); - const titleHref = typeof params.headerActions === "function" ? null : imgObj.tribeOrigin ? imgObj.tribeOrigin.href : `/images/${encodeURIComponent(imgObj.key)}`; - const engagement = typeof params.engagement === "function" - ? params.engagement(imgObj) - : imgObj.tribeOrigin ? null : renderEngagement(imgObj.key, - renderOpinionsVoting('/images/opinions', imgObj.key, imgObj.opinions, returnTo, imgObj.opinions_inhabitants), - renderCommentsLink({ href: `/images/${encodeURIComponent(imgObj.key)}`, count: commentCount }) - ); + ? renderCardTop(imgObj.key, imgObj.tribeOrigin.href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || null, reportTitle: imgObj.title }) + : renderCardTop(imgObj.key, href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(imgObj.key)) || params.spreads || null, favKind: 'images', torrentFrom: { blobId: imgObj.url, name: imgObj.title }, isFavorite: imgObj.isFavorite, reportTitle: imgObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction("image", imgObj.key) : null }); + const titleHref = typeof params.headerActions === "function" ? null : imgObj.tribeOrigin ? imgObj.tribeOrigin.href : href; + const engagement = typeof params.engagement === "function" ? params.engagement(imgObj) : null; return div( { class: "trending-card image-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + header, div( { class: "card-section image-card-body" }, - div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, imgObj.tribeOrigin ? renderTribeOriginChip(imgObj.tribeOrigin) : imgObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('images', imgObj.title, imgObj.key)) : null, renderLicenseChip(imgObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(imgObj) : [])), - imgObj.lifetime ? div({ class: "card-chips-row" }, - imgObj.lifetime ? renderLifespanChip(imgObj.lifetime, i18n) : null - ) : null, + title ? div({ class: "shop-title-row" }, h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title)) : null, renderImageMedia(imgObj, filter, params), ...(typeof params.bodyExtra === "function" ? params.bodyExtra(imgObj) : []), - engagement, renderMapLocationVisitLabel(imgObj.mapUrl), - br(), - (() => { - const createdTs = imgObj.createdAt ? new Date(imgObj.createdAt).getTime() : NaN; - const updatedTs = imgObj.updatedAt ? new Date(imgObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imgObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(imgObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imgObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })() + engagement ) ); }) @@ -140,7 +132,7 @@ const renderImageForm = (filter, imageId, imageToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), label(i18n.imageFileLabel), br(), - input({ type: "file", name: "image", required: filter !== "edit" }), + (filter === "edit" && renderMediaLockNote(imageToEdit)) || input({ type: "file", name: "image", accept: "image/*", required: filter !== "edit" }), br(), br(), imageToEdit?.url @@ -187,12 +179,18 @@ const renderGallery = (images) => { }; const renderLightbox = (images) => - images.map((imgObj) => { + images.map((imgObj, i) => { const src = imgObj.url ? blobUrlOf(imgObj.url) : ""; + const idOf = (o) => `img-${encodeURIComponent(o.key)}`; + const prev = images[i - 1]; + const next = images[i + 1]; return div( - { id: `img-${encodeURIComponent(imgObj.key)}`, class: "lightbox" }, - a({ href: `#img-${encodeURIComponent(imgObj.key)}-src`, class: "lightbox-close" }, "ร—"), - img({ src, class: "lightbox-image", alt: imgObj.title || "" }) + { id: idOf(imgObj), class: "lightbox" }, + src ? a({ href: downloadHref(src), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "โค“") : null, + a({ href: `#${idOf(imgObj)}-src`, class: "lightbox-close" }, "ร—"), + prev ? a({ href: `#${idOf(prev)}`, class: "lightbox-prev", title: "โ†" }, "โ€น") : null, + next ? a({ href: `#${idOf(next)}`, class: "lightbox-next", title: "โ†’" }, "โ€บ") : null, + img({ src, class: "lightbox-image", alt: imgObj.title || "", loading: "lazy" }) ); }); @@ -315,27 +313,30 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(imageObj.tags); + const href = `/images/${encodeURIComponent(imageObj.key)}`; - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(imageObj.key, `/images/${encodeURIComponent(imageObj.key)}`, { + const imageSide = div({ class: "tribe-side" }, + renderCardTop(imageObj.key, href, { arrow: false, author: imageObj.author, + date: imageObj.createdAt, + chips: [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('images', imageObj.title, imageObj.key) : null), + renderLicenseChip(imageObj.license), + ...chips + ], + bylineExtra: [renderEditedChip(imageObj.key, imageObj.rootId), renderImageUpdated(imageObj)], deleteAction: String(imageObj.author) === String(userId) ? contentDeleteAction("image", imageObj.key) : null, favKind: 'images', torrentFrom: { blobId: imageObj.url, name: imageObj.title }, isFavorite: imageObj.isFavorite, spread: params.spreads || null, returnTo, reportTitle: imageObj.title - }) - ); - - const imageSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('images', imageObj.title, imageObj.key) : null), - isAuthor ? renderClearnetSwitch('images', imageObj.rootId || imageObj.key, isClearnet) : null, - renderLicenseChip(imageObj.license) + isAuthor ? renderClearnetSwitch('images', imageObj.rootId || imageObj.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(imageObj.key, href, imageObj.title), safeText(imageObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(imageObj.description)) : null, @@ -345,39 +346,16 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params ); const imageMain = div({ class: "tribe-main" }, - detailActions, imageObj?.url ? div( { class: "image-container" }, - img({ - src: blobUrlOf(imageObj.url), - alt: imageObj.title || "", - class: "media-preview", - loading: "lazy" - }) + renderZoomableImage(blobUrlOf(imageObj.url), { alt: imageObj.title || "", imgClass: "media-preview", full: true }), + renderDownloadButton(blobUrlOf(imageObj.url), imageObj.title || "") ) : p(i18n.imageNoFile), - (() => { - const createdTs = imageObj.createdAt ? new Date(imageObj.createdAt).getTime() : NaN; - const updatedTs = imageObj.updatedAt ? new Date(imageObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(imageObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(imageObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.imageUpdatedAt}: ${moment(imageObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), renderEngagement(imageObj.key, renderOpinionsVoting('/images/opinions', imageObj.key, imageObj.opinions, returnTo, imageObj.opinions_inhabitants), - renderImageCommentsSection(imageObj.key, comments, returnTo) - ) + renderImageCommentsSection(imageObj.key, comments, returnTo)) ); return template( @@ -392,9 +370,10 @@ exports.singleImageView = async (imageObj, filter = "all", comments = [], params form( { method: "GET", action: "/images", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterAll).toUpperCase()), ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterRecent).toUpperCase())] : []), ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.imageFilterFavorites).toUpperCase() )] : []), diff --git a/src/views/industry_view.js b/src/views/industry_view.js index 13c7a65..21a276d 100644 --- a/src/views/industry_view.js +++ b/src/views/industry_view.js @@ -1,6 +1,7 @@ -const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, img, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe") +const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, ul, li, video, audio, table, thead, tbody, tr, td, th } = require("../server/node_modules/hyperaxe") const { renderZoomableImage } = require("./gallery_view") -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderContentActions , renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, renderCardMetaRow, moduleIsEmpty, paged } = require("./main_views") +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderCardTop, renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") +const { renderCommentsSection } = require("./comments_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderMapEmbedWithZoom } = require("./maps_view") @@ -124,28 +125,20 @@ const renderFacilityList = exports.renderFacilityList = (facilities, filter, spr : (fc.subscriptionIn === false ? renderStateChip("closed", "โœ‰", i18n.subscriptionOff) : null) ].filter(Boolean) return div({ class: "trending-card tribes-card industry-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(fc.id || fc.key, href, { spread: spreadMap.get(fc.id || fc.key) || null, author: fc.steward, reportTitle: fc.name, deleteAction: isOwn && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) - ), + renderCardTop(fc.id || fc.key, href, { date: fc.createdAt, chips, spread: spreadMap.get(fc.id || fc.key) || null, author: fc.steward, reportTitle: fc.name, deleteAction: isOwn && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }), div({ class: "card-section tribes-card-body" }, div({ class: "tribe-card-image-wrapper" }, - a({ href }, renderMediaBlob(fc.image, { class: "tribe-card-hero-image" }) || div({ class: "industry-card-noimage" }, span("๐Ÿญ"))), - isMember - ? form({ method: "GET", action: href, class: "tribe-visit-btn-wrapper" }, - button({ type: "submit", class: "filter-btn" }, String(i18n.industryVisitButton || "VISIT").toUpperCase()) - ) - : null + a({ href }, renderMediaBlob(fc.image, { class: "tribe-card-hero-image" }) || div({ class: "industry-card-noimage" }, span("๐Ÿญ"))) ), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, safeText(fc.name) || (i18n.industryTitle || "Industry"))) ), - div({ class: "card-chips-row" }, ...chips), fc.description ? p({ class: "tribe-card-description" }, safeText(fc.description).slice(0, 220)) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.industryMembers || "Members"}: ${memberCount}`) ), + null ) ) ) @@ -159,6 +152,7 @@ const renderFacilityForm = (facility, mode, spreadWarning = null) => { const returnTo = "/industry?filter=MINE" const curPolicy = fc.membershipPolicy || "vote" const curSector = fc.sector || "other" + const mediaLock = isEdit ? renderMediaLockNote(fc) : null return div({ class: "div-center industry-form" }, isEdit ? spreadWarning : null, form({ @@ -177,11 +171,7 @@ const renderFacilityForm = (facility, mode, spreadWarning = null) => { br(), label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), br(), - input({ type: "file", name: "image" }), - br(), - label(i18n.uploadMedia), - br(), - input({ type: "file", name: "blob" }), + ...(mediaLock ? [mediaLock] : [input({ type: "file", name: "image" }), br(), label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" })]), fc.image ? div({ class: "industry-form-media" }, renderMediaBlob(fc.image, { class: "industry-hero-image" })) : null, br(), label(i18n.industrySector || "Sector"), @@ -226,12 +216,15 @@ const renderGlobalBlueprints = (blueprints, spreadMap = new Map()) => { if (!list.length) return p(i18n.industryNoBlueprints || "No blueprints yet.") return div({ class: "industry-blueprints" }, list.map((bp) => div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, "/industry/blueprint/" + encodeURIComponent(bp.id), { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + renderCardTop(bp.id, "/industry/blueprint/" + encodeURIComponent(bp.id), { + date: bp.createdAt, + chips: [ + renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) + ], + spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name + }), div({ class: "industry-blueprint-card" + (bp.author === userId ? " own-content" : "") }, - div({ class: "card-chips-row" }, - renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) - ), p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(bp.facilityId)}` }, safeText(bp.facilityName))), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/blueprint/${encodeURIComponent(bp.id)}` }, safeText(bp.name)))), bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, @@ -243,7 +236,8 @@ const renderGlobalBlueprints = (blueprints, spreadMap = new Map()) => { (() => { const acts = renderBlueprintGovernActions(bp.facilityId, bp, safeArr(bp.members).includes(userId) || bp.author === userId, "/industry?filter=BLUEPRINTS") return acts.length ? div({ class: "industry-actions" }, ...acts) : null - })() + })(), + null ) )) ) @@ -278,16 +272,17 @@ const renderBuildGovernActions = (b, isMember, returnTo) => { const renderBuildCard = (b, opts = {}) => { const href = `/industry/build/${encodeURIComponent(b.id)}` + const chips = [ + renderBuildStatusChip(b.status), + ...((b.blueprintKind && opts.withBlueprintChips !== false) ? [ + renderStateChip("half", b.blueprintKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + b.blueprintKind] || b.blueprintKind), + renderStateChip("half", "โš–", String(b.blueprintLicense || "copyleft").toUpperCase()) + ] : []) + ] const card = div({ class: "industry-blueprint-card" + (b.proposer === userId ? " own-content" : "") }, - div({ class: "card-chips-row" }, - renderBuildStatusChip(b.status), - ...((b.blueprintKind && opts.withBlueprintChips !== false) ? [ - renderStateChip("half", b.blueprintKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + b.blueprintKind] || b.blueprintKind), - renderStateChip("half", "โš–", String(b.blueprintLicense || "copyleft").toUpperCase()) - ] : []) - ), + opts.withActions ? null : div({ class: "card-chips-row" }, ...chips), opts.withFacility ? p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: `/industry/${encodeURIComponent(b.facilityId)}` }, safeText(b.facilityName))) : null, - opts.withProposer !== false ? p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)) : null, + opts.withProposer !== false && !opts.withActions ? p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)) : null, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, safeText(b.title)))), b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, b.notes ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(b.notes))) : null, @@ -297,11 +292,12 @@ const renderBuildCard = (b, opts = {}) => { ), renderBlueprintEstimate(b, i18n.industryFinalPrice || "Final price"), p(span({ class: "industry-meta-label" }, `${i18n.industryContributions || "Contributions"}: `), String(safeArr(b.contributions).length)), - (opts.actions && opts.actions.length) ? div({ class: "industry-actions" }, ...opts.actions) : null + (opts.actions && opts.actions.length) ? div({ class: "industry-actions" }, ...opts.actions) : null, + null ) if (!opts.withActions) return card return div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(b.id, href, { spread: opts.spread || null, author: b.proposer, reportTitle: b.title })), + renderCardTop(b.id, href, { date: b.createdAt, chips, spread: opts.spread || null, author: b.proposer, reportTitle: b.title }), card ) } @@ -527,12 +523,15 @@ const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map() const cards = list.map((bp) => { const actions = renderBlueprintGovernActions(fc.id, bp, isMember, `/industry/${encodeURIComponent(fc.id)}`) return div({ class: "industry-card-wrap" }, - div({ class: "card-header activity-card-header" }, span(), renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name })), + renderCardTop(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { + date: bp.createdAt, + chips: [ + renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) + ], + spread: spreadMap.get(bp.id) || null, author: bp.author, reportTitle: bp.name + }), div({ class: "industry-blueprint-card" }, - div({ class: "card-chips-row" }, - renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) - ), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(bp.name))), bp.image ? renderMediaBlob(bp.image, { class: "post-image" }) : null, bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : (bp.outItem ? p(span({ class: "industry-meta-label" }, `${i18n.industryOutput || "Output"}: `), `${bp.outQty || 0} ร— ${safeText(bp.outItem)}`) : null), @@ -540,7 +539,8 @@ const renderBlueprintsSection = (fc, blueprints, isMember, spreadMap = new Map() safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} ร— ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), safeArr(bp.skills).length ? p(span({ class: "industry-meta-label" }, `${i18n.industrySkills || "Skills"}: `), bp.skills.join(", ")) : null, - actions.length ? div({ class: "industry-actions" }, ...actions) : null + actions.length ? div({ class: "industry-actions" }, ...actions) : null, + null ) ) }) @@ -564,7 +564,7 @@ const renderBlueprintForm = (fc, bp, mode, spreadWarning = null) => { label(i18n.industryDescriptionLabel || "Description"), br(), textarea({ name: "description", rows: "4", maxlength: "2000", placeholder: i18n.industryBlueprintDescPlaceholder || "Specs, dimensions, weight, docsโ€ฆ" }, bp.description || ""), br(), label(i18n.uploadMedia || "Upload media (max-size: 75MB)"), - br(), input({ type: "file", name: "image" }), + br(), (isEdit && renderMediaLockNote(bp)) || input({ type: "file", name: "image" }), bp.image ? div({ class: "industry-form-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, br(), label(i18n.industryOutputKind || "Product type"), @@ -629,13 +629,11 @@ const renderFacilityJobsSection = (fc, jobs) => { ) } -const renderFacilitySide = (fc, returnTo, params = {}) => { +const renderFacilityChips = (fc) => { const isSteward = fc.steward === userId const isMember = safeArr(fc.members).includes(userId) const isOwner = String(fc.steward || fc.author) === String(userId) - const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length - - const chips = [ + return [ renderStateChip("whole", "๐Ÿญ", sectorLabel(fc.sector)), renderStateChip("half", "โš–", policyLabel(fc.membershipPolicy)), renderLifespanChip(fc.lifetime, i18n), @@ -646,6 +644,13 @@ const renderFacilitySide = (fc, returnTo, params = {}) => { : renderStateChip("closed", "โœ‰", i18n.subscriptionOff)) : null ].filter(Boolean) +} + +const renderFacilitySide = (fc, returnTo, params = {}, cardTop = null) => { + const isSteward = fc.steward === userId + const isMember = safeArr(fc.members).includes(userId) + const isOwner = String(fc.steward || fc.author) === String(userId) + const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length const sideActions = [] for (const act of membershipActions(fc, returnTo)) sideActions.push(act) @@ -659,15 +664,15 @@ const renderFacilitySide = (fc, returnTo, params = {}) => { } return div({ class: "tribe-side" }, + cardTop, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: `/industry/${encodeURIComponent(fc.id)}` }, safeText(fc.name) || (i18n.industryTitle || "Industry")))), - div({ class: "card-chips-row" }, ...chips), fc.image ? renderMediaBlob(fc.image, { class: "tribe-detail-image" }) : null, + renderDetailShare(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`, fc.name), fc.description ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(fc.description))) : null, table({ class: "tribe-info-table" }, tr(td({ class: "tribe-info-label" }, i18n.industryLaborRate || "Labor rate"), td({ class: "tribe-info-value", colspan: "3" }, `${fc.laborRate || 0} ECO/h`)), tr(td({ class: "tribe-info-label" }, i18n.industryQuorum || "Quorum"), td({ class: "tribe-info-value", colspan: "3" }, String(fc.quorum))), - tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)), - tr(td({ class: "tribe-info-value tribe-author-cell", colspan: "4" }, userLink(fc.steward))) + tr(td({ class: "tribe-info-label" }, i18n.industryMajority || "Majority"), td({ class: "tribe-info-value", colspan: "3" }, `${Math.round((fc.majority || 0.5) * 100)}%`)) ), fc.mapUrl ? renderMapEmbedWithZoom(params.mapData, fc.mapUrl, returnTo, params.zoom) : null, safeArr(fc.tags).length @@ -697,13 +702,15 @@ exports.singleFacilityView = async (facility, filter, params = {}) => { const returnTo = `/industry/${encodeURIComponent(fc.id)}?filter=${encodeURIComponent(f)}` const isMember = safeArr(fc.members).includes(userId) const memberCount = fc.memberCount != null ? fc.memberCount : safeArr(fc.members).length - const facilitySide = renderFacilitySide(fc, returnTo, params) + const facilitySide = renderFacilitySide(fc, returnTo, params, + renderCardTop(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`, { arrow: false, date: fc.createdAt, chips: renderFacilityChips(fc), bylineExtra: [renderEditedChip(fc.id, fc.root)], spread: params.spreads || null, author: fc.steward, reportTitle: fc.name, deleteAction: fc.steward === userId && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) + ) const facilityMain = div({ class: "tribe-main" }, renderBlueprintsSection(fc, params.blueprints, isMember, params.childSpreadMap || new Map()), renderBuildsSection(fc, params.builds, params.blueprints, isMember, params.childSpreadMap || new Map()), renderFacilityJobsSection(fc, params.facilityJobs), - renderCardMetaRow(renderOpinionsVoting('/industry/opinions', fc.id || fc.key, fc.opinions, null, fc.opinions_inhabitants)), + renderEngagement(fc.id || fc.key, renderOpinionsVoting('/industry/opinions', fc.id || fc.key, fc.opinions, null, fc.opinions_inhabitants), null), div({ class: "industry-section" }, h2(i18n.industryMembers || "Members"), ul({ class: "industry-members-list" }, safeArr(fc.members).map((mid) => li( @@ -717,9 +724,6 @@ exports.singleFacilityView = async (facility, filter, params = {}) => { return template( i18n.industryTitle || "Industry", section( - div({ class: "card-header activity-card-header" }, - renderContentActions(fc.id || fc.key, `/industry/${encodeURIComponent(fc.id)}`, { spread: params.spreads || null, author: fc.steward, reportTitle: fc.name, deleteAction: fc.steward === userId && memberCount <= 1 ? `/industry/delete/${encodeURIComponent(fc.id)}` : null }) - ), div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), div({ class: "filters" }, form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, @@ -735,9 +739,7 @@ exports.singleFacilityView = async (facility, filter, params = {}) => { exports.blueprintEditView = async (bp, fc) => template( i18n.industryTitle || "Industry", section( - div({ class: "card-header activity-card-header" }, - renderContentActions(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { author: bp.author, reportTitle: bp.name }) - ), + renderCardTop(bp.id, `/industry/blueprint/${encodeURIComponent(bp.id)}`, { date: bp.createdAt, author: bp.author, reportTitle: bp.name }), div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), div({ class: "filters" }, form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, @@ -752,9 +754,7 @@ exports.blueprintEditView = async (bp, fc) => template( exports.buildEditView = async (b, fc) => template( i18n.industryTitle || "Industry", section( - div({ class: "card-header activity-card-header" }, - renderContentActions(b.id, `/industry/build/${encodeURIComponent(b.id)}`, { author: b.proposer, reportTitle: b.title }) - ), + renderCardTop(b.id, `/industry/build/${encodeURIComponent(b.id)}`, { date: b.createdAt, author: b.proposer, reportTitle: b.title }), div({ class: "tags-header module-header-line" }, h2(i18n.industryTitle || "Industry"), p(i18n.industryDescription || "Network-owned production facilities.")), div({ class: "filters" }, form({ method: "GET", action: "/industry", class: "ui-toolbar ui-toolbar--filters" }, @@ -786,21 +786,20 @@ exports.singleBlueprintView = async (blueprint, params = {}) => { ), section( div({ class: "shop-detail" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(bp.id, returnTo, { spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title }) - ), - div({ class: "card-chips-row" }, - renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), - renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) - ), + renderCardTop(bp.id, returnTo, { arrow: false, + date: bp.createdAt, + chips: [ + renderStateChip("half", bp.outKind === "digital" ? "๐Ÿ’พ" : "๐Ÿ“ฆ", i18n["industryKind_" + bp.outKind] || bp.outKind), + renderStateChip("half", "โš–", String(bp.license || "copyleft").toUpperCase()) + ], + bylineExtra: [renderEditedChip(bp.id, bp.rootId)], + spread: params.spreads || null, author: bp.author, reportTitle: bp.name || bp.title + }), p({ class: "industry-facility-line" }, span({ class: "industry-meta-label" }, `${i18n.industryFacility || "Facility"}: `), a({ href: facilityHref }, safeText(bp.facilityName))), h2(safeText(bp.name)), bp.image ? div({ class: "shop-detail-media" }, renderMediaBlob(bp.image, { class: "post-image" })) : null, + renderDetailShare(bp.rootId || bp.id, returnTo, bp.name || bp.title), bp.description ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(bp.description))) : null, - div({ class: "industry-meta" }, - p(span({ class: "industry-meta-label" }, `${i18n.industryAuthor || "Author"}: `), userLink(bp.author)) - ), renderBlueprintLaborHours(bp), safeArr(bp.materials).length ? div({ class: "industry-materials" }, ul({ class: "industry-materials-list" }, bp.materials.map(m => li(`${m.qty || 0} ร— ${safeText(m.item)}${m.price != null ? ` = ${m.price} ECO` : ""}`)))) : null, renderBlueprintEstimate(bp, i18n.industryBuildingPrice || "Building price"), @@ -815,6 +814,10 @@ exports.singleBlueprintView = async (blueprint, params = {}) => { ))) ) : null + ), + renderEngagement(bp.rootId || bp.id, + params.opinions ? renderOpinionsVoting('/industry/blueprint/opinions', bp.rootId || bp.id, params.opinions.opinions, `/industry/blueprint/${encodeURIComponent(bp.rootId || bp.id)}`, params.opinions.opinions_inhabitants) : null, + Array.isArray(params.comments) ? renderCommentsSection({ action: `/industry/blueprint/${encodeURIComponent(bp.rootId || bp.id)}/comments`, comments: params.comments, returnTo: `/industry/blueprint/${encodeURIComponent(bp.rootId || bp.id)}` }) : null ) ) ) @@ -971,17 +974,13 @@ exports.singleBuildView = async (build, params = {}) => { ), section( div({ class: "shop-detail" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(b.id, returnTo, { spread: params.spreads || null, author: b.proposer, reportTitle: b.title }) - ), - b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null, + renderCardTop(b.id, returnTo, { arrow: false, date: b.createdAt, chips: [renderBuildStatusChip(status)], spread: params.spreads || null, author: b.proposer, reportTitle: b.title }), + b.image ? div({ class: "shop-detail-media" }, renderMediaBlob(b.image, { class: "post-image" })) : null, + renderDetailShare(b.id, returnTo, b.title), h2(safeText(b.title) || (i18n.industryBuild || "Build")), - div({ class: "card-chips-row" }, renderBuildStatusChip(status)), div({ class: "industry-meta" }, b.blueprintName ? p(span({ class: "industry-meta-label" }, `${i18n.industryBlueprint || "Blueprint"}: `), safeText(b.blueprintName)) : null, b.blueprintImage ? renderMediaBlob(b.blueprintImage, { class: "post-image" }) : null, - p(span({ class: "industry-meta-label" }, `${i18n.industryProposer || "Proposer"}: `), userLink(b.proposer)), b.notes ? p({ class: "industry-build-notes" }, span({ class: "industry-meta-label" }, `${i18n.industryBuildNotes || "Notes"}: `), safeText(b.notes)) : null, div({ class: "industry-dates-row" }, b.startDate ? p(span({ class: "industry-meta-label" }, `${i18n.industryBuildStart || "Start date"}: `), moment(b.startDate).format("YYYY/MM/DD")) : null, diff --git a/src/views/inhabitants_view.js b/src/views/inhabitants_view.js index 399059e..049ede1 100644 --- a/src/views/inhabitants_view.js +++ b/src/views/inhabitants_view.js @@ -57,7 +57,7 @@ function resolvePhoto(photoField, size = 256) { } const filterLabel = (mode) => - String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || mode).toUpperCase(); + String(i18n[mode + 'Button'] || i18n[mode + 'SectionTitle'] || i18n[String(mode).toLowerCase() + 'Button'] || i18n[String(mode).toLowerCase() + 'SectionTitle'] || mode).toUpperCase(); const generateFilterButtons = (filters, currentFilter, labelOf = filterLabel) => filters.map(mode => diff --git a/src/views/jobs_view.js b/src/views/jobs_view.js index 9d89c07..90bddb9 100644 --- a/src/views/jobs_view.js +++ b/src/views/jobs_view.js @@ -1,10 +1,11 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") const { renderZoomableImage } = require("./gallery_view") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderCardTop, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderOpinionsVoting, renderEditedChip, renderMediaLockNote, renderEngagement, renderDetailShare } = require("./main_views") +const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderStyledText } = require("../backend/renderStyledText") -const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") +const { renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") const { resolvePhoto } = require("./inhabitants_view") const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") @@ -112,7 +113,7 @@ const renderJobOwnerActions = (job, returnTo) => { form( { method: "POST", action: `/jobs/status/${encodeURIComponent(job.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "status-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" }, isOpen ? i18n.jobSetClosed : i18n.jobSetOpen) + button({ class: isOpen ? "update-btn danger-btn" : "update-btn", type: "submit", name: "status", value: isOpen ? "CLOSED" : "OPEN" }, isOpen ? i18n.jobSetClosed : i18n.jobSetOpen) ), form( { method: "GET", action: `/jobs/edit/${encodeURIComponent(job.id)}` }, @@ -177,7 +178,7 @@ const renderJobInfoTable = (job, opts = {}) => { return table({ class: "tribe-info-table jobs-info-table" }, ...rows) } -const renderJobExtraDetails = (job) => { +const renderJobExtraDetails = (job, opts = {}) => { const langLabel = String(job.languages || "").toUpperCase() const isExchange = String(job.job_type || "").toLowerCase() === "exchange" const skill = safeText(job.exchangeSkill) @@ -194,9 +195,24 @@ const renderJobExtraDetails = (job) => { pushRow(i18n.jobsHoursRequested || "Hours requested", `${Number(job.hoursRequested) || 0} h`) if (skill) pushRow(i18n.jobsExchangeSkill || "Skill", skill) } - return table({ class: "tribe-info-table jobs-info-table" }, ...rows, ...(opts.includeCreated ? renderCreatedRows(job.createdAt, job.author, job.updatedAt) : [])) + return table({ class: "tribe-info-table jobs-info-table" }, ...rows) } +const jobChipFor = (filter, censusJ, params = {}) => (x) => { + const m = x.key; + if (m === filter) return true; + if (m === "TOP") return censusJ.length > 0; + if (m === "MINE") return censusJ.some(j => String(j.author) === String(userId)); + if (m === "RECENT") return censusJ.length > 0; + if (m === "APPLIED") return censusJ.some(j => safeArr(j.subscribers).includes(userId)); + if (m === "REMOTE") return censusJ.some(j => String(j.location || "").toLowerCase() === "remote"); + if (m === "PRESENCIAL") return censusJ.some(j => String(j.location || "").toLowerCase() === "presencial"); + if (m === "FREELANCER" || m === "EMPLOYEE" || m === "EXCHANGE") return censusJ.some(j => String(j.job_type || "").toUpperCase() === m); + if (m === "OPEN" || m === "CLOSED") return censusJ.some(j => String(j.status || "OPEN").toUpperCase() === m); + if (m === "CV") return params.anyCVs === true; + return true; +}; + const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { const list = safeArr(jobs) @@ -229,25 +245,22 @@ const renderJobList = exports.renderJobList = (jobs, filter, params = {}) => { : `${fmtSalary(job.salary)} ECO` const isOwn = job.author && String(job.author) === String(userId) + const jobHref = `/jobs/${encodeURIComponent(job.id)}` return div({ class: "trending-card job-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isOwn ? contentDeleteAction("job", job.id) : null }) - ), + renderCardTop(job.id, jobHref, { date: job.createdAt, chips, spread: params.spreadMap && params.spreadMap.get(job.id) || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isOwn ? contentDeleteAction("job", job.id) : null }), div({ class: "card-section job-card-body" }, heroNode, div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/jobs/${encodeURIComponent(job.id)}` }, safeText(job.title) || i18n.jobsTitle) + a({ href: jobHref }, safeText(job.title) || i18n.jobsTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, div({ class: "price-chip" }, compensationText), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) - ) + ), + null ) ) ) @@ -307,7 +320,7 @@ const renderJobForm = (job = {}, mode = "create", spreadWarning = null, params = br(), label(i18n.jobImage), br(), - input({ type: "file", name: "image" }), + (isEdit && renderMediaLockNote(job)) || input({ type: "file", name: "image" }), br(), job.image ? renderMediaBlob(job.image) : null, br(), @@ -460,20 +473,7 @@ exports.jobsView = async (jobsOrCVs, filter = "ALL", params = {}) => { const emptyMod = moduleIsEmpty(Array.isArray(jobsOrCVs) ? jobsOrCVs : [], filter, "ALL", search || String(minSalary || "") || String(maxSalary || "")); const censusJ = Array.isArray(params.censusList) ? params.censusList : (Array.isArray(jobsOrCVs) ? jobsOrCVs : []); - const jobChip = (x) => { - const m = x.key; - if (m === filter) return true; - if (m === "TOP") return censusJ.length > 0; - if (m === "MINE") return censusJ.some(j => String(j.author) === String(userId)); - if (m === "RECENT") return censusJ.length > 0; - if (m === "APPLIED") return censusJ.some(j => safeArr(j.subscribers).includes(userId)); - if (m === "REMOTE") return censusJ.some(j => String(j.location || "").toLowerCase() === "remote"); - if (m === "PRESENCIAL") return censusJ.some(j => String(j.location || "").toLowerCase() === "presencial"); - if (m === "FREELANCER" || m === "EMPLOYEE" || m === "EXCHANGE") return censusJ.some(j => String(j.job_type || "").toUpperCase() === m); - if (m === "OPEN" || m === "CLOSED") return censusJ.some(j => String(j.status || "OPEN").toUpperCase() === m); - if (m === "CV") return params.anyCVs === true; - return true; - }; + const jobChip = jobChipFor(filter, censusJ, params); const filterObj = FILTERS.find((f) => f.key === filter) || FILTERS[0] const sectionTitle = i18n[filterObj.title] || i18n.jobsTitle @@ -632,9 +632,10 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) job.industry ? a({ href: `/industry/${encodeURIComponent(job.industry)}` }, renderStateChip("whole", "๐Ÿญ", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(job.lifetime, i18n), renderEcoTax(job.msgSize, job.id), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('jobs', job.title, job.id) : null), - isAuthor && canClearnet ? renderClearnetSwitch('jobs', job.rootId || job.id, isClearnet) : null + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('jobs', job.title, job.id) : null) ].filter(Boolean) + const updatedAt = job.updatedAt ? moment(job.updatedAt) : null + const showUpdated = updatedAt && updatedAt.isValid() && updatedAt.valueOf() !== new Date(job.createdAt).getTime() const nextVisibility = visibility === 'PUBLIC' ? 'HIDDEN' : 'PUBLIC' const visibilityRow = isAuthor @@ -655,17 +656,24 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) for (const a of ownerActions) sideActions.push(a) const jobSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(job.id, `/jobs/${encodeURIComponent(job.id)}`, { spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("job", job.id) : null }) - ), + renderCardTop(job.id, `/jobs/${encodeURIComponent(job.id)}`, { arrow: false, + date: job.createdAt, + chips, + bylineExtra: [ + renderEditedChip(job.id, job.rootId), + showUpdated ? span({ class: "card-byline-time" }, `${i18n.jobsUpdatedAt}: ${updatedAt.format("YYYY/MM/DD HH:mm")}`) : null + ], + spread: params.spreads || null, author: job.author, favKind: 'jobs', isFavorite: job.isFavorite, reportTitle: job.title, returnTo, deleteAction: isAuthor ? contentDeleteAction("job", job.id) : null + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, safeText(job.title) || i18n.jobsTitle) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + isAuthor && canClearnet ? renderClearnetSwitch('jobs', job.rootId || job.id, isClearnet) : null, job.image ? renderMediaBlob(job.image, { class: "tribe-detail-image" }) : null, - renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false, includeCreated: true }), + renderDetailShare(job.rootId || job.id, `/jobs/${encodeURIComponent(job.id)}`, job.title), + renderJobInfoTable(job, { includeVacants: true, includeSubscribers: false }), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.jobSubscribers}: ${subs.length}`) ), @@ -692,7 +700,9 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) renderJobSection(i18n.jobTasks, job.tasks), job.mapUrl ? div({ class: "job-section" }, renderMapEmbed(params.mapData, job.mapUrl)) : null, candidatesBlock ? candidatesBlock : null, - renderJobCommentsSection(job.id, returnTo, comments) + renderEngagement(job.rootId || job.id, + params.opinions ? renderOpinionsVoting('/jobs/opinions', job.rootId || job.id, params.opinions.opinions, `/jobs/${encodeURIComponent(job.rootId || job.id)}`, params.opinions.opinions_inhabitants) : null, + renderJobCommentsSection(job.id, returnTo, comments)) ) return template( @@ -707,7 +717,7 @@ exports.singleJobsView = async (job, filter = "ALL", comments = [], params = {}) input({ type: "hidden", name: "minSalary", value: String(params.minSalary ?? "") }), input({ type: "hidden", name: "maxSalary", value: String(params.maxSalary ?? "") }), input({ type: "hidden", name: "sort", value: safeText(params.sort || "recent") }), - ...FILTERS.map((f) => + ...FILTERS.filter(jobChipFor(filter, Array.isArray(params.censusList) ? params.censusList : [], params)).map((f) => button({ type: "submit", name: "filter", value: f.key, class: filter === f.key ? "filter-btn active" : "filter-btn" }, String(i18n[f.i18n]).toUpperCase()) ), button({ type: "submit", name: "filter", value: "CREATE", class: "create-button" }, i18n.jobsCreateJob) diff --git a/src/views/larp_view.js b/src/views/larp_view.js index fe69c1f..81b47f5 100644 --- a/src/views/larp_view.js +++ b/src/views/larp_view.js @@ -43,7 +43,7 @@ const renderHouseNav = (houses, currentKey) => div({ class: 'larp-house-nav' }, }, h.name)) ); -const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false }) => { +const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cyclesUntilRuling, canTakeTest = false, tribeReady = false }) => { const { renderEncryptedChip } = require('./clearnet_view'); const isInHouse = viewerHouseKey === house.key; const isInLarp = viewerHouseKey !== null && viewerHouseKey !== undefined; @@ -81,7 +81,7 @@ const renderHousePanel = (house, members, posts, { canPost, viewerHouseKey, cycl isAcademia && isInHouse && canTakeTest ? a({ href: '/larp/test', class: 'filter-btn larp-start-journey' }, i18n.larpStartJourney) : null, - isInHouse + isInHouse && tribeReady ? a({ href: `/larp/tribe/${encodeURIComponent(house.key)}`, class: 'filter-btn' }, i18n.larpVisitTribe || 'Visit Tribe') : null, canInvite @@ -301,7 +301,7 @@ const renderHousesGrid = (houses, myHouseKey, governingKey) => { ); }; -exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, q }) => { +exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, governingHouse, governingMembers, governingPosts, canPost, tribeReady, q }) => { const title = i18n.larpTitle || 'L.A.R.P.'; const description = i18n.larpDescription || 'A live action role-playing layer for collaborative experimentation.'; const myHouse = houses.find(h => h.key === myHouseKey) || null; @@ -326,7 +326,7 @@ exports.larpListView = ({ filter, houses, myHouseKey, cycle, governingKey, gover : mode === 'rules' ? renderRules() : [ - renderHousePanel(governingHouse, governingMembers, governingPosts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: 0 }), + renderHousePanel(governingHouse, governingMembers, governingPosts, { canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: 0, tribeReady }), renderPostsBlock(governingPosts, governingHouse, canPost) ] ) @@ -344,7 +344,7 @@ const computeCyclesUntilRuling = (house, governingKey, now = new Date()) => { return 0; }; -exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, houses, posts, canPost, testStatus, inviteCode, questions }) => { +exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, houses, posts, canPost, testStatus, inviteCode, questions, tribeReady }) => { const myHouse = houses.find(h => h.key === myHouseKey) || null; const governingHouse = houses.find(h => h.key === governingKey) || null; const isAcademia = house.key === 'academia'; @@ -369,6 +369,7 @@ exports.larpHouseView = ({ house, members, myHouseKey, cycle, governingKey, hous canPost, viewerHouseKey: myHouseKey, cyclesUntilRuling: cycles, + tribeReady, canTakeTest: isAcademia && viewerInAcademia && !!(testStatus && testStatus.allowed) }), inviteCode diff --git a/src/views/logistics_view.js b/src/views/logistics_view.js index 4153b26..445224d 100644 --- a/src/views/logistics_view.js +++ b/src/views/logistics_view.js @@ -1,5 +1,5 @@ const { hr, div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderStateChip, renderContentActions, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged } = require("./main_views"); +const { template, i18n, renderStateChip, renderCardTop, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views"); const { renderCommentsSection } = require("./comments_view"); const { renderMapEmbed } = require("./maps_view"); const { renderStyledText } = require("../backend/renderStyledText"); @@ -87,18 +87,15 @@ exports.logisticsChipFor = logisticsChipFor; const renderRouteCard = (route, params = {}) => div({ class: `tribe-card logistics-card logistics-${route.kind.toLowerCase()}` }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(route.id, routeHref(route), { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: (params.spreadMap && params.spreadMap.get(route.id)) || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) - ), + renderCardTop(route.id, routeHref(route), { date: route.createdAt, chips: [kindChip(route), modeChip(route), statusChip(route), priceChip(route)], author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: (params.spreadMap && params.spreadMap.get(route.id)) || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }), div({ class: "tribe-card-body" }, renderCover(route), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: routeHref(route) }, route.title || "โ€”"))), - div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), routeLine(route), whenLine(route), capacityLine(route), - countersBlock(route) + countersBlock(route), + null ) ); @@ -166,7 +163,7 @@ const renderForm = (route, params = {}) => { label(i18n.logisticsDescriptionLabel), br(), textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.logisticsDescriptionPlaceholder }, route ? route.description : ""), br(), label(i18n.uploadMedia), br(), - input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), + (route && renderMediaLockNote(route)) || input({ type: "file", name: "blob", accept: "image/*,video/*,audio/*,application/pdf,.torrent" }), br(), br(), label(i18n.logisticsOriginLabel), br(), input({ type: "text", name: "origin", maxlength: "100", required: true, placeholder: i18n.logisticsOriginPlaceholder, value: route ? route.origin : "" }), br(), label(i18n.logisticsDestinationLabel), br(), @@ -251,8 +248,8 @@ const bookingStatusForm = (route, b, status, cls, text) => const renderBooking = (route, b) => div({ class: "logistics-booking" }, + renderCardTop(null, null, { author: b.booker, date: b.createdAt, chips: [bookingStatusChip(b), route.kind === "TRIP" ? renderStateChip("neutral", "๐Ÿ’บ", String(b.seats)) : null] }), div({ class: "emergency-update-head logistics-booking-head" }, - div({ class: "card-chips-row" }, bookingStatusChip(b), route.kind === "TRIP" ? renderStateChip("neutral", "๐Ÿ’บ", String(b.seats)) : null), div({ class: "tribe-side-actions emergency-update-actions" }, route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "CONFIRMED", "tribe-action-btn", i18n.logisticsConfirm) : null, route.isOwner && b.status === "PENDING" ? bookingStatusForm(route, b, "REJECTED", "delete-btn", i18n.logisticsReject) : null, @@ -269,8 +266,7 @@ const renderBooking = (route, b) => input({ type: "text", name: "receipt", maxlength: "200", placeholder: i18n.logisticsReceiptPlaceholder }), button({ type: "submit", class: "tribe-action-btn" }, String(i18n.logisticsDeliver).toUpperCase()) ) - : null, - p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(b.createdAt)}`), userLink(b.booker)) + : null ); const renderBookForm = (route) => @@ -289,9 +285,8 @@ const renderRatings = (route) => h3(i18n.logisticsRatingsTitle), route.ratings.length ? div({ class: "logistics-ratings-list" }, ...route.ratings.map(r => div({ class: "logistics-rating" }, - div({ class: "emergency-update-head logistics-rating-head" }, div({ class: "card-chips-row" }, renderStateChip("neutral", "", stars(r.score)))), - r.text ? p(r.text) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(r.createdAt)}`), userLink(r.author)) + renderCardTop(null, null, { author: r.author, date: r.createdAt, chips: [renderStateChip("neutral", "", stars(r.score))] }), + r.text ? p(r.text) : null ))) : p(route.closed ? i18n.logisticsNoRatings : i18n.logisticsRatingsWhenClosed), route.canRate @@ -317,16 +312,10 @@ exports.singleLogisticsView = async (route, params = {}) => { ? (route.seats ? [{ label: i18n.logisticsSeatsLabel, value: `${route.seatsLeft} / ${route.seats}` }] : []) : [route.size ? { label: i18n.logisticsSizeLabel, value: route.size } : null, route.weight ? { label: i18n.logisticsWeightLabel, value: route.weight } : null].filter(Boolean); const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(route.id, href, { author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: params.spread || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }) - ), + renderCardTop(route.id, href, { arrow: false, date: route.createdAt, chips: [kindChip(route), modeChip(route), statusChip(route), priceChip(route)], bylineExtra: [renderEditedChip(route.tipId, route.rootId)], author: route.author, favKind: "logistics", isFavorite: route.isFavorite, reportTitle: route.title, spread: params.spread || null, deleteAction: route.isOwner ? contentDeleteAction("logisticsRoute", route.id) : null }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, route.title)), - div({ class: "card-chips-row" }, kindChip(route), modeChip(route), statusChip(route), priceChip(route)), + renderDetailShare(route.id, href, route.title), table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, fmt(route.createdAt)) - ), tr( td({ class: "tribe-info-label" }, i18n.logisticsDateLabel), td({ class: "tribe-info-value", colspan: "3" }, fmt(route.date)) @@ -348,10 +337,7 @@ exports.singleLogisticsView = async (route, params = {}) => { ...capacity.map(c => tr( td({ class: "tribe-info-label" }, c.label), td({ class: "tribe-info-value", colspan: "3" }, c.value) - )), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(route.author)) - ) + )) ), countersBlock(route), renderMapEmbed(params.mapData, route.mapUrl), @@ -388,8 +374,7 @@ exports.singleLogisticsView = async (route, params = {}) => { renderRatings(route), renderEngagement(route.id, renderOpinionsVoting("/logistics/opinions", route.id, route.opinions, href, route.opinions_inhabitants), - renderCommentsSection({ action: `/logistics/${encodeURIComponent(route.id)}/comments`, comments: params.comments || [], returnTo: href }) - ) + renderCommentsSection({ action: `/logistics/${encodeURIComponent(route.id)}/comments`, comments: params.comments || [], returnTo: href })) ); return template( route.title, diff --git a/src/views/mailing_view.js b/src/views/mailing_view.js index 071b884..20effe6 100644 --- a/src/views/mailing_view.js +++ b/src/views/mailing_view.js @@ -1,8 +1,7 @@ -const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, details, summary, ul, li, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, details, summary, ul, li } = require("../server/node_modules/hyperaxe"); const { TEXT_CAP } = require('../backend/long_text'); -const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderContentActions, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { template, i18n, userLink, renderStateChip, renderOpenClosedChip, renderCardTop, renderCardByline, renderEngagement, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderDetailShare } = require("./main_views"); const { renderStyledText } = require("../backend/renderStyledText"); -const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const userId = config.keys.id; @@ -18,7 +17,6 @@ const statusLabel = (s) => String(i18n[`mailingStatus${cap(s)}`] || s).toUpperCa const historyLabel = (m) => String(i18n[`mailingHistory${cap(m)}`] || m).toUpperCase(); const listHref = (list) => `/mailing/${encodeURIComponent(list.id)}`; -const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); const statusChip = (list) => renderStateChip(list.status === "ACTIVE" ? "mutuals" : "closed", list.status === "ACTIVE" ? "โœ“" : "โœ—", statusLabel(list.status)); const countsLine = (list) => span({ class: "card-label activity-update-counts mailing-counts" }, `๐Ÿ‘ฅ: ${list.participantCount || 0} ยท ๐Ÿ’ฌ ${list.messageCount || 0}`); @@ -62,16 +60,20 @@ exports.mailingChipFor = mailingChipFor; const renderArchiveItem = (list, params = {}) => li({ class: "mailing-archive-item" }, - div({ class: "emergency-update-head mailing-archive-head" }, - div({ class: "mailing-archive-meta" }, - a({ href: listHref(list), class: "user-link" }, list.title || "โ€”"), - countsLine(list), + renderCardTop(list.id, listHref(list), { + date: list.createdAt, + chips: [ renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), statusChip(list), list.isMember && !list.isOwner ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) : null - ), - renderContentActions(list.id, listHref(list), { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: (params.spreadMap && params.spreadMap.get(list.id)) || null, deleteAction: String(list.author) === String(userId) ? contentDeleteAction("mailingList", list.id) : null }) - ) + ], + author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: (params.spreadMap && params.spreadMap.get(list.id)) || null, deleteAction: String(list.author) === String(userId) ? contentDeleteAction("mailingList", list.id) : null + }), + div({ class: "mailing-archive-meta" }, + a({ href: listHref(list), class: "user-link" }, list.title || "โ€”"), + countsLine(list) + ), + null ); const chipButtons = (modes, filter, census, labelOf) => @@ -159,6 +161,7 @@ exports.mailingView = async (lists, filter = "ALL", params = {}) => { const renderMessage = (list, m, opts = {}) => div({ class: "mailing-message" }, + renderCardByline({ author: m.author, date: m.sentAt }), div({ class: "emergency-update-head mailing-message-head" }, span({ class: "mailing-message-subject" }, m.subject || i18n.pmNoSubject), list.canWrite && !opts.noReply @@ -172,8 +175,7 @@ const renderMessage = (list, m, opts = {}) => ) : null ), - div({ class: "mailing-message-text" }, ...renderStyledText(m.text || "")), - p({ class: "card-footer" }, span({ class: "date-link" }, `${fmt(m.sentAt)}`), userLink(m.author)) + div({ class: "mailing-message-text" }, ...renderStyledText(m.text || "")) ); const renderHistory = (list, history, mode) => { @@ -259,21 +261,10 @@ exports.singleMailingView = async (list, params = {}) => { ] : []; const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(list.id, href, { author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: params.spread || null, deleteAction: isAuthor ? contentDeleteAction("mailingList", list.id) : null }) - ), + renderCardTop(list.id, href, { arrow: false, date: list.createdAt, chips: [renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), statusChip(list)], author: list.author, favKind: "mailing", isFavorite: list.isFavorite, reportTitle: list.title, spread: params.spread || null, deleteAction: isAuthor ? contentDeleteAction("mailingList", list.id) : null }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, list.title)), - div({ class: "card-chips-row" }, renderOpenClosedChip(list.listType, { statusChipOPEN: typeLabel("OPEN"), statusChipCLOSED: typeLabel("CLOSED") }), statusChip(list)), + renderDetailShare(list.id, href, list.title), list.description ? p({ class: "tribe-side-description" }, ...renderStyledText(list.description)) : null, - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, fmt(list.createdAt)) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(list.author)) - ) - ), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.mailingSubscribers}: ${list.participantCount || 0}`) ), @@ -297,7 +288,8 @@ exports.singleMailingView = async (list, params = {}) => { form({ method: "POST", action: `${href}/share` }, button({ type: "submit", class: "filter-btn" }, i18n.sharePm)) ), renderTags(list.tags), - ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + null ); const main = div({ class: "tribe-main" }, params.write && list.status === "ACTIVE" && (!list.closed || list.isMember) ? renderCompose(list, params) : null, diff --git a/src/views/main_views.js b/src/views/main_views.js index 82ff836..251b851 100644 --- a/src/views/main_views.js +++ b/src/views/main_views.js @@ -27,7 +27,7 @@ const getUserId = async () => { }; exports.getUserId = getUserId; -const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe"); +const { a, article, br, body, button, details, div, em, form, h1, h2, h3, head, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe, object } = require("../server/node_modules/hyperaxe"); const lodash = require("../server/node_modules/lodash"); const { sanitizeHtml } = require('../backend/sanitizeHtml'); @@ -62,6 +62,39 @@ const renderCreatedRows = (createdAt, author, updatedAt) => { }; exports.renderCreatedRows = renderCreatedRows; +const renderEditedChip = (tipId, rootId) => tipId && rootId && String(tipId) !== String(rootId) + ? a({ href: `/versions/${encodeURIComponent(tipId)}`, class: 'edited-chip', title: i18n.wikiHistory }, `โœŽ ${String(i18n.edited || 'edited').toUpperCase()}`) + : null; +exports.renderEditedChip = renderEditedChip; + +const renderMediaLockNote = (item) => (item && item.mediaLocked === true) || require('../backend/opinion_categories').hasOpinions(item) + ? p({ class: 'media-lock-note' }, i18n.mediaLockedAfterOpinions) + : null; +exports.renderMediaLockNote = renderMediaLockNote; + +const versionTitle = (c) => String(c.title || c.name || c.question || c.concept || c.url || '').trim(); +const versionText = (c) => String(c.description || c.text || '').trim(); +const versionsView = ({ versions = [], backHref = '/' } = {}) => template( + i18n.wikiHistory, + section(div({ class: 'tags-header module-header-line' }, h2(i18n.wikiHistory), p(versionTitle((versions[0] || {}).content || {})))), + section( + div({ class: 'versions-list' }, + ...versions.map((v, i) => div({ class: 'card card-rpg versions-item' }, + div({ class: 'card-chips-row' }, + span({ class: 'edited-chip' }, `${i18n.wikiVersion} ${versions.length - i}`), + i === 0 ? span({ class: 'edited-chip' }, i18n.wikiCurrentVersion) : null, + span({ class: 'votations-comment-date' }, moment(v.ts).format('YYYY/MM/DD HH:mm')) + ), + versionTitle(v.content) ? h2(versionTitle(v.content)) : null, + versionText(v.content) ? p({ class: 'post-text-pre' }, ...renderStyledText(versionText(v.content))) : null, + Array.isArray(v.content.tags) && v.content.tags.length ? div({ class: 'card-tags' }, ...v.content.tags.map(t => span({ class: 'tag-link' }, `#${t}`))) : null + )) + ), + p(a({ href: backHref, class: 'filter-btn' }, `โ† ${i18n.wikiBackToPage}`)) + ) +); +exports.versionsView = versionsView; + const inboxBadges = () => { const inboxCount = Number(sharedState.getInboxCount()) || 0; return [inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '']; @@ -258,12 +291,14 @@ const clearnetHrefFor = (viewHref, blockId, title) => { return base ? `${base}/c/${seg}/${id}` : null; }; let linkBoxSeq = 0; -const renderLinkBox = (blockId, viewHref, title) => { +const renderLinkBox = (blockId, viewHref, title, trigger = null) => { linkBoxSeq = (linkBoxSeq + 1) % 1000000; const id = `linkbox-${linkBoxSeq}-${String(blockId).replace(/[^a-zA-Z0-9]/g, '').slice(-10)}`; const clearnetHref = clearnetHrefFor(viewHref, blockId, title); return [ - a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')), + trigger + ? a({ href: `#${id}`, id: `${id}-src`, class: trigger.class, title: i18n.copyLinkTitle }, ...trigger.content) + : a({ href: `#${id}`, id: `${id}-src`, class: 'btn-singleview btn-link', title: i18n.copyLinkTitle }, renderIcon('link')), span({ id, class: 'lightbox link-lightbox' }, a({ href: `#${id}-src`, class: 'lightbox-close' }, '\u00d7'), span({ class: 'link-box' }, @@ -277,8 +312,8 @@ const renderLinkBox = (blockId, viewHref, title) => { const renderWalletChip = () => { try { if (getConfig().modules.walletMod !== 'on') return null; } catch (_) { return null; } const ready = sharedState.getWalletReady ? sharedState.getWalletReady() : false; - const chip = renderStateChip(ready ? 'mutuals' : 'closed', 'โ„', ready ? i18n.bankWalletReady : i18n.bankWalletNotConfigured); - return ready ? chip : a({ href: '/wallet', class: 'wallet-chip-link' }, chip); + const chip = renderStateChip(ready ? 'mutuals' : 'closed', 'โ„', 'ECOin'); + return ready ? chip : a({ href: '/wallet', class: 'wallet-chip-link', title: i18n.bankWalletNotConfigured }, chip); }; exports.renderWalletChip = renderWalletChip; @@ -329,11 +364,15 @@ const renderIcon = (name) => span({ class: 'btn-icon', [RAW_HTML]: ` renderIcon('hide'); exports.renderIcon = renderIcon; exports.renderHideIcon = renderHideIcon; +const AVATAR_SIZE = 64; +const CARD_MENU_GLYPH = '\u22EF'; +const SHARE_GLYPH = '\u2197'; +const COMMENTS_GLYPH = '\u2711'; const renderContentActions = (msgId, viewHref, opts = {}) => { const o = (opts && typeof opts === 'object') ? opts : {}; const blockId = (typeof msgId === 'string' && msgId.startsWith('%')) ? msgId : null; const myId = (config.keys && config.keys.id) ? config.keys.id : ''; - const linkBtn = blockId && viewHref ? renderLinkBox(blockId, viewHref, o.reportTitle) : null; + const linkBtn = blockId && viewHref && o.share !== false ? renderLinkBox(blockId, viewHref, o.reportTitle) : null; const pinBtn = o.favKind && blockId ? form({ method: 'POST', action: `/${o.favKind}/favorites/${o.isFavorite ? 'remove' : 'add'}/${encodeURIComponent(blockId)}`, class: 'content-action-form' }, @@ -357,9 +396,17 @@ const renderContentActions = (msgId, viewHref, opts = {}) => { ? a({ href: `/blockexplorer/block/${encodeURIComponent(blockId)}`, class: 'btn-singleview', title: i18n.blockchainViewBlockexplorer }, renderIcon('block')) : null; - const herePath = (listScope() || {}).path; - const onItself = (() => { try { return !!herePath && decodeURIComponent(String(viewHref || '').split(/[?#]/)[0]) === decodeURIComponent(String(herePath)); } catch (_) { return false; } })(); - const contentBtn = viewHref && !onItself + const scope = listScope() || {}; + const herePath = scope.path; + const onItself = (() => { + try { + if (!herePath) return false; + const [path, query = ''] = String(viewHref || '').split('#')[0].split('?'); + if (decodeURIComponent(path) !== decodeURIComponent(String(herePath))) return false; + return !query || query === String(scope.query || ''); + } catch (_) { return false; } + })(); + const contentBtn = viewHref && !onItself && o.arrow !== false ? a({ href: viewHref, class: 'btn-singleview btn-content', title: i18n.visitContent }, renderIcon('open')) : null; @@ -404,12 +451,114 @@ const renderContentActions = (msgId, viewHref, opts = {}) => { ) : null; - if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null; - // movil: borrar al final, lejos del pulgar - if (process.env.OASIS_MOBILE === '1') return div({ class: 'content-actions' }, contentBtn, linkBtn, pinBtn, spreadBtn, pmBtn, callBtn, donateBtn, reportBtn, hideBtn, deleteBtn); - return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn); + if (o.menu !== true) { + if (!pinBtn && !spreadBtn && !chainBtn && !contentBtn && !reportBtn && !pmBtn && !callBtn && !deleteBtn && !donateBtn && !torrentBtn && !hideBtn) return null; + return div({ class: 'content-actions' }, deleteBtn, spreadBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn, contentBtn); + } + const menuItems = [deleteBtn, pinBtn, reportBtn, hideBtn, donateBtn, pmBtn, callBtn, torrentBtn, linkBtn, chainBtn].flat().filter(Boolean); + if (!menuItems.length && !spreadBtn && !contentBtn) return null; + return div({ class: 'content-actions' }, + spreadBtn, + menuItems.length + ? details({ class: 'card-menu' }, + summary({ class: 'btn-singleview card-menu-toggle', title: i18n.cardMenuTitle }, CARD_MENU_GLYPH), + div({ class: 'card-menu-items' }, ...menuItems) + ) + : null, + contentBtn + ); }; exports.renderContentActions = renderContentActions; + +const avatarSrc = (feedId) => { + const cached = config.public ? null : nameCache.image(feedId); + return cached ? `/image/${AVATAR_SIZE}/${encodeURIComponent(cached)}?fallback=avatar` : `/avatar/${encodeURIComponent(feedId)}`; +}; +const renderCardByline = (opts = {}) => { + const author = opts.bylineAuthor || opts.author ? String(opts.bylineAuthor || opts.author) : ''; + const when = opts.date ? moment(opts.date) : null; + const time = when && when.isValid() ? span({ class: 'card-byline-time', title: when.format('YYYY/MM/DD HH:mm') }, when.fromNow()) : null; + const extra = [].concat(opts.bylineExtra || [], opts.chips || []).filter(Boolean); + if (!author && !time && !extra.length) return span(); + return div({ class: 'card-byline' }, + author ? a({ href: `/author/${encodeURIComponent(author)}`, class: 'card-byline-avatar' }, img({ src: avatarSrc(author), alt: '' })) : null, + author ? userLink(author, opts.authorName) : null, + time, + ...extra + ); +}; +const renderCardTop = (msgId, viewHref, opts = {}) => { + const o = (opts && typeof opts === 'object') ? opts : {}; + return div({ class: 'card-header activity-card-header card-top' }, + renderCardByline(o), + renderContentActions(msgId, viewHref, { ...o, share: false, menu: true }) + ); +}; +const COMMENTABLE_HREF_RE = /^\/(?:(?:audios|videos|images|documents|bookmarks|files|torrents|blogs|feed|wiki|events|tasks|votes|polls|projects|reports|emergencies|maps|transfers|jobs|housing|campaigns|logistics|market|podcasts)\/[^/?#]+|(?:shops\/product|industry\/blueprint|school\/course|podcasts\/episode)\/[^/?#]+)(?:[?#]|$)/; +const commentCountFor = (ids) => { + const counts = (listScope() || {}).commentCounts; + if (!(counts instanceof Map)) return 0; + for (const id of [].concat(ids)) { const hit = id ? counts.get(String(id)) : null; if (hit) return hit.count; } + return 0; +}; +const renderCommentsPill = (id, viewHref) => { + const href = String(viewHref || ''); + if (!id || !COMMENTABLE_HREF_RE.test(href)) return null; + const scope = listScope() || {}; + try { if (scope.path && decodeURIComponent(href.split(/[?#]/)[0]) === decodeURIComponent(String(scope.path))) return null; } catch (_) {} + const counts = scope.commentCounts instanceof Map ? scope.commentCounts : null; + const hit = counts ? counts.get(String(id)) : null; + const total = hit ? hit.count : 0; + return a({ href: commentsOpenHref(href.split('#')[0], total), class: total ? 'comments-summary engage-on comments-link' : 'comments-summary comments-link', title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments' }, + span({ class: 'comments-summary-icon' }, COMMENTS_GLYPH), + counts ? span({ class: 'comments-summary-count' }, `(${total})`) : null + ); +}; +const OPINION_ROUTE_BASES = [ + [/^\/(?:shops\/product|industry\/blueprint)\/[^/?#]+(?:[?#]|$)/, (m) => `${m[0].replace(/\/[^/?#]+(?:[?#].*)?$/, '')}/opinions`], + [/^\/(?:school\/course|podcasts\/episode)\/[^/?#]+(?:[?#]|$)/, (m) => (m[0].startsWith('/school') ? '/school/opinions' : '/podcasts/opinions')], + [/^\/(audios|videos|images|documents|bookmarks|files|torrents|blogs|feed|wiki|events|tasks|votes|polls|projects|reports|transfers|jobs|housing|campaigns|logistics|market|maps|industry|podcasts)\/[^/?#]+(?:[?#]|$)/, (m) => `/${m[1]}/opinions`] +]; +const opinionsBaseFor = (href) => { + for (const [re, base] of OPINION_ROUTE_BASES) { const m = href.match(re); if (m) return base(m); } + return null; +}; +const renderOpinionsPill = (ids, viewHref) => { + const href = String(viewHref || '').split('#')[0]; + const list = [...new Set([].concat(ids).filter(Boolean).map(String))]; + const base = list.length ? opinionsBaseFor(href) : null; + if (!base) return null; + const scope = listScope() || {}; + try { if (scope.path && decodeURIComponent(href.split('?')[0]) === decodeURIComponent(String(scope.path))) return null; } catch (_) {} + const tallies = scope.opinionTallies instanceof Map ? scope.opinionTallies : null; + const merged = { opinions: {}, opinions_inhabitants: [] }; + for (const id of list) { + const t = tallies ? tallies.get(id) : null; + if (!t) continue; + for (const [k, v] of Object.entries(t.opinions || {})) merged.opinions[k] = (merged.opinions[k] || 0) + (Number(v) || 0); + for (const who of t.opinions_inhabitants || []) if (!merged.opinions_inhabitants.includes(who)) merged.opinions_inhabitants.push(who); + } + const here = scope.path ? `${scope.path}${scope.query ? `?${scope.query}` : ''}` : null; + return renderOpinionsVoting(base, list[0], merged.opinions, here, merged.opinions_inhabitants); +}; +const renderShareButton = (blockId, viewHref, title) => { + if (typeof blockId !== 'string' || !blockId.startsWith('%') || !viewHref) return null; + return span({ class: 'card-share' }, ...renderLinkBox(blockId, viewHref, title, { + class: 'share-summary-link', + content: [span({ class: 'share-summary-icon' }, SHARE_GLYPH), span({ class: 'share-summary-label' }, i18n.shareContent)] + })); +}; +exports.renderCardByline = renderCardByline; +exports.renderCardTop = renderCardTop; +exports.renderShareButton = renderShareButton; +const renderDetailShare = (blockId, viewHref, title) => { + const share = renderShareButton(blockId, viewHref, title); + return share ? div({ class: 'detail-share' }, share) : null; +}; +exports.renderDetailShare = renderDetailShare; +exports.renderCommentsPill = renderCommentsPill; +exports.commentCountFor = commentCountFor; +exports.renderOpinionsPill = renderOpinionsPill; const CONTENT_FAV_KIND = { image: 'images', audio: 'audios', video: 'videos', document: 'documents', bookmark: 'bookmarks', torrent: 'torrents', file: 'files', event: 'events', task: 'tasks', @@ -811,8 +960,10 @@ exports.i18n = i18n; Object.defineProperty(exports, 'selectedLanguage', { get: () => selectedLanguage }); const opinionCategoriesList = require('../backend/opinion_categories'); -const renderEngagement = (id, opinionsNode, commentsNode) => { - const parts = [opinionsNode, commentsNode].filter(Boolean); +const renderEngagement = (id, opinionsNode, commentsNode, share = null) => { + const autoOpinions = !opinionsNode && share && share.opinions !== false ? renderOpinionsPill([id, share.rootId], share.href) : null; + const autoComments = !commentsNode && share && share.comments !== false ? renderCommentsPill(id, share.href) : null; + const parts = [opinionsNode || autoOpinions, commentsNode || autoComments, share ? renderShareButton(id, share.href, share.title) : null].filter(Boolean); if (!parts.length) return null; return div({ class: 'card-engage' }, ...parts); }; @@ -855,6 +1006,109 @@ const renderCardMetaRow = (...nodes) => { return div({ class: 'card-meta-row' }, ...flat); }; exports.renderCardMetaRow = renderCardMetaRow; +const commentsOpenHref = (detailHref, total = 0) => `${detailHref}${detailHref.includes('?') ? '&' : '?'}comments=open${total ? '#comments-latest' : ''}`; +const renderCommentsSummary = (href, total = 0) => href + ? a({ href, class: total ? 'comments-summary-link engage-on' : 'comments-summary-link', title: i18n.commentsGoToLast || i18n.voteNewCommentLabel || 'Comments' }, + span({ class: 'comments-summary-icon' }, 'โœ‘'), + span({ class: 'comments-summary-count' }, `(${total})`) + ) + : null; +exports.commentsOpenHref = commentsOpenHref; +exports.renderCommentsSummary = renderCommentsSummary; +const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; +const formatSize = (bytes) => { + let n = Number(bytes) || 0; + if (n <= 0) return "โ€”"; + let unit = 0; + while (n >= 1024 && unit < SIZE_UNITS.length - 1) { n /= 1024; unit++; } + return unit === 0 ? `${n} B` : `${n.toFixed(1)} ${SIZE_UNITS[unit]}`; +}; +exports.formatSize = formatSize; +const forumReplyAnchor = (key) => `msg-${String(key || "").replace(/[^a-zA-Z0-9]/g, "")}`; +exports.forumReplyAnchor = forumReplyAnchor; +const pdfView = require("../backend/pdf_view"); +const pdfFileName = (t) => { const n = String(t || "document").replace(/[\\/:*?"<>|\r\n]+/g, " ").trim().slice(0, 120) || "document"; return n.toLowerCase().endsWith(".pdf") ? n : `${n}.pdf`; }; +const pdfDownloadHref = (src, t) => `${src}${String(src).includes("?") ? "&" : "?"}download=1&name=${encodeURIComponent(pdfFileName(t))}`; +const pdfPageImage = (src, page, width, rotation, prefix = "") => `${prefix}/pdf/page?${new URLSearchParams({ src, p: String(page), w: String(width), ...(rotation ? { r: String(rotation) } : {}) })}`; +const pdfEmbedHref = (src, t, fid) => `/pdf/embed?${new URLSearchParams({ src, ...(t ? { title: String(t).slice(0, 200) } : {}), fid })}`; +const renderPdfDocument = (url, { title: docTitle = "" } = {}) => { + if (!url) return null; + const fid = pdfView.frameIdOf(url); + return span({ class: "pdf-holder" }, + object({ id: fid, data: pdfEmbedHref(url, docTitle, fid), type: "text/html", class: "pdf-frame", title: String(docTitle || "PDF"), "aria-label": String(docTitle || "PDF") }, ""), + a({ href: "#", class: "pdf-unmax", title: i18n.pdfClose, "aria-label": i18n.pdfClose }, "โœ•") + ); +}; +exports.renderPdfDocument = renderPdfDocument; + +const renderPdfViewer = (state, { title: docTitle = "", base = "/pdf", keep = {}, embed = false, back = "", prefix = "" } = {}) => { + if (!state) return null; + if (!state.src && !state.error) state = { ...state, error: "missing" }; + const src = state.src || ""; + const clean = (v) => { for (const k of Object.keys(v)) if (v[k] === null || v[k] === undefined || v[k] === "" || v[k] === 0) delete v[k]; return v; }; + const params = (over = {}) => clean({ ...keep, fid: state.fid || null, p: state.page > 1 ? state.page : null, z: state.zoom !== pdfView.PDF_ZOOM_DEFAULT ? state.zoom : null, r: state.rotation || null, full: state.full ? "1" : null, ...over }); + const href = (over) => { const q = new URLSearchParams(params(over)).toString(); return `${base}${q ? `?${q}` : ""}#pdf-viewer`; }; + const ctl = (url, label, tip, extra = {}) => url ? a({ href: url, title: tip, "aria-label": tip, ...extra }, label) : span({ class: "pdf-off", title: tip }, label); + const download = a({ href: pdfDownloadHref(src, docTitle), target: "_top", title: i18n.fileShareDownload || "Download", "aria-label": i18n.fileShareDownload || "Download" }, "โฌ‡๏ธ"); + const shell = (...children) => div({ id: "pdf-viewer", class: ["pdf-viewer-container", embed ? "pdf-embedded" : "", state.full ? "pdf-maximized" : ""].filter(Boolean).join(" ") }, ...children); + if (state.error) { + return shell( + div({ class: "pdf-canvas-wrap pdf-message-wrap" }, p({ class: "pdf-message" }, i18n.contentNotAvailableYet)), + state.error === "unavailable" ? div({ class: "pdf-controls" }, ctl(href({}), "โ†ป", i18n.pdfRetry), download) : null + ); + } + const { page, pages, zoom } = state; + const rotation = state.rotation || 0; + const zooms = pdfView.PDF_ZOOMS; + const zi = zooms.indexOf(zoom); + const fullOn = { ...keep, src, title: docTitle || null, back: back || null, p: page > 1 ? page : null, z: zoom !== pdfView.PDF_ZOOM_DEFAULT ? zoom : null, r: rotation || null, full: "1" }; + const fullscreen = embed + ? a({ href: state.fid && back ? `${back}#${state.fid}` : `${prefix}/pdf?${new URLSearchParams(clean(fullOn))}`, target: "_top", title: i18n.pdfFullscreen, "aria-label": i18n.pdfFullscreen }, "๐Ÿ”ฒ") + : state.full + ? a({ href: back || href({ full: null }), title: i18n.pdfFullscreen, "aria-label": i18n.pdfFullscreen }, "๐Ÿ”ฒ") + : ctl(href({ full: "1" }), "๐Ÿ”ฒ", i18n.pdfFullscreen); + return shell( + div({ class: zoom > pdfView.PDF_ZOOM_DEFAULT ? "pdf-canvas-wrap pdf-wrap-scroll" : "pdf-canvas-wrap" }, + img({ src: pdfPageImage(src, page, pdfView.widthOf(zoom), rotation, prefix), class: `pdf-page-image pdf-z${zoom}`, alt: `${docTitle || "PDF"} โ€” ${i18n.pdfPage} ${page} / ${pages}` }) + ), + div({ class: "pdf-controls" }, + ctl(page > 1 ? href({ p: page - 1 }) : null, "โฌ…๏ธ", i18n.pdfPrevPage), + ctl(page < pages ? href({ p: page + 1 }) : null, "โžก๏ธ", i18n.pdfNextPage), + ctl(zi < zooms.length - 1 ? href({ z: zooms[zi + 1] }) : null, "๐Ÿ”+", i18n.pdfZoomIn), + ctl(zi > 0 ? href({ z: zooms[zi - 1] }) : null, "๐Ÿ”โˆ’", i18n.pdfZoomOut), + ctl(href({ r: (rotation + 90) % 360 || null }), "โ†ป", i18n.pdfRotate), + download, + fullscreen + ) + ); +}; +exports.renderPdfViewer = renderPdfViewer; + +exports.pdfEmbedView = (state, { title: docTitle = "", back = "" } = {}) => { + const theme = getConfig().themes.current || "Dark-SNH"; + return doctypeString + html( + { lang: i18nBase[selectedLanguage] ? selectedLanguage : "en", class: "pdf-embed-root" }, + head( + title(docTitle || "PDF"), + link({ rel: "stylesheet", href: `/assets/styles/style.css?v=${assetVersion()}` }), + link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), + meta({ charset: "utf-8" }), + meta({ name: "viewport", content: "width=device-width, initial-scale=1" }) + ), + body({ class: "pdf-embed" }, renderPdfViewer(state, { title: docTitle, base: "/pdf/embed", keep: { src: state && state.src, title: docTitle || null, back: back || null }, embed: true, back })) + ).outerHTML; +}; + +exports.pdfViewerView = (state, { title: docTitle = "", back = "" } = {}) => template( + docTitle || "PDF", + section( + div({ class: "tags-header module-header-line" }, + h2(docTitle || "PDF"), + back ? a({ href: back, class: "filter-btn" }, `โ† ${i18n.goBack}`) : null + ), + renderPdfViewer(state, { title: docTitle, base: "/pdf", keep: { src: state && state.src, title: docTitle || null, back: back || null } }) + ) +); const renderOpinionsVoting = (basePath, id, opinions, returnTo, voters, pathFor = null) => { const ops = opinions || {}; @@ -1895,9 +2149,217 @@ const renderEmergencyBanner = (featured, opts = {}) => { ); }; +const apkModbar = (els) => { + const clsOf = (n) => (n && typeof n === 'object') ? String(n['class'] || n.className || '') : ''; + const has = (n, c) => (' ' + clsOf(n) + ' ').includes(' ' + c + ' '); + const tag = (n) => (n && typeof n === 'object') ? String(n.nodeName || '').toLowerCase() : ''; + const kids = (n) => (n && Array.isArray(n.childNodes)) ? n.childNodes : []; + const nodes = (n) => kids(n).filter(c => c && typeof c === 'object' && tag(c) !== '#text'); + const walk = (list, pred, depth) => { + for (const n of list) { + if (!n || typeof n !== 'object') continue; + if (pred(n)) return n; + if (depth > 0) { const r = walk(kids(n), pred, depth - 1); if (r) return r; } + } + return null; + }; + const addCls = (n, c) => { if (n) n['class'] = (clsOf(n) + ' ' + c).trim(); }; + const detach = (n) => { if (n && n.parentElement) n.parentElement.removeChild(n); return n; }; + const roots = [].concat(...els.map(e => Array.isArray(e) ? e : [e])).filter(n => n && typeof n === 'object'); + const compactSearch = (fb) => { + if (!fb) return; + const isField = (c) => (tag(c) === 'input' && !['hidden', 'submit', 'button', 'checkbox', 'radio'].includes(String(c.type || c.getAttribute('type') || 'text').toLowerCase())) || tag(c) === 'select'; + const fields = []; + const collect = (n, d) => { for (const c of nodes(n)) { if (isField(c)) fields.push(c); else if (d > 0) collect(c, d - 1); } }; + collect(fb, 2); + const main = fields.find(c => tag(c) === 'input' && has(c, 'filter-box__input')) || fields[0]; + const more = fields.filter(c => c !== main); + if (more.length >= 1) { + const firstOpt = (sel) => nodes(sel).find(o => tag(o) === 'option'); + const changedSel = (sel) => { const opts = nodes(sel).filter(o => tag(o) === 'option'); const chosen = opts.find(o => o.selected || o.getAttribute('selected') != null); return !!chosen && chosen !== firstOpt(sel); }; + const filled = more.some(c => (tag(c) === 'input' && String(c.value || c.getAttribute('value') || '') !== '') || (tag(c) === 'select' && changedSel(c))); + more.forEach(detach); + const go = walk([fb], n => has(n, 'filter-box__button'), 3); + const toggleLabel = label({ for: 'apk-more-toggle', class: 'apk-more-btn' }, ''); + if (go && go.parentElement) go.parentElement.insertBefore(toggleLabel, go); else fb.appendChild(toggleLabel); + fb.appendChild(input(filled ? { type: 'checkbox', id: 'apk-more-toggle', class: 'apk-more-toggle', checked: true } : { type: 'checkbox', id: 'apk-more-toggle', class: 'apk-more-toggle' })); + fb.appendChild(div({ class: 'apk-more-fields' }, ...more)); + } + }; + const textOfS = (n) => { if (!n || typeof n !== 'object') return ''; if (tag(n) === '#text') return String(n.nodeValue != null ? n.nodeValue : (n.value || '')); return kids(n).map(textOfS).join(''); }; + const compactSearchForm = (sf) => { + if (!sf) return null; + const typeOf = (c) => String(c.type || c.getAttribute('type') || 'text').toLowerCase(); + const q = walk([sf], n => tag(n) === 'input' && (['q', 'query'].includes(String(n.name || n.getAttribute('name') || '')) || typeOf(n) === 'search'), 6); + const go = walk([sf], n => tag(n) === 'button' && typeOf(n) === 'submit', 6); + if (!q || !go) return null; + const hidden = []; + const keepHidden = (n, d) => { for (const c of nodes(n)) { if (tag(c) === 'input' && typeOf(c) === 'hidden') hidden.push(c); else if (d > 0) keepHidden(c, d - 1); } }; + keepHidden(sf, 1); + const more = []; + const rows = []; + const grabRows = (n, d) => { for (const c of nodes(n)) { if (tag(c) === 'tr') rows.push(c); else if (d > 0) grabRows(c, d - 1); } }; + grabRows(sf, 6); + for (const tr of rows) { + const field = walk([tr], n => (tag(n) === 'input' && typeOf(n) !== 'hidden') || tag(n) === 'select', 3); + if (!field || field === q) continue; + const lab = walk([tr], n => tag(n) === 'label', 3); + detach(field); + more.push(div({ class: 'apk-more-field' }, span({ class: 'apk-more-label' }, textOfS(lab).trim()), field)); + } + const filled = more.some(w => { const f = nodes(w)[1]; return f && tag(f) === 'input' && String(f.value || f.getAttribute('value') || '') !== ''; }); + detach(q); detach(go); + for (const c of nodes(sf).slice()) sf.removeChild(c); + q['class'] = (clsOf(q) + ' filter-box__input').trim(); + go['class'] = (clsOf(go) + ' filter-box__button').trim(); + hidden.forEach(h => sf.appendChild(h)); + sf.appendChild(q); + sf.appendChild(div({ class: 'filter-box__controls' }, more.length ? label({ for: 'apk-more-toggle', class: 'apk-more-btn' }, '') : null, go)); + if (more.length) { + sf.appendChild(input(filled ? { type: 'checkbox', id: 'apk-more-toggle', class: 'apk-more-toggle', checked: true } : { type: 'checkbox', id: 'apk-more-toggle', class: 'apk-more-toggle' })); + sf.appendChild(div({ class: 'apk-more-fields' }, ...more)); + } + sf['class'] = 'filter-box apk-searchform'; + return sf; + }; + const tnav = walk(roots, n => has(n, 'tribe-section-nav'), 8); + if (tnav && tnav.parentElement) { + const textOfT = (n) => { if (!n || typeof n !== 'object') return ''; if (tag(n) === '#text') return String(n.nodeValue != null ? n.nodeValue : (n.value || '')); return kids(n).map(textOfT).join(''); }; + const tside = walk(roots, n => has(n, 'tribe-side'), 8); + const tname = textOfT(tside ? walk([tside], n => tag(n) === 'h2', 3) : null).trim(); + const tforms = []; + const grab = (n, d) => { for (const c of nodes(n)) { if (tag(c) === 'form') tforms.push(c); else if (d > 0) grab(c, d - 1); } }; + grab(tnav, 3); + const secOf = (f) => { const h = walk([f], x => tag(x) === 'input' && String(x.name || x.getAttribute('name') || '') === 'section', 2); return h ? String(h.value || h.getAttribute('value') || '') : ''; }; + const tsearchForm = tforms.find(f => secOf(f) === 'search'); + const taction = tsearchForm ? String(tsearchForm.getAttribute('action') || tsearchForm.action || '') : ''; + if (tsearchForm) { tforms.splice(tforms.indexOf(tsearchForm), 1); detach(tsearchForm); } + const tact = tforms.find(f => !!walk([f], x => has(x, 'active'), 2)); + tforms.forEach(detach); + detach(tnav); + const tordered = tact ? [tact, ...tforms.filter(f => f !== tact)] : tforms; + const tsec = tact ? secOf(tact) : 'activity'; + if (tside && tsec && tsec !== 'activity') detach(tside); + let tpin = null; + const tmain = walk(roots, n => has(n, 'tribe-main'), 8); + const tcb = tmain ? walk([tmain], n => has(n, 'create-button'), 6) : null; + if (tcb && tcb.parentElement) { + const cp = tcb.parentElement; + const lone = tag(cp) === 'form' && nodes(cp).every(c => c === tcb || (tag(c) === 'input' && String(c.type || c.getAttribute('type') || '') === 'hidden')); + if (lone) tpin = cp; + else { + let f = cp; + while (f && tag(f) !== 'form') f = f.parentElement; + if (f && tag(tcb) === 'button') { if (!f.getAttribute('id')) f.setAttribute('id', 'apk-tribe-form'); tcb.setAttribute('form', f.getAttribute('id')); } + tpin = tcb; + } + const holder = tpin.parentElement; + detach(tpin); + addCls(tpin, 'apk-pin'); + if (holder && nodes(holder).length === 0) detach(holder); + } + let tfb = tmain ? walk([tmain], n => has(n, 'filter-box'), 8) : null; + if (!tfb && tmain) { const sfx = walk([tmain], n => has(n, 'search-form'), 8); if (sfx) { detach(sfx); tfb = compactSearchForm(sfx); } } + if (!tfb && taction) { try { tfb = compactSearchForm(require('./search_view').renderSearchForm({ action: taction, query: '', queryName: 'q', hidden: [['section', 'search']] })); } catch (_) { tfb = null; } } + const tst = tmain ? walk([tmain], n => has(n, 'module-stats-line'), 8) : null; + detach(tfb); detach(tst); + if (tst) addCls(tst, 'apk-stats'); + if (tfb && !has(tfb, 'apk-searchform')) compactSearch(tfb); + return { bar: div({ class: 'apk-modbar apk-tribebar' }, div({ class: 'apk-modbar-row' }, h2({ class: 'apk-title' }, tname), tpin), div({ class: 'filters apk-strip' }, ...tordered), tfb, tst ? div({ class: 'apk-modbar-foot' }, tst) : null), stats: null }; + } + const th = walk(roots, n => has(n, 'tags-header'), 3); + if (!th || !th.parentElement) return null; + const isChip = (n) => has(n, 'activity-chip') || has(n, 'filter-btn') || has(n, 'create-button'); + const STRIPS = ['activity-filter-chips', 'filters', 'mode-buttons-row', 'mode-buttons', 'tribe-mode-buttons', 'inhabitant-action']; + const fl = walk(roots, n => STRIPS.some(c => has(n, c)) && !has(n, 'activity-toolbar-row') && !!walk(kids(n), isChip, 3), 5); + let fb = walk(roots, n => has(n, 'filter-box'), 6); + if (!fb) fb = compactSearchForm(walk(roots, n => has(n, 'search-form'), 8)); + let st = walk(roots, n => has(n, 'module-stats-line'), 6); + if (!st) { + const gb = walk(roots, n => has(n, 'graphos-statsbar'), 6); + if (gb) { + const textOf = (n) => { if (!n || typeof n !== 'object') return ''; if (tag(n) === '#text') return String(n.nodeValue != null ? n.nodeValue : (n.value || '')); return kids(n).map(textOf).join(''); }; + const items = nodes(gb).filter(c => has(c, 'graphos-kpi')).map(k => span(textOf(walk([k], x => has(x, 'stats-kpi-label'), 2)) + ': ', strong(textOf(walk([k], x => has(x, 'stats-kpi-value'), 2))))); + detach(gb); + if (items.length) st = div({ class: 'module-stats-line' }, ...items); + } + } + if (!st) { + const mc = walk(roots, n => has(n, 'modules-counts'), 6); + if (mc) { + const textOf = (n) => { if (!n || typeof n !== 'object') return ''; if (tag(n) === '#text') return String(n.nodeValue != null ? n.nodeValue : (n.value || '')); return kids(n).map(textOf).join(''); }; + const items = nodes(mc).filter(c => has(c, 'modules-count-item')).map(k => { const v = walk([k], x => has(x, 'modules-count-value'), 2); const label = kids(k).filter(x => x !== v).map(textOf).join('').trim(); return span(label.endsWith(':') ? label + ' ' : label + ': ', strong(textOf(v))); }); + detach(mc); + if (items.length) st = div({ class: 'module-stats-line' }, ...items); + } + } + if (fl) { + const isFilter = (c) => has(c, 'activity-chip') || has(c, 'filter-btn'); + const unitOf = (c) => isFilter(c) || (tag(c) === 'form' && nodes(c).some(isFilter)); + const isActive = (c) => has(c, 'active') || (tag(c) === 'form' && nodes(c).some(x => isFilter(x) && has(x, 'active'))); + const chips = (p) => nodes(p).filter(unitOf); + const holder = walk([fl], n => chips(n).length > 1, 3) || walk([fl], n => chips(n).length > 0, 3); + if (holder) { + const active = chips(holder).find(isActive); + const first = chips(holder)[0]; + if (active && first && active !== first) { holder.removeChild(active); holder.insertBefore(active, first); } + } + } + let pin = null; + const create = fl ? walk([fl], n => has(n, 'create-button'), 4) : null; + if (create && create.parentElement) { + const cp = create.parentElement; + const lone = tag(cp) === 'form' && cp !== fl && nodes(cp).every(c => c === create || (tag(c) === 'input' && String(c.type || c.getAttribute('type') || '') === 'hidden')); + if (lone) { + pin = cp; + } else { + let f = cp; + while (f && tag(f) !== 'form') f = f.parentElement; + if (f && tag(create) === 'button') { if (!f.getAttribute('id')) f.setAttribute('id', 'apk-filters-form'); create.setAttribute('form', f.getAttribute('id')); } + pin = create; + } + detach(pin); + addCls(pin, 'apk-pin'); + } + const stripHasChips = fl && !!walk([fl], n => has(n, 'activity-chip') || has(n, 'filter-btn') || has(n, 'create-button'), 4); + if (fl) addCls(fl, 'apk-strip'); + const title = nodes(th).find(c => tag(c) === 'h2'); + const extras = nodes(th).filter(c => c !== title && tag(c) !== 'p'); + detach(fl); detach(fb); detach(st); detach(th); + extras.forEach(detach); + if (title) { detach(title); addCls(title, 'apk-title'); } + if (st) addCls(st, 'apk-stats'); + if (fb && !has(fb, 'apk-searchform')) compactSearch(fb); + const subs = []; + for (const sc of ['inbox-bot-filters', 'inhabitant-subfilters']) { + const sb = walk(roots, n => has(n, sc), 8); + if (!sb || sb === fl) continue; + const sIsF = (c) => has(c, 'activity-chip') || has(c, 'filter-btn'); + const sUnits = (p) => nodes(p).filter(c => sIsF(c) || (tag(c) === 'form' && nodes(c).some(sIsF))); + const sHolder = walk([sb], n => sUnits(n).length > 1, 3); + if (sHolder) { + const sAct = sUnits(sHolder).find(c => has(c, 'active') || (tag(c) === 'form' && nodes(c).some(x => sIsF(x) && has(x, 'active')))); + const sFirst = sUnits(sHolder)[0]; + if (sAct && sFirst && sAct !== sFirst) { sHolder.removeChild(sAct); sHolder.insertBefore(sAct, sFirst); } + } + detach(sb); + addCls(sb, 'apk-strip'); + addCls(sb, 'apk-substrip'); + subs.push(sb); + } + const xrow = walk(roots, n => nodes(n).some(c => has(c, 'inbox-filters-label')), 8); + detach(xrow); + if (xrow) addCls(xrow, 'apk-xrow'); + const head = div({ class: 'apk-modbar-row' }, title || th, ...extras, pin); + const foot = st ? div({ class: 'apk-modbar-foot' }, st) : null; + return { bar: div({ class: 'apk-modbar' }, head, stripHasChips ? fl : null, xrow, fb, ...subs, foot), stats: null }; +}; + const template = (titlePrefix, ...elements) => { const scope = listScope(); if (scope && scope.dryRun) return ''; + let apkBanners = []; + const apkBar = apkModbar(elements); const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; const buildAiSuggestion = (compact) => { @@ -2162,7 +2624,7 @@ const template = (titlePrefix, ...elements) => { ) ) ), - (() => { + (apkBanners = [(() => { const updateInfo = (() => { try { return require('../backend/updater').readUpdateInfo(); } catch (_) { return null; } })(); if (updateInfo) { return div( @@ -2199,7 +2661,7 @@ const template = (titlePrefix, ...elements) => { })(), (getConfig().modules.aiNavMod === 'on' || uxMode === 'ainav') ? null : buildAiSuggestion(false), buildPhoneBanner(), - buildEmergencyBanner(false), + buildEmergencyBanner(false)].filter(Boolean), null), div( { class: uxMode === "ainav" ? "main-content ainav-only" : uxMode === "chats" ? "main-content chatsux-only" : uxMode === "feed" || uxMode === "phone" || uxMode === "rooms" ? "main-content chatsux-only" : "main-content" }, uxMode !== "blocks" ? null : div( @@ -2374,7 +2836,7 @@ const template = (titlePrefix, ...elements) => { ) ) ), - main({ id: "content", class: "main-column" }, elements, renderListPager(), renderLogWindowNote(), renderMobileCounters()) + main({ id: "content", class: "main-column" }, apkBar ? apkBar.bar : null, apkBanners.length ? div({ class: "apk-banners" }, ...apkBanners) : null, apkBar ? apkBar.stats : null, elements, renderListPager(), renderLogWindowNote(), renderMobileCounters()) ), renderBottomBar(), renderFooter() @@ -2488,23 +2950,6 @@ exports.tribeAccessDeniedView = (tribe) => { ); }; -exports.inviteRequiredView = (kind, tribe) => { - const msg = kind === 'pad' ? (i18n.tribePadInviteRequired || 'You do not have access to the pad. Ask for an invitation to access the content.') - : kind === 'chat' ? (i18n.tribeChatInviteRequired || 'You do not have access to the chat. Ask for an invitation to access the content.') - : (i18n.tribeContentAccessDeniedMsg); - const backHref = tribe ? `/tribe/${encodeURIComponent(tribe.id)}?section=${kind === 'chat' ? 'chats' : 'pads'}` : (kind === 'chat' ? '/chats' : '/pads'); - return template( - i18n.tribeContentAccessDenied, - div({ class: "div-center" }, - h2(i18n.tribeContentAccessDenied), - p(msg), - div({ class: "visit-btn-centered" }, - a({ href: backHref, class: "filter-btn" }, i18n.walletBack || "Back") - ) - ) - ); -}; - const thread = (messages, spreadMap = null) => { let lookingForTarget = true; let shallowest = Infinity; @@ -2638,16 +3083,12 @@ const postAside = ({ key, value }) => { const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const encoded = { key: encodeURIComponent(msg.key), - author: encodeURIComponent(msg.value?.author), parent: encodeURIComponent(msg.value?.content?.root), }; const url = { - author: `/author/${encoded.author}`, - spreadForm: `/spread/${encoded.key}`, link: `/thread/${encoded.key}#${encoded.key}`, parent: `/thread/${encoded.parent}#${encoded.parent}`, - avatar: msg.value?.meta?.author?.avatar?.url || '/assets/images/default-avatar.png', json: `/json/${encoded.key}`, subtopic: `/subtopic/${encoded.key}`, comment: `/comment/${encoded.key}`, @@ -2835,11 +3276,7 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const safeId = String(msg.key || u).replace(/[^a-zA-Z0-9_-]/g, ''); nodes.push( div({ class: 'card-field card-field-mt' }, - div({ - id: `pdf-container-${safeId}`, - class: 'pdf-viewer-container', - 'data-pdf-url': `/blob/${encodeURIComponent(u)}` - }) + renderPdfDocument(`/blob/${encodeURIComponent(u)}`, { title: c.title || safeId }) ) ); } @@ -2945,10 +3382,6 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { return null; } - const validTimestamp = moment(iso, moment.ISO_8601); - const timeAgo = validTimestamp.fromNow(); - const timeAbsolute = validTimestamp.toISOString().split(".")[0].replace("T", " "); - const fallbackVoted = !!msg.value?.meta?.voted; const fallbackVoteCount = msg.value?.meta?.votes?.length || 0; const spreadInfoObj = (spreadInfo && typeof spreadInfo === 'object') ? spreadInfo : null; @@ -2958,17 +3391,16 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { const alreadySpread = spreadInfoObj && typeof spreadInfoObj.alreadySpread === 'boolean' ? spreadInfoObj.alreadySpread : fallbackVoted; - const spreadButtonClass = alreadySpread ? 'liked' : null; - const messageClasses = ["post"]; + const messageClasses = ["post", "trending-card", "post-card"]; const recps = []; const addRecps = (recpsInfo) => { recpsInfo.forEach((recp) => { recps.push( a( - { href: `/author/${encodeURIComponent(recp.feedId)}` }, - img({ class: "avatar", src: recp.avatarUrl, alt: "" }) + { href: `/author/${encodeURIComponent(recp.feedId)}`, class: "card-byline-avatar" }, + img({ src: recp.avatarUrl, alt: "" }) ) ); }); @@ -2983,6 +3415,10 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { messageClasses.push("thread-target"); } + if (authorIdForName && config.keys && String(authorIdForName) === String(config.keys.id)) { + messageClasses.push("own-content"); + } + if (isBlocked) { messageClasses.push("blocked"); return section( @@ -3000,48 +3436,36 @@ const post = ({ msg, aside = false, preview = false, spreadInfo = null }) => { articleElement ); - const fragment = section( + const postKind = msg.value?.meta?.postType; + const kindLabel = postKind === "comment" ? i18n.typeComment : postKind === "subtopic" ? i18n.subtopic : postKind === "post" ? i18n.typePost : null; + const cardTitle = (hasContentWarning && msg.value.content.contentWarning) + || (THREAD_ENTITY_TYPES.has(contentType) ? entityTitle(content) : "") + || (typeof rawText === "string" ? postSnippet(rawText) : ""); + + const fragment = div( { id: msg.key, class: messageClasses.join(" "), }, - header( - div( - { class: "header-content" }, - a( - { href: url.author }, - img({ class: "avatar-profile", src: url.avatar, alt: "" }) - ), - span( - { class: "created-at" }, - userLink(authorIdForName, msg.value?.meta?.author?.name), - ` | ${timeAbsolute} | ${i18n.sendTime} `, - a({ href: url.link }, timeAgo) - ), - isPrivate ? "๐Ÿ”’" : null, - isPrivate ? recps : null - ) - ), + renderCardTop(msg.key, url.link, { + author: authorIdForName, + authorName: msg.value?.meta?.author?.name, + date: iso, + chips: [div({ class: "card-chips-row" }, + kindLabel ? span({ class: "pm-exposition-chip pm-exposition-whole" }, span({ class: "pm-exposition-text" }, String(kindLabel).toUpperCase())) : null, + isPrivate ? renderStateChip("encrypted", "๐Ÿ”’", i18n.encryptedChipLabel || "E2E") : null, + ...(isPrivate ? recps : []) + )], + spread: { count: spreadCount, alreadySpread }, + reportTitle: cardTitle + }), articleContent, - footer( - div( - form( - { action: url.spreadForm, method: "post" }, - button( - { - type: "submit", - class: spreadButtonClass, - title: i18n.spreadContent, - }, - `โ˜‰ ${spreadCount}` - ) - ), - a({ href: url.comment }, i18n.comment), - isPrivate || isRoot || isFork - ? null - : a({ href: url.subtopic }, nbsp, i18n.subtopic) - ), - br() + renderCardMetaRow( + a({ href: url.comment, class: "comments-summary-link" }, span({ class: "comments-summary-icon" }, "โœ‘"), span(i18n.comment)), + isPrivate || isRoot || isFork + ? null + : a({ href: url.subtopic, class: "comments-summary-link" }, span(i18n.subtopic)), + renderShareButton(msg.key, url.link, cardTitle) ) ); @@ -3352,11 +3776,11 @@ const buildClearnetHub = ({ items = {}, filterBase, filterType = '', query = '', ${blob ? `` : ''}
${kind ? `
${kind}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} ${it.title ? `
${title}
` : ''} ${author} ${player} ${snippet ? `
${snippet}${raw.length > 300 ? 'โ€ฆ' : ''}
` : ''} - ${chipRow ? `
${chipRow}
` : ''} ${renderTagChips(it.tags)}
`; @@ -3419,7 +3843,8 @@ exports.clearnetHubView = async ({ authors = [], items = {}, tribes = [], filter ogDescription: i18n.cnHubDescription, extraCss, body, - headerExtra: renderClearnetHubSearch({ type: filterType, query }) + headerExtra: renderClearnetHubSearch({ type: filterType, query }), + feed: filterType ? `/c/rss/${encodeURIComponent(filterType)}` : '/c/rss' }); }; @@ -3452,10 +3877,10 @@ exports.clearnetInhabitantView = async ({ feedId, name, description, image, pref ${blob ? `` : ''}
${kind ? `
${kind}
` : ''} + ${chipRow ? `
${chipRow}
` : ''} ${it.title ? `
${title}
` : ''} ${preview} ${snippet ? `
${snippet}${raw.length > 300 ? 'โ€ฆ' : ''}
` : ''} - ${chipRow ? `
${chipRow}
` : ''} ${renderTagChips(it.tags)}
`; @@ -3781,7 +4206,7 @@ exports.authorView = async ({ const allowedTypes = new Set(); for (const k of enabledKeys) for (const t of keyToTypes[k]) allowedTypes.add(t); const channelsWithEpisodes = new Set(allActions.filter(a => a && a.type === 'podcastEpisode' && a.content && a.content.channel).map(a => String(a.content.channel))); - const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type) && (a.type !== 'podcast' || channelsWithEpisodes.has(String(a.rootId || a.id)) || channelsWithEpisodes.has(String(a.id)))); + const authorActions = allActions.filter(a => a && a.author === feedId && allowedTypes.has(a.type) && !(a.type === 'post' && a.content && a.content.root) && (a.type !== 'podcast' || channelsWithEpisodes.has(String(a.rootId || a.id)) || channelsWithEpisodes.has(String(a.id)))); const counts = {}; for (const k of enabledKeys) counts[k] = 0; for (const a of authorActions) { @@ -3824,12 +4249,6 @@ exports.authorView = async ({ : section(div({ class: "profile-layout profile-layout-single" }, sideColumn)); let html = template(i18n.profile, layout); - const hasDocument = Array.isArray(allActions) && allActions.some(a => a && a.author === feedId && a.type === 'document'); - if (hasDocument) { - html += ` - - `; - } return html; }; @@ -3972,7 +4391,6 @@ exports.commentView = async ( const renderMessage = (msg) => { const content = lodash.get(msg, "value.content", {}); const authorId = msg.value.author || "Anonymous"; - const createdAt = moment(msg.value.timestamp).format("YYYY/MM/DD HH:mm"); const mentionsText = content.text || ''; const isTribe = content.type === 'tribe-content'; const visitUrl = isTribe @@ -3988,23 +4406,20 @@ const renderMessage = (msg) => { const typeLabel = isTribe ? 'TRIBE' : 'POST'; return div({ class: 'card card-rpg mention-card' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${typeLabel}]`), - visitUrl - ? form({ method: 'GET', action: visitUrl, class: 'inline-form' }, - button({ type: 'submit', class: 'filter-btn' }, i18n.viewDetails || 'View details') - ) - : null - ), + renderCardTop(msg.key, visitUrl, { + author: authorId, + date: msg.value.timestamp, + chips: [div({ class: 'card-chips-row' }, + span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, typeLabel)), + badge + )], + reportTitle: mentionsText + }), div({ class: 'card-body' }, div({ class: 'card-section' }, - badge, p({ class: 'post-text' }, ...renderStyledText(mentionsText || '[No content]')) - ) - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${createdAt}`), - userLink(authorId) + ), + renderEngagement(msg.key, null, null, { href: visitUrl, title: mentionsText }) ) ); }; @@ -4036,7 +4451,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' const qMatch = (m) => { if (!qNorm) return true const c = m.value.content || {} - const ids = [c.from, m.value.author, ...(Array.isArray(c.to) ? c.to : [])].filter(Boolean) + const ids = [c.from, m.value.author, ...(Array.isArray(c.to) && !inboxBotOf(c) ? c.to : [])].filter(Boolean) return [c.subject, c.text, ...ids, ...ids.map(id => userLinkLabel(id))] .some(v => String(v || '').toLowerCase().includes(qNorm)) } @@ -4688,7 +5103,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' ), unreadShown.length ? form({ method: 'POST', action: '/inbox/read-all', class: 'inbox-vis-toggle' }, ...unreadShown.map(k => input({ type: 'hidden', name: 'keys', value: k })), - button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkRead || i18n.inboxMarkAllRead} (${unreadShown.length})`) ) : null, (bulkDeletable && shownKeys.length) ? form({ method: 'POST', action: '/inbox/delete-many', class: 'inbox-vis-toggle inbox-bulk-delete' }, ...shownKeys.map(k => input({ type: 'hidden', name: 'keys', value: k })), @@ -4714,7 +5129,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null, notice = ' const text = content.text || '' const sentAt = new Date(content.sentAt || msg.timestamp) const fromResolved = author - const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : [] + const toLinks = isNotification(msg) ? [linkAuthor(config.keys.id)] : Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : [] const level = threadLevel(subjectRaw) const msgSize = msgSizeBytes(msg) @@ -4894,22 +5309,13 @@ exports.threadView = ({ messages, spreadMap = null, hiddenRoot = false }) => { const rootMessage = messages[0]; const rootAuthorName = rootMessage.value.meta.author.name; - const needsPdfViewer = Array.isArray(messages) && messages.some((m) => { - const t = String(m?.value?.content?.type || "").toLowerCase(); - return t === "document"; - }); - const tpl = template( [`@${rootAuthorName}`], hiddenRoot ? p({ class: 'muted hidden-note' }, i18n.hiddenByYou) : null, div(thread(messages, spreadMap)) ); - return `${tpl}${ - needsPdfViewer - ? `` - : "" - }`; + return tpl; }; exports.supportersView = async ({ supporters, feed, name }) => { diff --git a/src/views/maps_view.js b/src/views/maps_view.js index ff17622..0399435 100644 --- a/src/views/maps_view.js +++ b/src/views/maps_view.js @@ -4,11 +4,13 @@ const RAW_HTML = Symbol.for('oasis.rawHtml'); const { renderStyledText, renderStyledHtml, escapeHtml } = require("../backend/renderStyledText"); const moment = require("../server/node_modules/moment"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon } = require("./main_views"); +const { renderCommentsSection } = require("./comments_view"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderCardTop, renderSpreadEditWarning, renderDocumentActions, renderInviteQrCard, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, renderIcon, renderOpinionsVoting, renderEditedChip, renderMediaLockNote, renderEngagement, renderDetailShare } = require("./main_views"); const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); const { renderMapHtml, resolveView, makeView, fitView, parseView, viewParam, parsePick, MIN_ZOOM, MAX_ZOOM } = require("../maps/map_renderer"); const { searchPlaces, nearestPlace } = require("../maps/map_data"); +const mapIsOpen = (m) => !!m && !m.tribeId && !m.encrypted && !m.contentEncrypted && String(m.mapType || '').toUpperCase() === 'OPEN'; const userId = config.keys.id; const safeArr = (v) => (Array.isArray(v) ? v : []); @@ -256,7 +258,7 @@ const renderMapForm = (filter, mapId, mapToEdit, params = {}) => { textarea({ maxlength: "5000", name: "markerLabel", placeholder: i18n.mapMarkerLabelPlaceholder, rows: "3" }, markerLabelVal), br(), br(), label(i18n.markerImageLabel || "Marker Image"), br(), - input({ type: "file", name: "image", accept: "image/*" }), + (isEdit && renderMediaLockNote(mapToEdit)) || input({ type: "file", name: "image", accept: "image/*" }), br(), br(), label(i18n.mapLatLabel), br(), input({ type: "text", name: "lat", placeholder: i18n.mapLatPlaceholder, value: latVal }), @@ -415,12 +417,9 @@ const renderMapCard = exports.renderMapCard = (mapObj, filter, params = {}) => { const isOwn = mapObj.author && String(mapObj.author) === String(userId); const href = origin ? origin.href : `/maps/${encodeURIComponent(mapObj.key)}?filter=${encodeURIComponent(filter)}`; return div({ class: "tribe-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(mapObj.key, href, origin - ? { author: mapObj.author, reportTitle: mapObj.title } - : { spread: params.spreadMap && params.spreadMap.get(mapObj.key) || null, author: mapObj.author, favKind: 'maps', isFavorite: mapObj.isFavorite, reportTitle: mapObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('map', mapObj.key) : undefined }) - ), + renderCardTop(mapObj.key, href, origin + ? { author: mapObj.author, date: mapObj.createdAt, chips, reportTitle: mapObj.title } + : { date: mapObj.createdAt, chips, spread: params.spreadMap && params.spreadMap.get(mapObj.key) || null, author: mapObj.author, favKind: 'maps', isFavorite: mapObj.isFavorite, reportTitle: mapObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('map', mapObj.key) : undefined }), div({ class: "tribe-card-image-wrapper map-card-thumb" }, a({ href }, renderMap(thumbMarkers, null, 0, { thumb: true, singleZoom: 5 })) ), @@ -428,12 +427,12 @@ const renderMapCard = exports.renderMapCard = (mapObj, filter, params = {}) => { div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, mapObj.title || i18n.mapAllSectionTitle)) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, p({ class: "job-meta-line" }, `๐Ÿ“ ${mapObj.lat.toFixed(4)}, ${mapObj.lng.toFixed(4)}`), safeText(mapObj.description) ? p({ class: "tribe-card-description" }, ...renderStyledText(safeText(mapObj.description))) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.mapMarkersTitle || "Markers"}: ${markerCount}`) - ) + ), + null ) ); }; @@ -522,27 +521,32 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => { }) : null; const mapSide = div({ class: "tribe-side map-detail-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}`, { - author: mapObj.author, - favKind: 'maps', - isFavorite: mapObj.isFavorite, - spread: params.spreads || null, - returnTo, - reportTitle: mapObj.title, - deleteAction: String(mapObj.author) === String(userId) ? contentDeleteAction('map', mapObj.key) : undefined - }) - ), + renderCardTop(mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}`, { arrow: false, + date: mapObj.createdAt, + chips: [ + renderStateChip("whole", "๐Ÿ—บ", String(mapObj.mapType || "").toUpperCase()), + renderEncryptedChip(i18n), + renderLifespanChip(mapObj.lifetime, i18n), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? mapClearnetHref(mapObj) : null) : null + ], + bylineExtra: [ + renderEditedChip(mapObj.key, mapObj.rootId), + mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt + ? span({ class: "votations-comment-date" }, `${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null + ], + author: mapObj.author, + favKind: 'maps', + isFavorite: mapObj.isFavorite, + spread: params.spreads || null, + returnTo, + reportTitle: mapObj.title, + deleteAction: String(mapObj.author) === String(userId) ? contentDeleteAction('map', mapObj.key) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, mapObj.title || i18n.mapAllSectionTitle) ), - div({ class: "card-chips-row" }, - renderStateChip("whole", "๐Ÿ—บ", String(mapObj.mapType || "").toUpperCase()), - renderEncryptedChip(i18n), - renderLifespanChip(mapObj.lifetime, i18n), - canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? mapClearnetHref(mapObj) : null) : null, - canClearnet && String(mapObj.author) === String(userId) ? renderClearnetSwitch("maps", mapObj.rootId || mapObj.key, isClearnet) : null - ), + canClearnet && String(mapObj.author) === String(userId) ? renderClearnetSwitch("maps", mapObj.rootId || mapObj.key, isClearnet) : null, safeText(mapObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(safeText(mapObj.description))) : null, table({ class: "tribe-info-table jobs-info-table" }, tr( @@ -552,13 +556,6 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => { tr( td({ class: "tribe-info-label" }, i18n.mapLngLabel), td({ class: "tribe-info-value", colspan: "3" }, mapObj.lng.toFixed(6)) - ), - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(mapObj.author)) ) ), renderTags(mapObj.tags), @@ -607,14 +604,13 @@ exports.singleMapView = async (mapObj, filter = "all", params = {}) => { })); const mapMain = div({ class: "tribe-main map-detail-main" }, + renderDetailShare(mapObj.rootId || mapObj.key, `/maps/${encodeURIComponent(mapObj.key)}`, mapObj.title), renderMarkersList(mapObj.markers, mapObj, { focus, view, viewHref, returnTo: pageReturnTo }), - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(mapObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(mapObj.author), - mapObj.updatedAt && mapObj.updatedAt !== mapObj.createdAt - ? span({ class: "votations-comment-date" }, ` ยท ${i18n.mapUpdatedAt}: ${moment(mapObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) - : null), - renderMarkerForm(mapObj, returnTo, { ...params, filter }, tribeMembers, { mkLat, mkLng, view, focus }) + renderMarkerForm(mapObj, returnTo, { ...params, filter }, tribeMembers, { mkLat, mkLng, view, focus }), + renderEngagement(mapObj.rootId || mapObj.key, + params.opinions ? renderOpinionsVoting('/maps/opinions', mapObj.rootId || mapObj.key, params.opinions.opinions, `/maps/${encodeURIComponent(mapObj.rootId || mapObj.key)}`, params.opinions.opinions_inhabitants) : null, + Array.isArray(params.comments) ? renderCommentsSection({ action: `/maps/${encodeURIComponent(mapObj.rootId || mapObj.key)}/comments`, comments: params.comments, returnTo: `/maps/${encodeURIComponent(mapObj.rootId || mapObj.key)}` }) : null + ) ); return template(mapObj.title || i18n.mapTitle, diff --git a/src/views/market_view.js b/src/views/market_view.js index 99098c2..0936b0f 100644 --- a/src/views/market_view.js +++ b/src/views/market_view.js @@ -1,25 +1,28 @@ const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, table, tr, th, td, progress, video, audio, hr } = require("../server/node_modules/hyperaxe") const { renderZoomableImage } = require("./gallery_view") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement , renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") -const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderVisibilityChip, renderLifespanChip, renderEcoTax, renderSpreadEditWarning, renderOpinionsVoting, renderEngagement, renderCardTop, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderEncryptedChip } = require("./clearnet_view") const opinionCategories = require("../backend/opinion_categories") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderStyledText } = require("../backend/renderStyledText") -const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") +const { renderMapEmbedWithZoom } = require("./maps_view") -const renderMediaBlob = (value, fallbackSrc = null, attrs = {}) => { +const renderMediaBlob = (value, fallbackSrc = null, attrs = {}, zoom = true) => { if (!value) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null const s = String(value).trim() if (!s) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null - if (s.startsWith('&')) return renderZoomableImage(`/blob/${encodeURIComponent(s)}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + const picture = (src) => zoom + ? renderZoomableImage(src, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + : img({ loading: 'lazy', src, class: attrs.class || 'post-image', alt: attrs.alt || '' }) + if (s.startsWith('&')) return picture(`/blob/${encodeURIComponent(s)}`) const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mVideo) return video({ controls: true, class: attrs.class || 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}`, preload: 'metadata' }) const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) if (mAudio) return audio({ controls: true, class: attrs.class || 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` }) const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/) - if (mImg) return renderZoomableImage(`/blob/${encodeURIComponent(mImg[1])}`, { imgClass: attrs.class || 'post-image', alt: attrs.alt || '' }) + if (mImg) return picture(`/blob/${encodeURIComponent(mImg[1])}`) return fallbackSrc ? img({ src: fallbackSrc, ...attrs }) : null } @@ -408,7 +411,7 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { br(), label(i18n.marketCreateFormImageLabel), br(), - input({ type: "file", name: "image", id: "image" }), + (filter === "edit" && renderMediaLockNote(itemEdit)) || input({ type: "file", name: "image", id: "image" }), br(), br(), label(i18n.marketItemStatus), @@ -475,23 +478,12 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { filtered.length > 0 ? paged(filtered).map((item) => { const isOwner = String(item.seller) === String(userId) + const itemHref = `/market/${encodeURIComponent(item.id)}` return div({ class: "tribe-card market-tribe-card" }, - div({ class: "tribe-card-image-wrapper" }, - a({ href: `/market/${encodeURIComponent(item.id)}` }, - renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' }) - ) - ), - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: (params.spreadMap && params.spreadMap.get(item.id)) || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwner ? contentDeleteAction('market', item.id) : undefined }) - ), - div({ class: "tribe-card-body" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, a({ href: `/market/${encodeURIComponent(item.id)}` }, item.title)) - ), - renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), - div({ class: "card-chips-row" }, + renderCardTop(item.id, itemHref, { + date: item.createdAt, + chips: [ String(item.visibility || "PUBLIC").toUpperCase() === "HIDDEN" ? renderVisibilityChip("HIDDEN", i18n) : null, renderStateChip("encrypted", "", String(item.item_type || "").toUpperCase()), item.item_status ? renderStateChip("whole", "", String(item.item_status).toUpperCase()) : null, @@ -499,8 +491,21 @@ exports.marketView = async (items, filter, itemToEdit = null, params = {}) => { item.industry ? a({ href: `/industry/${encodeURIComponent(item.industry)}` }, renderStateChip("whole", "๐Ÿญ", String(i18n.industryTitle || "Industry").toUpperCase())) : null, renderLifespanChip(item.lifetime, i18n), item.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref("market", item.title, item.id)) : null + ], + spread: (params.spreadMap && params.spreadMap.get(item.id)) || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo, deleteAction: isOwner ? contentDeleteAction('market', item.id) : undefined + }), + div({ class: "tribe-card-image-wrapper" }, + a({ href: itemHref }, + renderMediaBlob(item.image, '/assets/images/default-market.png', { class: 'tribe-card-hero-image' }, false) + ) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href: itemHref }, item.title)) ), - div({ class: "price-chip" }, `${item.price} ECO`) + renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), + div({ class: "price-chip" }, `${item.price} ECO`), + null ) ) }) @@ -573,7 +578,6 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { td({ class: "tribe-info-label" }, labelText), td({ class: "tribe-info-value" }, valueNode) )) - pushRow(i18n.marketItemSeller, userLink(item.seller)) if (item.shopId && item.shopTitle) pushRow(i18n.marketShopLabel || "Shop", a({ href: `/shops/${encodeURIComponent(item.shopId)}`, class: "user-link" }, item.shopTitle)) if (item.deadline) pushRow(i18n.marketItemAvailable, moment(item.deadline).format("YYYY/MM/DD HH:mm")) @@ -597,19 +601,17 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { const marketActions = String(item.seller) === String(userId) ? renderMarketOwnerActions(item, `/market/${encodeURIComponent(item.id)}`) : [] const itemSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(item.id, `/market/${encodeURIComponent(item.id)}`, { spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo: `/market/${encodeURIComponent(item.id)}`, deleteAction: String(item.seller) === String(userId) ? contentDeleteAction('market', item.id) : undefined }) - ), + renderCardTop(item.id, `/market/${encodeURIComponent(item.id)}`, { arrow: false, date: item.createdAt, chips, bylineExtra: [renderEditedChip(item.id, item.rootId)], spread: params.spreads || null, author: item.seller, favKind: 'market', isFavorite: item.isFavorite, reportTitle: item.title, returnTo: `/market/${encodeURIComponent(item.id)}`, deleteAction: String(item.seller) === String(userId) ? contentDeleteAction('market', item.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.title) ), renderStarRating(item.opinions, Array.isArray(item.opinions_inhabitants) ? item.opinions_inhabitants.length : 0), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, String(item.seller) === String(userId) ? renderClearnetSwitch("market", item.rootId || item.id, isClearnet) : null, renderMediaBlob(item.image, "/assets/images/default-market.png"), - div({ class: "price-chip" }, `${item.price} ECO`), + renderDetailShare(item.id, `/market/${encodeURIComponent(item.id)}`, item.title), renderStockBar(item.stock, maxStock), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), + renderCountdownField(item), + infoRows.length ? table({ class: "tribe-info-table jobs-info-table" }, ...infoRows) : null, tagsNode, visibilityRow, marketActions.length ? div({ class: "tribe-side-actions" }, ...marketActions) : null @@ -622,8 +624,8 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { p({ class: "tribe-side-description" }, ...renderStyledText(item.description)) ) : null, - renderCountdownField(item), item.mapUrl ? div({ class: "job-section" }, renderMapEmbedWithZoom(params.mapData, item.mapUrl, `/market/${encodeURIComponent(item.id)}`, params.zoom)) : null, + showBuy ? null : div({ class: "price-chip" }, `${item.price} ECO`), item.item_type === "auction" ? div( { class: "auction-info job-section" }, @@ -650,20 +652,46 @@ exports.singleMarketView = async (item, filter, comments = [], params = {}) => { : null, showBuy ? div( - { class: "market-item actions" }, + { class: "job-section buy-box" }, form( { method: "POST", action: `/market/buy/${encodeURIComponent(item.id)}`, class: "market-buy-form" }, input({ type: "hidden", name: "returnTo", value: "/inbox?filter=sent" }), + div({ class: "title-with-chip" }, label(i18n.shopBuyDeliveryAddress), renderEncryptedChip(i18n)), + textarea({ maxlength: "5000", name: "deliveryAddress", rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder, ...(item.includesShipping ? { required: true } : {}) }), + br(), + label(i18n.shopBuyContact), + br(), + input({ type: "text", name: "contact", placeholder: i18n.shopBuyContactPlaceholder }), + br(), + label(i18n.shopBuyNotes), + br(), + textarea({ maxlength: "5000", name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder }), + br(), + div({ class: "price-chip" }, `${item.price} ECO`), + br(), + br(), button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy) ) ) : null, + Array.isArray(params.orders) && params.orders.length + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.shopOrdersTitle), + ...params.orders.map(o => div({ class: "shop-order-card card-section" }, + div({ class: "card-chips-row" }, renderEncryptedChip(i18n)), + div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopOrderBuyer}:`), userLink(o.buyer)), + o.deliveryAddress ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyDeliveryAddress}:`), span({ class: "card-value" }, String(o.deliveryAddress))) : null, + o.contact ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyContact}:`), span({ class: "card-value" }, String(o.contact))) : null, + o.notes ? div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopBuyNotes}:`), span({ class: "card-value" }, String(o.notes))) : null, + p({ class: "card-footer" }, span({ class: "date-link" }, moment(o.ts).format("YYYY/MM/DD HH:mm"))) + )) + ) + : null, renderEngagement(item.id, String(item.seller) !== String(userId) && item.purchasedByViewer ? renderOpinionsVoting("/market/opinions", item.id, item.opinions, returnTo, item.opinions_inhabitants) : null, - renderMarketCommentsSection(item.id, returnTo, comments) - ) + renderMarketCommentsSection(item.id, returnTo, comments)) ) return div({ class: "tribe-details" }, itemSide, itemMain) diff --git a/src/views/melody_view.js b/src/views/melody_view.js index fc82391..30d21b6 100644 --- a/src/views/melody_view.js +++ b/src/views/melody_view.js @@ -1,8 +1,7 @@ "use strict"; const { div, h2, p, section, audio, source, span, a, form, input, button, br, textarea, label } = require("../server/node_modules/hyperaxe"); -const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderEcoTax, renderContentActions, paged } = require("./main_views"); +const { template, i18n, userLink, renderCardTop, renderEngagement, paged } = require("./main_views"); const renderSequenceList = (sequence, availableIds = null) => { if (!sequence || sequence.length === 0) { @@ -98,17 +97,12 @@ const renderBcsList = (bcsAudios) => { } return div({ class: "melody-bcs-list" }, bcsAudios.map(item => div({ class: "melody-bcs-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(item.key, `/audios/${encodeURIComponent(item.key)}`, { author: item.author, favKind: 'audios', isFavorite: item.isFavorite, torrentFrom: { blobId: item.url, name: item.title }, reportTitle: item.title }) - ), - div({ class: "melody-bcs-head" }, - item.title ? h2({ class: "melody-bcs-title" }, item.title) : null, - div({ class: "melody-bcs-meta" }, - userLink(item.author), - span({ class: "melody-bcs-date" }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")) - ) - ), + renderCardTop(item.key, `/audios/${encodeURIComponent(item.key)}`, { date: item.createdAt, author: item.author, favKind: 'audios', isFavorite: item.isFavorite, torrentFrom: { blobId: item.url, name: item.title }, reportTitle: item.title }), + item.title + ? div({ class: "melody-bcs-head" }, + h2({ class: "melody-bcs-title" }, item.title) + ) + : null, item.url ? div({ class: "audio-container melody-bcs-player" }, audio({ controls: true, preload: "metadata", src: `/blob/${encodeURIComponent(item.url)}` }), @@ -121,7 +115,8 @@ const renderBcsList = (bcsAudios) => { button({ type: "submit", class: "filter-btn" }, i18n.audioTranscodeButton || "TRANSCODE") ) ), - item.description ? p({ class: "melody-bcs-desc" }, item.description) : null + item.description ? p({ class: "melody-bcs-desc" }, item.description) : null, + null )) ); }; diff --git a/src/views/mentions_view.js b/src/views/mentions_view.js index 3ef4313..30acc17 100644 --- a/src/views/mentions_view.js +++ b/src/views/mentions_view.js @@ -1,7 +1,6 @@ const { div, h2, p, section, button, form, input, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged } = require("./main_views"); -const { getViewDetailsAction } = require("./activity_view"); -const moment = require("../server/node_modules/moment"); +const { template, i18n, renderCardTop, renderShareButton, renderCardMetaRow, renderModuleStats, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged } = require("./main_views"); +const { getViewDetailsAction, commentHref, commentLabel } = require("./activity_view"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText"); @@ -12,45 +11,43 @@ const excerpt = (text, max = 320) => { return s.length > max ? s.slice(0, max) + "โ€ฆ" : s; }; -const typeLabel = (type) => i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || String(type || '').toUpperCase(); +const typeLabel = (type) => (type === 'comment' ? i18n.typeComment : i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)]) || String(type || '').toUpperCase(); const renderMentionCard = (item, readKeys, params = {}) => { - const href = getViewDetailsAction(item.type, { id: item.id, key: item.id, author: item.author, content: item.content }); + const href = item.type === 'comment' + ? commentHref(item.comment || { rootId: item.content && item.content.root }, item.id) + : getViewDetailsAction(item.type, { id: item.id, key: item.id, author: item.author, content: item.content }); const isOwn = String(item.author) === String(userId); const read = readKeys.has(String(item.key)); return div({ class: "trending-card mention-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - div({ class: 'card-chips-row' }, + renderCardTop(item.id, href, { + date: item.createdAt, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, - span({ class: 'pm-exposition-text' }, typeLabel(item.type)) + span({ class: 'pm-exposition-text' }, item.comment ? String(commentLabel(item.comment)).toUpperCase() : typeLabel(item.type)) ), read ? null : span({ class: 'pm-exposition-chip pm-unread-chip' }, span({ class: 'pm-exposition-text' }, i18n.inboxUnreadChip)) - ), - renderContentActions(item.id, href, { - author: item.author, - reportTitle: item.title || item.text, - spread: CONTENT_SPREADABLE.has(item.type) ? ((params.spreadMap && params.spreadMap.get(item.id)) || null) : undefined, - ...(item.type === 'torrent' || item.type === 'file' ? { spreadTitle: i18n.seedAction } : {}), - ...(CONTENT_TORRENTABLE.has(item.type) && item.content && typeof item.content.url === 'string' && item.content.url.startsWith('&') - ? { torrentFrom: { blobId: item.content.url, name: item.content.title || item.content.fileName || '' } } - : {}) - }) - ), + )], + author: item.author, + reportTitle: item.title || item.text, + spread: CONTENT_SPREADABLE.has(item.type) ? ((params.spreadMap && params.spreadMap.get(item.id)) || null) : undefined, + ...(item.type === 'torrent' || item.type === 'file' ? { spreadTitle: i18n.seedAction } : {}), + ...(CONTENT_TORRENTABLE.has(item.type) && item.content && typeof item.content.url === 'string' && item.content.url.startsWith('&') + ? { torrentFrom: { blobId: item.content.url, name: item.content.title || item.content.fileName || '' } } + : {}) + }), div({ class: "card-section mention-card-body" }, item.title ? div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, item.title)) : null, item.text ? p({ class: "mention-text" }, ...renderStyledText(excerpt(item.text))) : null, - p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(item.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(item.author) - ), div({ class: "pm-actions mention-actions" }, form({ method: "POST", action: `/mentions/${read ? 'unread' : 'read'}/${encodeURIComponent(item.key)}`, class: "pm-action-form" }, button({ type: "submit", class: "pm-btn read-btn" }, String(read ? i18n.inboxMarkUnread : i18n.inboxMarkRead).toUpperCase()) ) ) - ) + ), + renderCardMetaRow(renderShareButton(item.id, href, item.title || item.text)) ); }; @@ -83,7 +80,7 @@ exports.mentionsView = async (items = [], filter = 'ALL', params = {}) => { renderModuleStats(items.length), unreadShown.length ? form({ method: "POST", action: "/mentions/read-all", class: "inbox-vis-toggle" }, ...unreadShown.map(k => input({ type: "hidden", name: "keys", value: k })), - button({ type: "submit", class: "btn" }, `${i18n.inboxMarkAllRead} (${unreadShown.length})`) + button({ type: "submit", class: "btn" }, `${i18n.inboxMarkRead || i18n.inboxMarkAllRead} (${unreadShown.length})`) ) : null, form({ method: "GET", action: "/mentions", class: "filter-box" }, input({ type: "hidden", name: "filter", value: filter }), diff --git a/src/views/mobile_ui.js b/src/views/mobile_ui.js index c073464..3501640 100644 --- a/src/views/mobile_ui.js +++ b/src/views/mobile_ui.js @@ -153,8 +153,8 @@ const uxPick = (action, cfg, withHint) => { }; // perfil de uso (workflow), que en el movil es el skin: un boton por opcion con -// para que sirve, cuantos modulos enciende y su barra de abajo. Van de menos a mas -// modulos: Activista (accion directa) el primero +// para que sirve, cuantos modulos enciende y su barra de abajo. Primero Minimo, +// Acampada y Activista (fork_workflows.LADDER); el resto de menos a mas modulos const workflowName = (k) => { const i18n = t(); if (k === 'full') return i18n.welcomeWorkflow_full || k; @@ -164,7 +164,8 @@ const workflowPick = (action, current, keys, withHint) => { const i18n = t(); const fw = require('../models/fork_workflows'); const { PINNABLE_MODULES } = require('./main_views'); - const list = keys.slice().sort((x, y) => fw.moduleCount(x) - fw.moduleCount(y)); + const rank = (k) => { const i = fw.LADDER.indexOf(k); return i === -1 ? fw.LADDER.length : i; }; + const list = keys.slice().sort((x, y) => rank(x) - rank(y) || fw.moduleCount(x) - fw.moduleCount(y)); const barOf = (k) => (fw.BARS[k] || []).map(key => (PINNABLE_MODULES || []).find(m => m.key === key)).filter(Boolean); return form({ method: 'POST', action, class: 'ux-pick wf-pick' }, withHint ? p({ class: 'ux-pick-hint' }, i18n.uxPickHint || 'Tap one and it is saved. You can change it any time in Settings.') : null, @@ -195,15 +196,19 @@ const welcomeSteps = (usable) => { // ---------- actividad ---------- -// las fichas de tipo de actividad (de tres en tres, varias filas antes de la primera -// tarjeta) van plegadas tras un boton Filtrar; por defecto se ve todo. Con un filtro -// puesto, el boton dice cual. Sin JS, con
. En escritorio, las fichas tal cual +// las fichas de tipo de actividad van plegadas tras un boton Filtrar; por defecto se +// ve todo y con un filtro puesto el boton dice cual. Desde la 1.3.5 la cabecera del dev +// (apkModbar en main_views) se lleva la primera caja .activity-filter-chips a su barra +// fija: por eso el boton (casilla + etiqueta) y las fichas van dentro de esa misma caja +// y viajan juntos. Sin JS. En escritorio, las fichas tal cual const activityFilter = (filter, types, chips) => { if (!isMobile()) return chips; const i18n = t(); const cur = filter && filter !== 'all' ? ((types || []).find(x => x.type === filter) || {}).label : null; - return details({ class: cur ? 'activity-filter is-filtered' : 'activity-filter' }, - summary({ class: 'activity-filter-toggle' }, + chips['class'] = 'mobile-filter-chips'; + return div({ class: cur ? 'activity-filter-chips mobile-filter is-filtered' : 'activity-filter-chips mobile-filter' }, + input({ type: 'checkbox', id: 'mobile-filter-toggle', class: 'mobile-filter-toggle', autocomplete: 'off' }), + label({ for: 'mobile-filter-toggle', class: 'mobile-filter-btn' }, span({ class: 'activity-filter-name' }, i18n.activityFilter || 'Filter'), cur ? span({ class: 'activity-filter-now' }, cur) : null), chips diff --git a/src/views/modules_view.js b/src/views/modules_view.js index c558559..58efeef 100644 --- a/src/views/modules_view.js +++ b/src/views/modules_view.js @@ -149,7 +149,7 @@ const modulesView = () => { ), div( { class: "save-button-container" }, - button({ type: "submit", class: "submit-button" }, i18n.saveSettings) + button({ type: "submit" }, i18n.saveSettings) ) ) ) diff --git a/src/views/opinions_view.js b/src/views/opinions_view.js index cb020b5..0413076 100644 --- a/src/views/opinions_view.js +++ b/src/views/opinions_view.js @@ -1,7 +1,7 @@ -const { div, h2, p, section, button, form, a, img, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br, span, details, summary } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, video: videoHyperaxe, audio: audioHyperaxe, input, table, tr, th, td, br, span, details, summary } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { template, i18n, userLink, renderCardTop, renderShareButton, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderCommentsSummary, commentsOpenHref, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged, renderPdfDocument } = require('./main_views'); const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); const { renderStyledText, renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); @@ -9,6 +9,9 @@ const opinionCategories = require('../backend/opinion_categories'); const { sanitizeHtml } = require('../backend/sanitizeHtml'); +const opinionFilterLabel = (mode) => + String(i18n[mode + 'Button'] || i18n['vote' + mode.charAt(0).toUpperCase() + mode.slice(1)] || mode).toUpperCase(); + const detailHref = (type, key) => { switch (type) { case 'bookmark': return `/bookmarks/${encodeURIComponent(key)}`; @@ -35,6 +38,10 @@ const detailHref = (type, key) => { case 'podcastEpisode': return `/podcasts/episode/${encodeURIComponent(key)}`; case 'campaign': return `/campaigns/${encodeURIComponent(key)}`; case 'logisticsRoute': return `/logistics/${encodeURIComponent(key)}`; + case 'wikiPage': return `/wiki/${encodeURIComponent(key)}`; + case 'map': return `/maps/${encodeURIComponent(key)}`; + case 'industryBlueprint': return `/industry/blueprint/${encodeURIComponent(key)}`; + case 'job': return `/jobs/${encodeURIComponent(key)}`; default: return null; } }; @@ -260,7 +267,7 @@ const renderContentHtml = (content, key) => { ] : null, div({ class: 'card-field' }, - div({ class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(content.url)}` }) + renderPdfDocument(`/blob/${encodeURIComponent(content.url)}`, { title: content.title }) ) ) ); @@ -395,28 +402,28 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = const voteEntries = Object.entries(c.opinions || {}); const total = voteEntries.reduce((sum, [, v]) => sum + v, 0); const voted = c.opinions_inhabitants?.includes(config.keys.id); - const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); const allCats = opinionCategories; const isOwn = item.value.author && String(item.value.author) === String(config.keys.id); + const commentTotal = params.commentCounts instanceof Map ? (params.commentCounts.get(key) || 0) : 0; + const shareTitle = c.title || c.question || c.concept || c.name || c.contentWarning || ''; return div( { class: 'trending-card opinions-card' + (isOwn ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - span({ class: 'pm-exposition-chip pm-exposition-whole' }, + renderCardTop(key, detailHref(c.type, key), { + date: item.value.timestamp, + chips: [span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(c.type === 'post' ? (i18n.typePost || 'BLOG') : (c.type || '')).toUpperCase()) - ), - renderContentActions(key, detailHref(c.type, key), { - spread: spreadMap.get(key) || null, - author: item.value.author, - reportTitle: c.title || c.question || c.concept || c.name || c.contentWarning || '', - ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') - ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } - : {}), - ...(CONTENT_FAV_KIND[c.type] - ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: `/opinions?filter=${encodeURIComponent(filter)}` } - : {}) - }) - ), + )], + spread: spreadMap.get(key) || null, + author: item.value.author, + reportTitle: shareTitle, + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: `/opinions?filter=${encodeURIComponent(filter)}` } + : {}) + }), div( { class: 'card-section opinions-card-body' }, contentHtml @@ -424,7 +431,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = renderVotesSummary(c.opinions), renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, + summary({ class: Object.values(c.opinions || {}).some(n => Number(n) > 0) ? 'opinions-summary engage-on' : 'opinions-summary' }, span({ class: 'opinions-summary-icon' }, '๊”'), span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div({ class: 'voting-buttons' }, @@ -439,16 +446,13 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = }) ) ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created}`), - userLink(item.value.author) - ) + renderCommentsSummary(commentsOpenHref(detailHref(c.type, key), commentTotal), commentTotal), + renderShareButton(key, detailHref(c.type, key), shareTitle) ) ); }) .filter(Boolean); - const hasDocuments = items.some(item => item.value.content?.type === 'document'); const header = div({ class: 'tags-header module-header-line' }, h2(title), p(i18n.shareYourOpinions) @@ -463,7 +467,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = baseFilters.map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -471,7 +475,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.positive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -479,7 +483,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.positive.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -487,7 +491,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.positive.slice(10, 15).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ) @@ -496,7 +500,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.constructive.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -504,7 +508,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.constructive.slice(5, 11).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -512,7 +516,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.moderation.slice(0, 5).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ), @@ -520,7 +524,7 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = opinionCategories.moderation.slice(5, 10).filter(mode => presentCats.has(mode)).map(mode => form({ method: 'GET', action: '/opinions' }, input({ type: 'hidden', name: 'filter', value: mode }), - button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, String(i18n[mode + 'Button'] || mode).toUpperCase()) + button({ type: 'submit', class: filter === mode ? 'filter-btn active' : 'filter-btn' }, opinionFilterLabel(mode)) ) ) ) @@ -543,8 +547,6 @@ exports.opinionsView = (items, filter, spreadMap = new Map(), q = '', allItems = ) ); - return `${html}${hasDocuments - ? `` - : ''}`; + return html; }; diff --git a/src/views/pads_view.js b/src/views/pads_view.js index 26eae79..4d212c5 100644 --- a/src/views/pads_view.js +++ b/src/views/pads_view.js @@ -1,5 +1,5 @@ -const { div, h2, h3, h4, p, section, button, form, a, span, br, textarea, input, label, select, option, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe") -const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton , renderContentActions, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views") +const { div, h2, h4, p, section, button, form, a, span, textarea, input, label, select, option, table, tr, td, details, summary } = require("../server/node_modules/hyperaxe") +const { template, i18n, userLink, renderStateChip, renderLifespanChip, renderCardTop, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderDetailShare } = require("./main_views") const { renderEncryptedChip } = require("./clearnet_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -109,24 +109,20 @@ const renderPadCard = exports.renderPadCard = (pad, filter, spreadInfo) => { ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) : (pad.subscriptionIn === false ? renderStateChip("closed", "โœ‰", i18n.subscriptionOff) : null) ].filter(Boolean) + const href = `/pads/${encodeURIComponent(pad.rootId)}` return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(pad.rootId || pad.key, `/pads/${encodeURIComponent(pad.rootId)}`, { spread: spreadInfo || null, author: pad.author, favKind: 'pads', isFavorite: pad.isFavorite, reportTitle: pad.title, deleteAction: String(pad.author) === String(userId) ? contentDeleteAction('pad', pad.rootId || pad.key) : undefined }) - ), + renderCardTop(pad.rootId || pad.key, href, { date: pad.createdAt, chips, spread: spreadInfo || null, author: pad.author, favKind: 'pads', isFavorite: pad.isFavorite, reportTitle: pad.title, deleteAction: String(pad.author) === String(userId) ? contentDeleteAction('pad', pad.rootId || pad.key) : undefined }), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/pads/${encodeURIComponent(pad.rootId)}` }, pad.title || "\u2014") + a({ href }, pad.title || "\u2014") ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - pad.deadline - ? p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${moment(pad.deadline).format("YYYY/MM/DD HH:mm")}`) - : null, + p({ class: "job-meta-line" }, `${i18n.padDeadlineLabel || "Deadline"}: ${pad.deadline ? moment(pad.deadline).format("YYYY/MM/DD HH:mm") : "-"}`), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.padMembersLabel || "Members"}: ${pad.members.length}`) - ) + ), + null ) ) } @@ -288,32 +284,25 @@ exports.singlePadView = async (pad, entries, params) => { ].filter(Boolean) const padSide = div({ class: "tribe-side" }, - div({ class: "tribe-side-actions wiki-actions-top pad-actions-top" }, - span({ class: "wiki-actions-left" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(pad.rootId, returnTo, { - author: pad.author, - favKind: 'pads', - isFavorite: pad.isFavorite, - spread: (params && params.spreads) || null, - returnTo, - reportTitle: pad.title, - deleteAction: isAuthor ? contentDeleteAction('pad', pad.rootId) : undefined - }) - ) - ), - ...topButtons - ), + renderCardTop(pad.rootId, returnTo, { arrow: false, + date: pad.createdAt, + chips: detailChips, + author: pad.author, + favKind: 'pads', + isFavorite: pad.isFavorite, + spread: (params && params.spreads) || null, + returnTo, + reportTitle: pad.title, + deleteAction: isAuthor ? contentDeleteAction('pad', pad.rootId) : undefined + }), + topButtons.length ? div({ class: "tribe-side-actions wiki-actions-top pad-actions-top" }, ...topButtons) : null, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pad.title || "\u2014") ), - detailChips.length ? div({ class: "card-chips-row" }, ...detailChips) : null, - div({ class: "pad-meta" }, - span({ class: "card-label" }, `${i18n.padCreated || "Created"}: `), - span(moment(pad.createdAt).format("YYYY/MM/DD HH:mm")), - (isRestrictedInviteOnly || !pad.deadline) ? null : span({ class: "card-label" }, `${i18n.padDeadlineLabel || "Deadline"}: `), - (isRestrictedInviteOnly || !pad.deadline) ? null : span(moment(pad.deadline).format("YYYY/MM/DD HH:mm")), - isRestrictedInviteOnly ? null : userLink(pad.author) + renderDetailShare(pad.rootId, returnTo, pad.title), + (isRestrictedInviteOnly || !pad.deadline) ? null : div({ class: "pad-meta" }, + span({ class: "card-label" }, `${i18n.padDeadlineLabel || "Deadline"}: `), + span(moment(pad.deadline).format("YYYY/MM/DD HH:mm")) ), tags, (isRestrictedInviteOnly || !inviteActions.length) ? null : div({ class: "tribe-side-actions" }, ...inviteActions), @@ -337,7 +326,8 @@ exports.singlePadView = async (pad, entries, params) => { div({ class: "pad-members-chips" }, ...pad.members.map(m => span({ class: "pad-member-chip" }, span({ class: "pad-author-swatch " + memberColorClass(pad.members, m) }), userLink(m))) ) - ) + ), + null ) let canonicalEntries = entries diff --git a/src/views/parliament_view.js b/src/views/parliament_view.js index 0558668..ee04564 100644 --- a/src/views/parliament_view.js +++ b/src/views/parliament_view.js @@ -1,8 +1,17 @@ const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe'); const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, paged } = require('./main_views'); +const { template, i18n, userLink, paged, renderEngagement, renderCardTop } = require('./main_views'); const { renderStyledText } = require('../backend/renderStyledText'); +const { renderCommentsSection } = require('./comments_view'); + +const commentsBlock = (comments, item, returnTo, title) => { + const root = item && item.rootId; + const commentsNode = root && comments instanceof Map ? renderCommentsSection({ action: `/parliament/comments/${encodeURIComponent(root)}`, comments: comments.get(root) || [], returnTo }) : null; + return renderEngagement(root || (item && item.id), null, commentsNode, { href: returnTo, title }); +}; + +const parliamentTop = (item, href, date) => renderCardTop(null, href, { author: item.proposer, date: date || item.createdAt }); const { seatPositions } = require('../models/parliament_model').hemicycle; @@ -478,7 +487,7 @@ const ProposalForm = () => ) ); -const ProposalsList = (proposals) => { +const ProposalsList = (proposals, comments = null) => { if (!proposals || !proposals.length) return null; const cards = proposals.map(pItem => { const titleNode = pItem && pItem.voteId @@ -489,17 +498,7 @@ const ProposalsList = (proposals) => { : (i18n.parliamentProposalOnTrackNo || 'BELOW THRESHOLD'); return div( { class: 'card' }, - br(), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), - span({ class: 'card-value' }, fmt(pItem.createdAt)) - ), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), - span({ class: 'card-value' }, userLink(pItem.proposer)) - ), + parliamentTop(pItem, '/parliament?filter=proposals'), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), @@ -550,7 +549,8 @@ const ProposalsList = (proposals) => { { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) - : null + : null, + commentsBlock(comments, pItem, '/parliament?filter=proposals', pItem.title || '') ); }); return div( @@ -565,13 +565,12 @@ const FutureLawsList = (rows) => { const cards = rows.map(pItem => div( { class: 'card' }, - br(), + parliamentTop(pItem, '/parliament?filter=proposals'), div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))), h2(pItem.title || ''), p(...renderStyledText(pItem.description || '')), - ...campaignBlock(pItem) + ...campaignBlock(pItem), + commentsBlock(null, pItem, '/parliament?filter=proposals', pItem.title || '') ) ); return div( @@ -619,20 +618,7 @@ const RevocationsList = (revocations) => { : (i18n.parliamentProposalOnTrackNo || 'BELOW THRESHOLD'); return div( { class: 'card' }, - br(), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), - span({ class: 'card-value' }, fmt(pItem.createdAt)) - ), - div( - { class: 'card-field' }, - span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), - span( - { class: 'card-value' }, - userLink(pItem.proposer) - ) - ), + parliamentTop(pItem, '/parliament?filter=revocations'), div( { class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod + ': '), @@ -683,7 +669,8 @@ const RevocationsList = (revocations) => { { method: 'GET', action: `/votes/${encodeURIComponent(pItem.voteId)}` }, button({ type: 'submit', class: 'buy-btn' }, i18n.parliamentVoteAction) ) - : null + : null, + commentsBlock(null, pItem, '/parliament?filter=revocations', pItem.title || pItem.lawTitle || '') ); }); return div( @@ -698,12 +685,11 @@ const FutureRevocationsList = (rows) => { const cards = rows.map(pItem => div( { class: 'card' }, - br(), + parliamentTop(pItem, '/parliament?filter=revocations'), pItem.method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentGovMethod.toUpperCase() + ': '), span({ class: 'card-value' }, pItem.method)) : null, - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentThProposalDate.toUpperCase() + ': '), span({ class: 'card-value' }, fmt(pItem.createdAt))), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(pItem.proposer))), h2(pItem.title || pItem.lawTitle || ''), - p(pItem.reasons || '') + p(pItem.reasons || ''), + commentsBlock(null, pItem, '/parliament?filter=revocations', pItem.title || pItem.lawTitle || '') ) ); return div( @@ -743,7 +729,7 @@ const LawsStats = (laws = [], revocatedCount = 0) => { ); }; -const LawsList = (laws) => { +const LawsList = (laws, comments = null) => { if (!laws || !laws.length) return NoLaws(); const cards = laws.map(l => { const total = Number((l.votes && (l.votes.total || l.votes.TOTAL)) || 0); @@ -752,14 +738,13 @@ const LawsList = (laws) => { const showMetricsFlag = showVoteMetrics(l.method); return div( { class: 'card' }, - br(), + parliamentTop(l, '/parliament?filter=laws', l.enactedAt), div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod + ': ').toUpperCase()), span({ class: 'card-value' }, l.method)), - div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted + ': ').toUpperCase()), span({ class: 'card-value' }, fmt(l.enactedAt))), - div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.parliamentLawProposer.toUpperCase() + ': '), span({ class: 'card-value' }, userLink(l.proposer))), h2(l.question || ''), p(l.description || ''), showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesNeeded + ': ').toUpperCase()), span({ class: 'card-value' }, String(needed))) : null, - showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesSlashTotal + ': ').toUpperCase()), span({ class: 'card-value' }, `${yes}/${total}`)) : null + showMetricsFlag ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentVotesSlashTotal + ': ').toUpperCase()), span({ class: 'card-value' }, `${yes}/${total}`)) : null, + commentsBlock(comments, l, '/parliament?filter=laws', l.question || '') ); }); return div( @@ -1021,12 +1006,12 @@ const CandidaturesSection = (governmentCard, candidatures, leaderMeta, electionQ ); }; -const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose) => { +const ProposalsSection = (governmentCard, proposals, futureLaws, canPropose, comments = null) => { const has = proposals && proposals.length > 0; const fl = FutureLawsList(futureLaws || []); if (!has && canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), fl); if (!has && !canPropose) return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), NoProposals(), fl); - return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(paged(proposals)), fl); + return div(h2(i18n.parliamentGovernmentCard), GovHeader(governmentCard || {}), ProposalForm(), ProposalsList(paged(proposals), comments), fl); }; const RevocationsSection = (governmentCard, laws, revocations, futureRevocations) => @@ -1067,7 +1052,8 @@ const parliamentView = async (state) => { revocationsEnactedCount, historicalMetas = {}, leadersMetas = {}, - inhabitantsTotal + inhabitantsTotal, + comments = null } = state; const fallbackGov = { @@ -1096,7 +1082,7 @@ const parliamentView = async (state) => { const LawsSectionWrap = () => div( LawsStats(laws || [], revocationsEnactedCount || 0), - LawsList(paged(laws || [])) + LawsList(paged(laws || []), comments) ); return template( @@ -1112,7 +1098,7 @@ const parliamentView = async (state) => { section( filter === 'government' ? GovernmentCard(gov, powerMeta, hemicycle || null, seatsMode || 'election', houseNames || {}) : null, filter === 'candidatures' ? CandidaturesSection(gov, candidatures, leaderMeta, electionQuorum) : null, - filter === 'proposals' ? ProposalsSection(gov, proposals, futureLaws, canPropose) : null, + filter === 'proposals' ? ProposalsSection(gov, proposals, futureLaws, canPropose, comments) : null, filter === 'laws' ? LawsSectionWrap() : null, filter === 'revocations' ? RevocationsSection(gov, laws, revocations, futureRevocations) : null, filter === 'historical' ? div(HistoricalGovsSummary(historical || []), HistoricalList(paged(historical || []), historicalMetas)) : null, diff --git a/src/views/peers_view.js b/src/views/peers_view.js index f2bd5b1..b3c8761 100644 --- a/src/views/peers_view.js +++ b/src/views/peers_view.js @@ -268,7 +268,7 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers, lanBroadc const tab = tabs.includes(filter) ? filter : 'status'; const tabLabel = { status: i18n.status || 'Status', connections: i18n.peerConnectionsTitle || 'Connections', lan: i18n.peersTabLan, health: i18n.peersTabHealth }; const statusToolbar = peersToolbar('status', onlineCount + discoveredCount + unknownCount, [ - { label: i18n.online, count: onlineCount }, + { label: i18n.onlinePlural || i18n.online, count: onlineCount }, { label: i18n.discovered, count: discoveredCount }, { label: i18n.unknown, count: unknownCount } ]); diff --git a/src/views/phone_view.js b/src/views/phone_view.js index 96acdfd..87727b4 100644 --- a/src/views/phone_view.js +++ b/src/views/phone_view.js @@ -276,7 +276,7 @@ exports.phoneView = ({ available = false, state = null, pams = [], history = [], span({ class: 'inbox-filters-label' }, i18n.inboxFiltersLabel || 'Filters:'), unheard.length ? form({ method: 'POST', action: '/phone/heard-all', class: 'inbox-vis-toggle' }, ...unheard.map(k => input({ type: 'hidden', name: 'keys', value: k })), - button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkAllRead} (${unheard.length})`) + button({ type: 'submit', class: 'btn' }, `${i18n.inboxMarkRead || i18n.inboxMarkAllRead} (${unheard.length})`) ) : null, deletable.length ? form({ method: 'POST', action: '/phone/delete-shown', class: 'inbox-vis-toggle inbox-bulk-delete' }, ...(showRecords ? records.map(m => input({ type: 'hidden', name: 'keys', value: m.key })) : []), diff --git a/src/views/podcasts_view.js b/src/views/podcasts_view.js index 4c7141f..f95643e 100644 --- a/src/views/podcasts_view.js +++ b/src/views/podcasts_view.js @@ -1,14 +1,14 @@ -const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, table, tr, td, audio: audioHyperaxe, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged } = require("./main_views"); +const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, renderStateChip, renderCardTop, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, renderEngagement, moduleIsEmpty, contentDeleteAction, paged, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const { renderCommentsSection } = require("./comments_view"); const { renderStyledText } = require("../backend/renderStyledText"); -const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view"); -const moment = require("../server/node_modules/moment"); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseSelect, renderLicenseChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); const userId = config.keys.id; -const BASE_FILTERS = ["ALL", "RECENT", "MINE", "TOP", "VIEWERS"]; +const BASE_FILTERS = ["ALL", "RECENT", "MINE", "FAVORITES", "TOP", "VIEWERS"]; +const SORTS = ["recent", "oldest", "top"]; const CATEGORIES = ["NEWS", "MUSIC", "TALK", "EDUCATION", "TECH", "CULTURE", "COMMUNITY", "OASIS", "OTHER"]; const cap = (s) => s.charAt(0) + s.slice(1).toLowerCase(); @@ -16,7 +16,6 @@ const filterLabel = (f) => String(i18n[`podcastFilter${cap(f)}`] || f).toUpperCa const catLabel = (c) => String(i18n[`podcastCategory${cap(c)}`] || c).toUpperCase(); const channelHref = (ch) => `/podcasts/${encodeURIComponent(ch.id)}`; const episodeHref = (ep) => `/podcasts/episode/${encodeURIComponent(ep.id)}`; -const fmt = (d) => moment(d).format("YYYY/MM/DD HH:mm"); const blobSrc = (m) => `/blob/${encodeURIComponent(m.blobId)}`; const renderTags = (tags) => (Array.isArray(tags) && tags.length) @@ -32,21 +31,13 @@ const renderCover = (ch, cls = "podcast-cover") => { const countsLine = (ch) => div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.podcastPlaysLabel}: ${ch.playCount || 0}`) ); -const infoTable = (item) => table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, fmt(item.createdAt)) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(item.author)) - ) -); const podcastChipFor = (mode, census) => { if (mode === "ALL") return true; if (!Array.isArray(census)) return true; if (mode === "MINE") return census.some(c => String(c.author) === String(userId)); if (mode === "RECENT") return census.length > 0; + if (mode === "FAVORITES") return census.some(c => c.isFavorite); if (mode === "TOP") return census.some(c => (c.opinionCount || 0) > 0 || (c.spreadCount || 0) > 0); if (mode === "VIEWERS") return census.some(c => (c.playCount || 0) > 0); if (CATEGORIES.includes(mode)) return census.some(c => c.category === mode); @@ -56,15 +47,12 @@ exports.podcastChipFor = podcastChipFor; const renderChannelCard = (ch, params = {}) => div({ class: "tribe-card podcast-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(ch.id, channelHref(ch), { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: (params.spreadMap && params.spreadMap.get(ch.id)) || null, deleteAction: String(ch.author) === String(userId) ? contentDeleteAction("podcast", ch.id) : undefined }) - ), + renderCardTop(ch.id, channelHref(ch), { date: ch.createdAt, chips: [renderStateChip("neutral", "", catLabel(ch.category)), ch.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('podcasts', ch.title, ch.id)) : null], author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: (params.spreadMap && params.spreadMap.get(ch.id)) || null, deleteAction: String(ch.author) === String(userId) ? contentDeleteAction("podcast", ch.id) : undefined }), div({ class: "tribe-card-body" }, ch.cover && ch.cover.blobId ? a({ href: channelHref(ch) }, renderCover(ch, "podcast-card-cover")) : null, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch) }, ch.title || "โ€”"))), - div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category)), ch.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('podcasts', ch.title, ch.id)) : null), - countsLine(ch) + countsLine(ch), + null ) ); @@ -93,11 +81,12 @@ const renderChannelForm = (ch) => label(i18n.podcastDescriptionLabel), br(), textarea({ name: "description", rows: 5, maxlength: "3000", placeholder: i18n.podcastDescriptionPlaceholder }, ch ? ch.description : ""), br(), label(i18n.podcastCoverLabel), br(), - input({ type: "file", name: "cover", accept: "image/*,video/*" }), br(), br(), + (ch && renderMediaLockNote(ch)) || input({ type: "file", name: "cover", accept: "image/*,video/*" }), br(), br(), label(i18n.podcastCategoryLabel), br(), select({ name: "category" }, ...CATEGORIES.map(c => option({ value: c, ...((ch ? ch.category : "TALK") === c ? { selected: true } : {}) }, catLabel(c)))), br(), br(), label(i18n.podcastTagsLabel), br(), input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ch ? ch.tags.join(", ") : "" }), br(), br(), + ...renderLicenseSelect(ch ? ch.license : undefined, i18n), renderClearnetSelector(ch ? !!ch.clearnet : false, i18n), br(), button({ type: "submit", class: "create-button" }, ch ? i18n.podcastUpdate : i18n.podcastCreate) ) @@ -109,10 +98,12 @@ const renderEpisodeForm = (ch, ep) => form({ method: "POST", action: ep ? `/podcasts/episode/update/${encodeURIComponent(ep.id)}` : `/podcasts/${encodeURIComponent(ch.id)}/episodes`, enctype: "multipart/form-data" }, label(i18n.podcastEpisodeTitleLabel), br(), input({ type: "text", name: "title", maxlength: "120", required: true, placeholder: i18n.podcastEpisodeTitlePlaceholder, value: ep ? ep.title : "" }), br(), + label(i18n.podcastEpisodeNumberLabel), br(), + input({ type: "number", name: "number", min: "1", step: "1", value: ep ? String(ep.number || "") : "" }), br(), label(i18n.podcastEpisodeDescriptionLabel), br(), textarea({ name: "description", rows: 6, maxlength: "5000", placeholder: i18n.podcastEpisodeDescriptionPlaceholder }, ep ? ep.description : ""), br(), label(i18n.podcastEpisodeAudioLabel), br(), - input({ type: "file", name: "media", accept: "audio/*,video/*", ...(ep ? {} : { required: true }) }), br(), br(), + (ep && renderMediaLockNote(ep)) || input({ type: "file", name: "media", accept: "audio/*,video/*", ...(ep ? {} : { required: true }) }), br(), br(), label(i18n.podcastTagsLabel), br(), input({ type: "text", name: "tags", maxlength: "200", placeholder: i18n.podcastTagsPlaceholder, value: ep ? ep.tags.join(", ") : "" }), br(), br(), button({ type: "submit", class: "create-button" }, ep ? i18n.podcastUpdate : i18n.podcastEpisodePublish) @@ -144,7 +135,10 @@ exports.podcastsView = async (channels, filter = "ALL", params = {}) => { form({ method: "GET", action: "/podcasts", class: "filter-box" }, input({ type: "hidden", name: "filter", value: f }), input({ type: "text", name: "q", value: q, placeholder: i18n.podcastSearchPlaceholder, class: "filter-box__input" }), - div({ class: "filter-box__controls" }, button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) + div({ class: "filter-box__controls" }, + select({ name: "sort", class: "filter-box__select" }, + ...SORTS.map(s => option({ value: s, ...((params.sort || "recent") === s ? { selected: true } : {}) }, i18n[`audioSort${s.charAt(0).toUpperCase()}${s.slice(1)}`]))), + button({ type: "submit", class: "filter-box__button" }, i18n.searchButton)) ) ), div({ class: "tribe-grid" }, list.length ? paged(list).map(c => renderChannelCard(c, params)) : p(i18n.podcastNoItems)) @@ -159,9 +153,11 @@ const episodeChips = (ep) => ep.opinionCount > 0 ? renderStateChip("mutuals", "๊”", String(ep.opinionCount)) : null ); -const episodeActions = (ep, params = {}, returnTo) => - renderContentActions(ep.id, episodeHref(ep), { +const episodeTop = (ep, params = {}, returnTo) => + renderCardTop(ep.id, episodeHref(ep), { author: ep.author, + date: ep.createdAt, + chips: [episodeChips(ep)], favKind: "podcasts", isFavorite: ep.isFavorite, reportTitle: ep.title, @@ -175,15 +171,10 @@ const episodeRow = (ch, ep, params = {}, returnTo) => div({ class: "podcast-episode-row" }, span({ class: "podcast-episode-number" }, `#${ep.number}`), div({ class: "podcast-episode-main" }, - div({ class: "emergency-update-head podcast-episode-head" }, - div({ class: "podcast-episode-head-left" }, - a({ href: episodeHref(ep), class: "podcast-episode-title" }, ep.title), - episodeChips(ep) - ), - episodeActions(ep, params, returnTo) - ), + episodeTop(ep, params, returnTo), + a({ href: episodeHref(ep), class: "podcast-episode-title" }, ep.title), ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, fmt(ep.createdAt))) + null ) ); @@ -210,15 +201,12 @@ exports.singleChannelView = async (ch, params = {}) => { ) : null; const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(ch.id, href, { author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("podcast", ch.id) : undefined }) - ), - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, ch.title), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + renderCardTop(ch.id, href, { arrow: false, date: ch.createdAt, chips: [renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null), renderLicenseChip(ch.license), renderStateChip("neutral", "", catLabel(ch.category))], bylineExtra: [renderEditedChip(ch.tipId, ch.rootId)], author: ch.author, favKind: "podcasts", isFavorite: ch.isFavorite, reportTitle: ch.title, spread: params.spread || null, returnTo: href, deleteAction: isAuthor ? contentDeleteAction("podcast", ch.id) : undefined }), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, ch.title)), isAuthor ? renderClearnetSwitch("podcasts", ch.rootId || ch.id, !!ch.clearnet) : null, - div({ class: "card-chips-row" }, renderStateChip("neutral", "", catLabel(ch.category))), ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + renderDetailShare(ch.id, href, ch.title), ch.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ch.description)) : null, - infoTable(ch), countsLine(ch), isAuthor ? div({ class: "tribe-side-actions podcast-episode-add" }, @@ -235,16 +223,11 @@ exports.singleChannelView = async (ch, params = {}) => { isAuthor && mode === "EPISODE" ? renderEpisodeForm(ch, null) : null, allEpisodes.length ? div({ class: "podcast-featured" }, - div({ class: "emergency-update-head podcast-episode-head" }, - div({ class: "podcast-episode-head-left" }, - h2({ class: "tribe-card-title" }, a({ href: episodeHref(allEpisodes[0]) }, `#${allEpisodes[0].number} ยท ${allEpisodes[0].title}`)), - episodeChips(allEpisodes[0]) - ), - episodeActions(allEpisodes[0], params, href) - ), + episodeTop(allEpisodes[0], params, href), + h2({ class: "tribe-card-title" }, a({ href: episodeHref(allEpisodes[0]) }, `#${allEpisodes[0].number} ยท ${allEpisodes[0].title}`)), renderPlayer(allEpisodes[0]), allEpisodes[0].description ? p({ class: "tribe-side-description" }, ...renderStyledText(allEpisodes[0].description)) : null, - p({ class: "card-footer" }, span({ class: "date-link" }, fmt(allEpisodes[0].createdAt))) + null ) : null, div({ class: "card-section podcast-episodes", id: "episodes" }, @@ -288,29 +271,25 @@ exports.singleEpisodeView = async (ep, params = {}) => { ] : []; const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(ep.id, href, { author: ep.author, favKind: "podcasts", isFavorite: ep.isFavorite, reportTitle: ep.title, spread: params.spread || null, returnTo: href, torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, deleteAction: isAuthor ? contentDeleteAction("podcastEpisode", ep.id) : undefined }) - ), - div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch), class: "user-link" }, ch.title || i18n.podcastsTitle)), renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null)), + renderCardTop(ep.id, href, { arrow: false, date: ep.createdAt, chips: [renderReachChip(!!ch.clearnet, i18n, ch.clearnet ? clearnetItemHref('podcasts', ch.title, ch.id) : null), renderLicenseChip(ch.license), episodeChips(ep)], bylineExtra: [renderEditedChip(ep.tipId, ep.rootId)], author: ep.author, favKind: "podcasts", isFavorite: ep.isFavorite, reportTitle: ep.title, spread: params.spread || null, returnTo: href, torrentFrom: { blobId: ep.media && ep.media.blobId, name: ep.title }, deleteAction: isAuthor ? contentDeleteAction("podcastEpisode", ep.id) : undefined }), + div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href: channelHref(ch), class: "user-link" }, ch.title || i18n.podcastsTitle))), ch.cover && ch.cover.blobId ? a({ href: channelHref(ch), class: "podcast-cover-link" }, renderCover(ch, "podcast-cover podcast-cover-large")) : null, + renderDetailShare(ep.id, href, ep.title), ep.description ? p({ class: "tribe-side-description" }, ...renderStyledText(ep.description)) : null, - infoTable(ep), renderTags(ep.tags), ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null ); const main = div({ class: "tribe-main" }, div({ class: "emergency-update-head podcast-episode-head" }, div({ class: "podcast-episode-head-left" }, - h2({ class: "tribe-card-title" }, `#${ep.number} ยท ${ep.title}`), - episodeChips(ep) + h2({ class: "tribe-card-title" }, `#${ep.number} ยท ${ep.title}`) ), headActions.length ? div({ class: "tribe-side-actions emergency-update-actions" }, ...headActions) : null ), renderPlayer(ep), renderEngagement(ep.id, renderOpinionsVoting("/podcasts/opinions", ep.id, ep.opinions, href, ep.opinions_inhabitants), - renderCommentsSection({ action: `/podcasts/episode/${encodeURIComponent(ep.id)}/comments`, comments: params.comments || [], returnTo: href }) - ), + renderCommentsSection({ action: `/podcasts/episode/${encodeURIComponent(ep.id)}/comments`, comments: params.comments || [], returnTo: href })), isAuthor && mode === "EDIT" ? renderEpisodeForm(ch, ep) : null ); return template( diff --git a/src/views/polls_view.js b/src/views/polls_view.js index 61461fd..1f82cd9 100644 --- a/src/views/polls_view.js +++ b/src/views/polls_view.js @@ -1,9 +1,9 @@ const { div, h2, p, section, button, form, a, input, label, span, textarea, br, table, tr, td } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderContentActions, renderStateChip, renderLifespanChip, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { template, i18n, userLink, renderOpinionsVoting, renderEngagement, renderCardTop, renderStateChip, renderLifespanChip, renderSpreadEditWarning, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); -const { MAX_OPTIONS, MIN_OPTIONS, MAX_OPTION_LENGTH } = require("../models/polls_model_limits"); +const { MAX_OPTIONS, MAX_OPTION_LENGTH } = require("../models/polls_model_limits"); const userId = config.keys.id; @@ -103,18 +103,15 @@ const renderPollCard = (poll, filter, spreadInfo, basePath = "/polls") => { renderLifespanChip(poll.lifetime, i18n) ].filter(Boolean); return div({ class: "tribe-card poll-card" + (isOwn ? " own-content" : "") }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(poll.id, href, { spread: spreadInfo || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) - ), + renderCardTop(poll.id, href, { date: poll.createdAt, chips, spread: spreadInfo || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, poll.question)) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) - ) + ), + null ) ); }; @@ -122,6 +119,8 @@ const renderPollCard = (poll, filter, spreadInfo, basePath = "/polls") => { const renderCreateForm = (poll = null, params = {}) => { const isEdit = !!poll; const action = isEdit ? `/polls/update/${encodeURIComponent(poll.id)}` : "/polls/create"; + const lock = isEdit ? renderMediaLockNote(poll) : null; + const ro = lock ? { readonly: true } : {}; return section( params.spreadWarning || null, div({ class: "div-center audio-form" }, @@ -129,7 +128,7 @@ const renderCreateForm = (poll = null, params = {}) => { form({ method: "POST", action }, label(i18n.pollQuestion), br(), - input({ type: "text", name: "question", required: true, maxlength: "300", value: isEdit ? poll.question : "", placeholder: i18n.pollQuestionPlaceholder }), + input({ type: "text", name: "question", required: true, maxlength: "300", value: isEdit ? poll.question : "", placeholder: i18n.pollQuestionPlaceholder, ...ro }), br(), br(), label(i18n.pollOptions), br(), @@ -137,8 +136,10 @@ const renderCreateForm = (poll = null, params = {}) => { name: "options", rows: String(MAX_OPTIONS), required: true, - maxlength: String(MAX_OPTIONS * (MAX_OPTION_LENGTH + 1)) + maxlength: String(MAX_OPTIONS * (MAX_OPTION_LENGTH + 1)), + ...ro }, isEdit ? poll.options.join("\n") : ""), + lock, br(), div({ class: "poll-switch" }, input({ type: "hidden", name: "anonymous", value: "0" }), @@ -273,12 +274,10 @@ exports.singlePollView = async (poll, comments = [], params = {}) => { : null; const pollSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(poll.id, href, { spread: params.spreads || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, returnTo: href, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }) - ), + renderCardTop(poll.id, href, { arrow: false, date: poll.createdAt, chips, bylineExtra: [renderEditedChip(poll.tipId, poll.rootId)], spread: params.spreads || null, author: poll.author, favKind: 'polls', isFavorite: poll.isFavorite, returnTo: href, reportTitle: poll.question, deleteAction: isOwn ? contentDeleteAction('poll', poll.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, poll.question)), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(poll.createdAt, poll.author)), + renderDetailShare(poll.id, href, poll.question), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.pollVoters}: ${poll.totalVoters}`) @@ -308,10 +307,9 @@ exports.singlePollView = async (poll, comments = [], params = {}) => { : (poll.voters.length ? div({ class: "card-assigned-list" }, ...poll.voters.map(v => userLink(v))) : null), - poll.tribeId ? null : renderEngagement(poll.id, - renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants), - renderComments(poll, comments, basePath) - ) + renderEngagement(poll.id, + poll.tribeId ? null : renderOpinionsVoting('/polls/opinions', poll.id, poll.opinions, href, poll.opinions_inhabitants), + poll.tribeId ? null : renderComments(poll, comments, basePath)) ); return template( diff --git a/src/views/projects_view.js b/src/views/projects_view.js index df75a33..a5e58c9 100644 --- a/src/views/projects_view.js +++ b/src/views/projects_view.js @@ -1,11 +1,11 @@ const { form, button, div, h2, p, section, input, label, textarea, br, a, span, select, option, img, ul, li, table, thead, tbody, tr, th, td, progress, video, audio } = require("../server/node_modules/hyperaxe") const { renderZoomableImage } = require("./gallery_view") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderCardTop, renderSpreadEditWarning, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderStyledText } = require("../backend/renderStyledText") -const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel, renderMapEmbedWithZoom } = require("./maps_view") +const { renderMapEmbedWithZoom } = require("./maps_view") const { renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const renderMediaBlob = (value, attrs = {}) => { @@ -414,11 +414,7 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM const isOwn = pr.author && String(pr.author) === String(userId) return div({ class: "trending-card project-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: spreadMap.get(pr.id || pr.key) || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, deleteAction: isOwn ? contentDeleteAction('project', pr.id || pr.key) : undefined }) - ), + renderCardTop(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { date: pr.createdAt, chips, spread: spreadMap.get(pr.id || pr.key) || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, deleteAction: isOwn ? contentDeleteAction('project', pr.id || pr.key) : undefined }), div({ class: "card-section project-card-body" }, heroNode, div({ class: "shop-title-row" }, @@ -426,7 +422,6 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM a({ href: `/projects/${encodeURIComponent(pr.id)}` }, pr.title || i18n.projectsTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, goal > 0 ? div({ class: "price-chip" }, `${pr.goal} ECO`) : null, @@ -435,7 +430,8 @@ const renderProjectList = exports.renderProjectList = (projects, filter, spreadM : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.projectFollowers}: ${followersCount(pr)}`) - ) + ), + null ) ) }) @@ -449,6 +445,7 @@ const renderProjectForm = (project, mode, spreadWarning = null) => { const deadlineValue = pr.deadline ? moment(pr.deadline).format("YYYY-MM-DDTHH:mm") : "" const milestoneMax = deadlineValue || undefined const returnTo = "/projects?filter=MINE" + const mediaLock = isEdit ? renderMediaLockNote(pr) : null return div( { class: "div-center project-form" }, @@ -470,10 +467,7 @@ const renderProjectForm = (project, mode, spreadWarning = null) => { br(), label(i18n.projectImage), br(), - input({ type: "file", name: "image" }), - br(), - label(i18n.uploadMedia), br(), - input({ type: "file", name: "blob" }), + ...(mediaLock ? [mediaLock] : [input({ type: "file", name: "image" }), br(), label(i18n.uploadMedia), br(), input({ type: "file", name: "blob" })]), br(), pr.image ? renderMediaBlob(pr.image) : null, br(), @@ -604,7 +598,6 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { renderLifespanChip(pr.lifetime, i18n), renderEcoTax(pr.msgSize, pr.id || pr.key), renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('projects', pr.title, pr.id || pr.key) : null), - isAuthor && canClearnet ? renderClearnetSwitch('projects', pr.rootId || pr.id || pr.key, isClearnet) : null, pr.subscription ? ((isAuthor || pr.subscription.subscribed === true) ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) @@ -617,11 +610,11 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { sideActions.push(isFollower ? form({ method: "POST", action: `/projects/unfollow/${encodeURIComponent(pr.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "unsubscribe-btn" }, i18n.projectUnfollowButton) + button({ type: "submit", class: "tribe-action-btn danger-btn" }, String(i18n.projectUnfollowButton).toUpperCase()) ) : form({ method: "POST", action: `/projects/follow/${encodeURIComponent(pr.id)}` }, input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ type: "submit", class: "subscribe-btn" }, i18n.projectFollowButton) + button({ type: "submit", class: "tribe-action-btn" }, String(i18n.projectFollowButton).toUpperCase()) ) ) } @@ -650,19 +643,17 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { } const projectSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { spread: params.spreads || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, returnTo: `/projects/${encodeURIComponent(pr.id)}`, deleteAction: isAuthor ? contentDeleteAction('project', pr.id || pr.key) : undefined }) - ), + renderCardTop(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, { arrow: false, date: pr.createdAt, chips, bylineExtra: [renderEditedChip(pr.id, pr.rootId)], spread: params.spreads || null, author: pr.author, favKind: 'projects', isFavorite: pr.isFavorite, reportTitle: pr.title, returnTo: `/projects/${encodeURIComponent(pr.id)}`, deleteAction: isAuthor ? contentDeleteAction('project', pr.id || pr.key) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, pr.title) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + isAuthor && canClearnet ? renderClearnetSwitch('projects', pr.rootId || pr.id || pr.key, isClearnet) : null, pr.image ? renderMediaBlob(pr.image, { class: "tribe-detail-image" }) : null, + renderDetailShare(pr.id || pr.key, `/projects/${encodeURIComponent(pr.id)}`, pr.title), div({ class: "price-chip" }, `${pr.goal || 0} ECO`), div({ class: "job-price-line card-salary" }, `${i18n.projectFollowers}: ${followersCount(pr)}`), renderProgressBlock(i18n.projectProgress + ":", `${pct}%`, pct, 100), goal > 0 ? renderProgressBlock(i18n.projectFunding + ":", `${fundingPct}%`, fundingPct, 100) : null, - table({ class: "tribe-info-table" }, ...renderCreatedRows(pr.createdAt, pr.author)), sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, pr.subscription ? renderSubscriptionBox({ @@ -697,8 +688,7 @@ exports.singleProjectView = async (project, filter, comments, params = {}) => { action: `/projects/${encodeURIComponent(pr.id || pr.key)}/comments`, comments, returnTo: `/projects/${encodeURIComponent(pr.id || pr.key)}` - }) - ), + })), ) return template( diff --git a/src/views/report_view.js b/src/views/report_view.js index 3dc904f..e71d220 100644 --- a/src/views/report_view.js +++ b/src/views/report_view.js @@ -1,6 +1,6 @@ -const { hr, div, h2, p, section, button, form, a, textarea, br, input, img, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, textarea, br, input, span, label, select, option, video, audio, table, tr, td } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, renderEngagement, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadEditWarning, renderCardTop, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderEditedChip, renderDetailShare } = require("./main_views"); const { renderPhotoGallery, renderGalleryFields, renderZoomableImage } = require("./gallery_view"); const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); @@ -50,27 +50,6 @@ const SEVERITY_BY_FILTER = { const opt = (value, isSelected, text) => option(Object.assign({ value }, isSelected ? { ...("selected" ? { selected: true } : {})} : {}), text); -const hasAnyTemplateValue = (t) => { - if (!t || typeof t !== "object") return false; - return Object.values(t).some((v) => String(v || "").trim()); -}; - -const renderCardField = (labelText, value = "") => - div( - { class: "card-field" }, - span({ class: "card-label" }, labelText), - span({ class: "card-value" }, ...renderStyledText(String(value ?? ""))) - ); - -const renderStackedTextField = (lbl, val) => - String(val || "").trim() - ? div( - { class: "card-field card-field-stacked" }, - span({ class: "card-label" }, lbl), - span({ class: "card-value" }, ...renderStyledText(String(val))) - ) - : null; - const renderReportStatusSetter = (report) => { const st = normalizeStatus(report && report.status ? report.status : "OPEN"); return form( @@ -87,61 +66,6 @@ const renderReportStatusSetter = (report) => { ); }; -const renderTemplateDetails = (report) => { - const category = normU(report.category); - const t = report.template && typeof report.template === "object" ? report.template : {}; - if (!hasAnyTemplateValue(t)) return null; - - const renderValueField = (lbl, val) => - String(val || "").trim() - ? renderCardField(lbl, String(val)) - : null; - - if (category === "BUGS") { - return div( - { class: "report-template" }, - h2({ class: "report-template-title" }, i18n.reportsBugTemplateTitle), - renderStackedTextField(i18n.reportsStepsToReproduceLabel + ":", t.stepsToReproduce), - renderStackedTextField(i18n.reportsExpectedBehaviorLabel + ":", t.expectedBehavior), - renderStackedTextField(i18n.reportsActualBehaviorLabel + ":", t.actualBehavior), - renderStackedTextField(i18n.reportsEnvironmentLabel + ":", t.environment), - renderValueField(i18n.reportsReproduceRateLabel + ":", t.reproduceRate) - ); - } - - if (category === "FEATURES") { - return div( - { class: "report-template" }, - h2({ class: "report-template-title" }, i18n.reportsFeatureTemplateTitle), - renderStackedTextField(i18n.reportsProblemStatementLabel + ":", t.problemStatement), - renderStackedTextField(i18n.reportsUserStoryLabel + ":", t.userStory), - renderStackedTextField(i18n.reportsAcceptanceCriteriaLabel + ":", t.acceptanceCriteria) - ); - } - - if (category === "ABUSE") { - return div( - { class: "report-template" }, - h2({ class: "report-template-title" }, i18n.reportsAbuseTemplateTitle), - renderStackedTextField(i18n.reportsWhatHappenedLabel + ":", t.whatHappened), - renderStackedTextField(i18n.reportsReportedUserLabel + ":", t.reportedUser), - renderStackedTextField(i18n.reportsEvidenceLinksLabel + ":", t.evidenceLinks) - ); - } - - if (category === "CONTENT") { - return div( - { class: "report-template" }, - h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), - renderStackedTextField(i18n.reportsContentLocationLabel + ":", t.contentLocation), - renderStackedTextField(i18n.reportsWhyInappropriateLabel + ":", t.whyInappropriate), - renderStackedTextField(i18n.reportsEvidenceLinksLabel + ":", t.evidenceLinks) - ); - } - - return null; -}; - const renderReportCommentsSection = (reportId, comments = []) => { return renderSharedCommentsSection({ action: `/reports/${encodeURIComponent(reportId)}/comments`, @@ -150,81 +74,6 @@ const renderReportCommentsSection = (reportId, comments = []) => { }); }; -const renderTemplateForCategory = (category, templateData = {}) => { - const cat = normU(category || "FEATURES"); - const t = templateData && typeof templateData === "object" ? templateData : {}; - const tval = (k) => String(t[k] || ""); - const reproduceRateVal = normU(t.reproduceRate || ""); - - if (cat === "BUGS") { - return div( - { class: "report-template-block" }, - h2({ class: "report-template-title" }, i18n.reportsBugTemplateTitle), - label(i18n.reportsStepsToReproduceLabel), - br(), - textarea({ maxlength: "5000", name: "stepsToReproduce", rows: "4", placeholder: i18n.reportsStepsToReproducePlaceholder }, tval("stepsToReproduce")), - br(), - label(i18n.reportsExpectedBehaviorLabel), - br(), - textarea({ maxlength: "5000", name: "expectedBehavior", rows: "3", placeholder: i18n.reportsExpectedBehaviorPlaceholder }, tval("expectedBehavior")), - br(), - label(i18n.reportsActualBehaviorLabel), - br(), - textarea({ maxlength: "5000", name: "actualBehavior", rows: "3", placeholder: i18n.reportsActualBehaviorPlaceholder }, tval("actualBehavior")), - br(), - label(i18n.reportsEnvironmentLabel), - br(), - textarea({ maxlength: "5000", name: "environment", rows: "3", placeholder: i18n.reportsEnvironmentPlaceholder }, tval("environment")), - br(), - label(i18n.reportsReproduceRateLabel), - br(), - select( - { name: "reproduceRate" }, - opt("", !reproduceRateVal, i18n.reportsReproduceRateUnknown), - opt("ALWAYS", reproduceRateVal === "ALWAYS", i18n.reportsReproduceRateAlways), - opt("OFTEN", reproduceRateVal === "OFTEN", i18n.reportsReproduceRateOften), - opt("SOMETIMES", reproduceRateVal === "SOMETIMES", i18n.reportsReproduceRateSometimes), - opt("RARELY", reproduceRateVal === "RARELY", i18n.reportsReproduceRateRarely), - opt("UNABLE", reproduceRateVal === "UNABLE", i18n.reportsReproduceRateUnable) - ) - ); - } - - if (cat === "CONTENT") { - return div( - { class: "report-template-block" }, - h2({ class: "report-template-title" }, i18n.reportsContentTemplateTitle), - label(i18n.reportsContentLocationLabel), - br(), - textarea({ maxlength: "5000", name: "contentLocation", rows: "3", placeholder: i18n.reportsContentLocationPlaceholder }, tval("contentLocation")), - br(), - label(i18n.reportsWhyInappropriateLabel), - br(), - textarea({ maxlength: "5000", name: "whyInappropriate", rows: "4", placeholder: i18n.reportsWhyInappropriatePlaceholder }, tval("whyInappropriate")), - br(), - label(i18n.reportsEvidenceLinksLabel), - br(), - textarea({ maxlength: "5000", name: "evidenceLinks", rows: "3", placeholder: i18n.reportsEvidenceLinksPlaceholder }, tval("evidenceLinks")) - ); - } - - return div( - { class: "report-template-block" }, - h2({ class: "report-template-title" }, i18n.reportsFeatureTemplateTitle), - label(i18n.reportsProblemStatementLabel), - br(), - textarea({ maxlength: "5000", name: "problemStatement", rows: "4", placeholder: i18n.reportsProblemStatementPlaceholder }, tval("problemStatement")), - br(), - label(i18n.reportsUserStoryLabel), - br(), - textarea({ maxlength: "5000", name: "userStory", rows: "3", placeholder: i18n.reportsUserStoryPlaceholder }, tval("userStory")), - br(), - label(i18n.reportsAcceptanceCriteriaLabel), - br(), - textarea({ maxlength: "5000", name: "acceptanceCriteria", rows: "4", placeholder: i18n.reportsAcceptanceCriteriaPlaceholder }, tval("acceptanceCriteria")) - ); -}; - const renderReportStatusChip = (status) => { const s = normalizeStatus(status); const variant = @@ -282,21 +131,21 @@ const renderReportCard = (report, userId, currentFilter = "all", spreadInfo) => origin ? renderTribeOriginChip(origin) : null ].filter(Boolean); + const href = `/reports/${encodeURIComponent(report.id)}`; return div({ class: "tribe-card report-card" }, - origin ? null : div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: spreadInfo || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, deleteAction: String(report.author) === String(userId) ? contentDeleteAction('report', report.id) : undefined }) - ), + origin + ? renderCardTop(report.id, origin.href, { author: report.author, date: report.createdAt, chips, spread: spreadInfo || null, reportTitle: report.title }) + : renderCardTop(report.id, href, { date: report.createdAt, chips, spread: spreadInfo || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, deleteAction: String(report.author) === String(userId) ? contentDeleteAction('report', report.id) : undefined }), div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: origin ? origin.href : `/reports/${encodeURIComponent(report.id)}` }, report.title || i18n.reportsTitle) + a({ href: origin ? origin.href : href }, report.title || i18n.reportsTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) - ) + ), + null ) ); }; @@ -364,10 +213,7 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = ); const selectedCategory = ["FEATURES", "BUGS", "CONTENT"].includes(rawCategory) ? rawCategory : "FEATURES"; - const selectedTemplate = reportToEdit?.template && typeof reportToEdit.template === "object" ? reportToEdit.template : {}; - const applyLabel = i18n.apply || "Apply"; const sev = String(formData.severity || "low"); - const hiddenDescription = String(reportToEdit?.description || ""); return template( title, @@ -425,30 +271,25 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = filter === "edit" ? await renderSpreadEditWarning(reportToEdit && (reportToEdit.id || reportToEdit.key)) : null, filter === "create" ? [ - form( - { id: "report-category-form", method: "GET", action: "/reports" }, - input({ type: "hidden", name: "filter", value: "create" }), - div({ class: "apply-row" }, - select( - { name: "category", class: "report-category-select" }, - opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), - opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), - opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) - ), - button({ type: "submit", class: "create-button" }, applyLabel) - ) - ), - hr({ class: "form-sep" }), - h2({ class: "report-category-fixed" }, selectedCategory), form( { id: "report-create-form", action: "/reports/create", method: "POST", enctype: "multipart/form-data" }, - input({ type: "hidden", name: "category", value: selectedCategory }), - input({ type: "hidden", name: "description", value: "" }), label(i18n.reportsTitleLabel), br(), - input({ type: "text", name: "title", maxlength: "100", required: true, value: params.prefillTitle || "" }), + input({ type: "text", name: "title", maxlength: "100", required: true, value: formData.title || params.prefillTitle || "" }), + br(), + label(i18n.description), + br(), + textarea({ maxlength: "5000", name: "description", rows: "6" }, formData.description || ""), + br(), + label(i18n.reportsCategory), + br(), + select( + { name: "category", required: true }, + opt("FEATURES", selectedCategory === "FEATURES", i18n.reportsCategoryFeatures), + opt("BUGS", selectedCategory === "BUGS", i18n.reportsCategoryBugs), + opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) + ), br(), - renderTemplateForCategory(selectedCategory, params.draft || {}), br(), ...renderGalleryFields(formData, false, 8), label(i18n.uploadMedia), br(), @@ -478,7 +319,10 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = br(), input({ type: "text", name: "title", maxlength: "100", required: true, value: reportToEdit?.title || "" }), br(), - input({ type: "hidden", name: "description", value: hiddenDescription }), + label(i18n.description), + br(), + textarea({ maxlength: "5000", name: "description", rows: "6" }, reportToEdit?.description || ""), + br(), label(i18n.reportsCategory), br(), select( @@ -488,7 +332,6 @@ exports.reportView = async (reports, filter, reportId, createCategory, params = opt("CONTENT", selectedCategory === "CONTENT", i18n.reportsCategoryContent) ), br(), - renderTemplateForCategory(selectedCategory, selectedTemplate), br(), ...renderGalleryFields(reportToEdit || {}, true, 8), label("Tags"), @@ -522,7 +365,6 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => const btnClass = (v) => (filter === v ? "filter-btn active" : "filter-btn"); const confirmations = Array.isArray(report.confirmations) ? report.confirmations : []; const isAuthor = String(report.author) === String(userId); - const details = renderTemplateDetails(report); const chips = [ renderReportStatusChip(report.status), @@ -533,7 +375,7 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => ].filter(Boolean); const sideActions = []; - sideActions.push(form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` }, + if (!isAuthor && !confirmations.includes(userId)) sideActions.push(form({ method: "POST", action: `/reports/confirm/${encodeURIComponent(report.id)}` }, button({ type: "submit", class: "tribe-action-btn" }, i18n.reportsConfirmButton) )); const ownerActions = []; @@ -560,19 +402,17 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => pushRow(i18n.reportsCategory, report.category); const reportSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(report.id, `/reports/${encodeURIComponent(report.id)}`, { spread: params.spreads || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, returnTo: `/reports/${encodeURIComponent(report.id)}`, deleteAction: isAuthor ? contentDeleteAction('report', report.id) : undefined }) - ), + renderCardTop(report.id, `/reports/${encodeURIComponent(report.id)}`, { arrow: false, date: report.createdAt, chips, bylineExtra: [renderEditedChip(report.id, report.rootId)], spread: params.spreads || null, author: report.author, favKind: 'reports', isFavorite: report.isFavorite, reportTitle: report.title, report: false, returnTo: `/reports/${encodeURIComponent(report.id)}`, deleteAction: isAuthor ? contentDeleteAction('report', report.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, report.title) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(report.createdAt, report.author)), + renderDetailShare(report.id, `/reports/${encodeURIComponent(report.id)}`, report.title), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.reportsConfirmations}: ${confirmations.length}`) ), - sideActions.length ? div({ class: "tribe-side-actions" }, ...sideActions) : null, + sideActions.length ? div({ class: "tribe-side-actions report-confirm-actions" }, ...sideActions) : null, isAuthor ? div({ class: "tribe-side-actions housing-status-row" }, span({ class: "card-label" }, `${i18n.reportsStatus}: `), @@ -589,7 +429,12 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => const opinionsBar = renderOpinionsVoting('/reports/opinions', report.id, report.opinions, null, report.opinions_inhabitants); const reportMain = div({ class: "tribe-main" }, - details ? div({ class: "job-section" }, details) : null, + String(report.description || "").trim() + ? div({ class: "job-section" }, + h2({ class: "job-section-title" }, i18n.description), + p({ class: "tribe-side-description" }, ...renderStyledText(String(report.description))) + ) + : null, renderPhotoGallery(report, 'report'), renderEngagement(report.id, opinionsBar, renderReportCommentsSection(report.id, comments)) ); @@ -609,8 +454,7 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => ...(reportChip("features") ? [button({ type: "submit", name: "filter", value: "features", class: btnClass("features") }, String(i18n.reportsFilterFeatures).toUpperCase())] : []), ...(reportChip("bugs") ? [button({ type: "submit", name: "filter", value: "bugs", class: btnClass("bugs") }, String(i18n.reportsFilterBugs).toUpperCase())] : []), ...(reportChip("content") ? [button({ type: "submit", name: "filter", value: "content", class: btnClass("content") }, String(i18n.reportsFilterContent).toUpperCase())] : []), - ...(reportChip("confirmed") ? [button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase())] : []), - button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) + ...(reportChip("confirmed") ? [button({ type: "submit", name: "filter", value: "confirmed", class: btnClass("confirmed") }, String(i18n.reportsFilterConfirmed).toUpperCase())] : []) ), form( { method: "GET", action: "/reports", class: "ui-toolbar ui-toolbar--filters reports-subfilters" }, @@ -621,7 +465,8 @@ exports.singleReportView = async (report, filter, comments = [], params = {}) => ...(reportChip("sev_low") ? [button({ type: "submit", name: "filter", value: "sev_low", class: btnClass("sev_low") }, String(i18n.reportsSeverityLow).toUpperCase())] : []), ...(reportChip("sev_medium") ? [button({ type: "submit", name: "filter", value: "sev_medium", class: btnClass("sev_medium") }, String(i18n.reportsSeverityMedium).toUpperCase())] : []), ...(reportChip("sev_high") ? [button({ type: "submit", name: "filter", value: "sev_high", class: btnClass("sev_high") }, String(i18n.reportsSeverityHigh).toUpperCase())] : []), - ...(reportChip("sev_critical") ? [button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())] : []) + ...(reportChip("sev_critical") ? [button({ type: "submit", name: "filter", value: "sev_critical", class: btnClass("sev_critical") }, String(i18n.reportsSeverityCritical).toUpperCase())] : []), + button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.reportsCreateButton) ) ), div({ class: "tribe-details" }, reportSide, reportMain) diff --git a/src/views/rooms_view.js b/src/views/rooms_view.js index ba9efc7..734c113 100644 --- a/src/views/rooms_view.js +++ b/src/views/rooms_view.js @@ -1,5 +1,5 @@ -const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, table, tr, td, details, summary, ul, li } = require("../server/node_modules/hyperaxe") -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderContentActions, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon } = require("./main_views") +const { div, h2, p, section, button, form, a, span, br, hr, label, textarea, input, select, option, img, details, summary, ul, li } = require("../server/node_modules/hyperaxe") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderCardTop, renderEngagement, renderInviteQrCard, renderModuleStatsBy, moduleIsEmpty, liveClock, renderPhoneChip, contentDeleteAction, paged, renderVideoGrid, renderLiveFeed, renderIcon, renderDetailShare } = require("./main_views") const { renderEncryptedChip, renderTransportChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch } = require("./clearnet_view") const { renderStyledText } = require("../backend/renderStyledText") const moment = require("../server/node_modules/moment") @@ -87,16 +87,20 @@ const renderRoomItem = (room, params = {}) => { const occ = inside && Number.isFinite(Number(params.live.count)) ? { count: Number(params.live.count), max: params.live.max } : occOf(params, room) const canJoin = !room.isClosed && !inside && (room.type === "OPEN" || room.tribeId || String(room.author) === String(userId) || safeArr(room.members).includes(userId)) return li({ class: "mailing-archive-item room-item" }, - div({ class: "emergency-update-head mailing-archive-head" }, - div({ class: "mailing-archive-meta" }, - a({ href: roomHref(room), class: "user-link" }, room.title || "โ€”"), - span({ class: "card-label activity-update-counts mailing-counts" }, `๐Ÿ‘ฅ: ${safeArr(room.members).length}`), + renderCardTop(room.rootId, roomHref(room), { + date: room.createdAt, + chips: [ ...roomChips(room, occ, inside), room.clearnet === true && clearnetEligible(room) ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, inside ? renderStateChip("whole", "๊˜’", String(i18n.roomYouAreIn).toUpperCase()) : null - ), - renderContentActions(room.rootId, roomHref(room), { spread: (params.spreadMap && params.spreadMap.get(room.rootId)) || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, reportTitle: room.title, deleteAction: String(room.author) === String(userId) ? contentDeleteAction("room", room.rootId) : undefined }) - ) + ], + spread: (params.spreadMap && params.spreadMap.get(room.rootId)) || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, reportTitle: room.title, deleteAction: String(room.author) === String(userId) ? contentDeleteAction("room", room.rootId) : undefined + }), + div({ class: "mailing-archive-meta" }, + a({ href: roomHref(room), class: "user-link" }, room.title || "โ€”"), + span({ class: "card-label activity-update-counts mailing-counts" }, `๐Ÿ‘ฅ: ${safeArr(room.members).length}`) + ), + null ) } @@ -220,6 +224,13 @@ const renderLiveControls = (room, live) => div({ class: "room-live-controls" }, button({ type: "submit", class: "tribe-action-btn room-cam-btn-on room-stream-go", title: i18n.roomStreamGo }, "โ–ถ") ) : null, + live.streaming == null && live.screenAvailable + ? form({ method: "POST", action: "/rooms/stream", class: "phone-action-form" }, + input({ type: "hidden", name: "screen", value: "1" }), + input({ type: "hidden", name: "returnTo", value: `${roomHref(room)}#room-video` }), + button({ type: "submit", class: "tribe-action-btn", title: i18n.roomScreenWarning }, String(i18n.roomScreen).toUpperCase()) + ) + : null, form({ method: "POST", action: "/rooms/hand", class: "phone-action-form" }, input({ type: "hidden", name: "hand", value: live.hand ? "0" : "1" }), input({ type: "hidden", name: "returnTo", value: roomHref(room) }), @@ -288,7 +299,7 @@ const renderLivePanel = (room, live, occ, canJoin = false) => { you ? null : silenceForm(id, silenced) ) return div({ class: "phone-call-panel room-live-panel" }, - head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "โ–ถ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)), + head(div({ class: "card-chips-row" }, renderEncryptedChip(i18n), isPublic ? renderReachChip(true, i18n, roomClearnetHref(room)) : null, renderLiveChip({ count: live.count, max: live.max }), live.joinedAt ? span({ class: "phone-call-clock" }, liveClock(live.joinedAt)) : null, live.sharingScreen ? span({ class: "room-stream-chip", title: i18n.roomScreenWarning }, "๐Ÿ–ฅ " + String(i18n.roomScreenSharing)) : live.streaming != null ? span({ class: "room-stream-chip", title: live.streaming || "" }, "โ–ถ " + String(live.streaming || i18n.roomStreaming)) : null), renderLiveControls(room, live)), live.grid ? (safeArr(live.gridOrder).length ? div({ id: "room-video", class: safeArr(live.gridOrder).length === 1 ? "video-grid video-grid-one video-grid-solo" : "video-grid video-grid-one" }, @@ -367,30 +378,32 @@ exports.singleRoomView = async (room, params = {}) => { : [] const side = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(room.rootId, roomHref(room), { spread: params.spreads || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, returnTo: roomHref(room), reportTitle: room.title, deleteAction: isAuthor ? contentDeleteAction("room", room.rootId) : undefined }) - ), + renderCardTop(room.rootId, roomHref(room), { arrow: false, + date: room.createdAt, + chips: [ + ...roomChips(room, live ? { count: live.count, max: live.max } : occ, !!live), + canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? roomClearnetHref(room) : null) : null + ], + spread: params.spreads || null, author: room.author, favKind: "rooms", isFavorite: room.isFavorite, returnTo: roomHref(room), reportTitle: room.title, deleteAction: isAuthor ? contentDeleteAction("room", room.rootId) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, restricted ? i18n.roomStatusInviteOnly : (room.title || "โ€”"))), - div({ class: "card-chips-row" }, - ...roomChips(room, live ? { count: live.count, max: live.max } : occ, !!live), - canClearnet ? renderReachChip(isClearnet, i18n, isClearnet ? roomClearnetHref(room) : null) : null, - isAuthor && canClearnet ? renderClearnetSwitch("rooms", room.rootId, isClearnet) : null - ), + isAuthor && canClearnet ? div({ class: "card-chips-row" }, renderClearnetSwitch("rooms", room.rootId, isClearnet)) : null, restricted ? null : renderCover(room, "tribe-detail-image"), + renderDetailShare(room.rootId, roomHref(room), room.title), restricted || !room.description ? null : p({ class: "tribe-side-description" }, ...renderStyledText(room.description)), - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.createdAtLabel || "Created at"), - td({ class: "tribe-info-value", colspan: "3" }, moment(room.createdAt).format("YYYY/MM/DD HH:mm")) - ), - restricted ? null : tr(td({ class: "tribe-info-value", colspan: "4" }, userLink(room.author))) - ), live || restricted ? div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.roomParticipants}: ${safeArr(room.members).length}`) ) : null, inviteActions.length ? div({ class: "tribe-side-actions" }, ...inviteActions) : null, + !isAuthor && !room.tribeId && safeArr(room.members).includes(userId) + ? div({ class: "tribe-side-actions" }, + form({ method: "POST", action: `/rooms/leave/${encodeURIComponent(room.rootId)}` }, + button({ type: "submit", class: "tribe-action-btn danger-btn" }, i18n.tribeLeaveButton) + ) + ) + : null, isAuthor && !room.isClosed ? div({ class: "tribe-side-actions housing-status-row" }, span({ class: "card-label" }, `${i18n.statusLabel || "Status"}: `), @@ -407,7 +420,8 @@ exports.singleRoomView = async (room, params = {}) => { restricted || !safeArr(room.tags).length ? null : div({ class: "card-tags" }, ...room.tags.map(t => a({ href: `/search?query=%23${encodeURIComponent(t)}`, class: "tag-link" }, `#${t}`))), - ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null + ownerActions.length ? div({ class: "tribe-side-actions owner-actions" }, ...ownerActions) : null, + null ) const main = div({ class: "tribe-main" }, diff --git a/src/views/school_view.js b/src/views/school_view.js index eb6c031..bce2527 100644 --- a/src/views/school_view.js +++ b/src/views/school_view.js @@ -1,7 +1,8 @@ const { div, h2, h3, p, section, button, form, a, span, textarea, br, input, label, select, option, table, tr, td, th, details, summary, datalist, progress, hr } = require("../server/node_modules/hyperaxe") const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderOpinionsVoting, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderCardTop, renderOpinionsVoting, renderEngagement, renderInviteQrCard, renderSubscriptionBox, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderPdfDocument, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") +const { renderCommentsSection } = require("./comments_view") const opinionCategories = require("../backend/opinion_categories") const { config } = require("../server/SSB_server.js") const { renderStyledText, renderStyledHtml } = require("../backend/renderStyledText") @@ -87,18 +88,20 @@ const renderCourseCard = (course, filter, params = {}) => { const url = `/school/course/${encodeURIComponent(course.id)}` const isOwn = String(course.author) === String(userId) return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(course.id, url, { - spread: (params.spreadMap && params.spreadMap.get(course.id)) || null, - author: course.author, - favKind: 'school', - isFavorite: course.isFavorite, - reportTitle: course.title, - returnTo: buildReturnTo(filter, params), - deleteAction: isOwn ? contentDeleteAction('schoolCourse', course.id) : undefined - }) - ), + renderCardTop(course.id, url, { + date: course.createdAt, + chips: [renderCourseChips(course, params.subscriptions && (String(course.author) === String(userId) || safeArr(course.students).includes(userId)) ? { in: params.subscriptions.mine.has(course.rootId || course.id) || String(course.author) === String(userId) } : null)], + spread: (params.spreadMap && params.spreadMap.get(course.id)) || null, + author: course.author, + favKind: 'school', + isFavorite: course.isFavorite, + reportTitle: course.title, + returnTo: buildReturnTo(filter, params), + deleteAction: isOwn ? contentDeleteAction('schoolCourse', course.id) : undefined + }), + course.startDate ? div({ class: "tribe-card-members" }, + span({ class: "tribe-members-count" }, `${i18n.schoolStartDate}: ${moment(course.startDate).format("YYYY/MM/DD HH:mm")}`) + ) : null, div({ class: "tribe-card-image-wrapper" }, a({ href: url }, renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) @@ -107,14 +110,11 @@ const renderCourseCard = (course, filter, params = {}) => { div({ class: "tribe-card-body" }, h2({ class: "tribe-card-title" }, a({ href: url }, course.title)), renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), - renderCourseChips(course, params.subscriptions && (String(course.author) === String(userId) || safeArr(course.students).includes(userId)) ? { in: params.subscriptions.mine.has(course.rootId || course.id) || String(course.author) === String(userId) } : null), course.description ? p({ class: "tribe-card-description" }, ...renderStyledText(course.description)) : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) ), - course.startDate ? div({ class: "tribe-card-members" }, - span({ class: "tribe-members-count" }, `${i18n.schoolStartDate}: ${moment(course.startDate).format("YYYY/MM/DD HH:mm")}`) - ) : null + null ) ) } @@ -147,7 +147,7 @@ const renderCourseForm = (filter, course = {}, params = {}) => { label(i18n.description), br, textarea({ name: "description", rows: 5, required: true, maxlength: "2000", placeholder: i18n.schoolCourseDescriptionPlaceholder }, course.description || ""), br, label(i18n.blogImage), br, - input({ type: "file", name: "image", accept: "image/*" }), br(), br(), + (isEdit && renderMediaLockNote(course)) || input({ type: "file", name: "image", accept: "image/*" }), br(), br(), label(i18n.schoolTags), br, input({ type: "text", name: "tags", placeholder: i18n.schoolTagsPlaceholder, value: Array.isArray(course.tags) ? course.tags.join(", ") : (course.tags || "") }), br, ...(reach === "PAID" @@ -396,42 +396,45 @@ exports.singleCourseView = async (course, lessons = [], certificates = [], param const nextLesson = isStudent ? lessons.find(l => !l.locked && !l.completed) : null const courseSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(course.id, returnTo, { spread: params.spreads || null, author: course.author, favKind: 'school', isFavorite: course.isFavorite, returnTo, reportTitle: course.title, deleteAction: isTeacher ? contentDeleteAction('schoolCourse', course.id) : undefined }) - ), + renderCardTop(course.id, returnTo, { arrow: false, + date: course.createdAt, + chips: [ + course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, + course.status === "CLOSED" + ? renderStateChip("closed", "โœ—", i18n.schoolClosed) + : renderStateChip("mutuals", "โœ“", i18n.schoolOngoing), + renderPriceChip(course), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('school', course.title, course.id) : null), + courseApproved ? renderStateChip("mutuals", "๐ŸŽ“", i18n.schoolApproved) : null, + params.subscription && (isTeacher || isStudent) + ? ((isTeacher || params.subscription.subscribed === true) + ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) + : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) + : null + ], + bylineExtra: [renderEditedChip(course.id, course.rootId)], + spread: params.spreads || null, + author: course.author, + favKind: 'school', + isFavorite: course.isFavorite, + returnTo, + reportTitle: course.title, + deleteAction: isTeacher ? contentDeleteAction('schoolCourse', course.id) : undefined + }), h2({ class: "tribe-card-title" }, course.title), renderStarRating(course.opinions, safeArr(course.opinions_inhabitants).length), - div({ class: "card-chips-row" }, - course.visibility === "INVITE" ? renderStateChip("whole", "\uD83D\uDD11", i18n.chatStatusInviteOnly) : null, - course.status === "CLOSED" - ? renderStateChip("closed", "โœ—", i18n.schoolClosed) - : renderStateChip("mutuals", "โœ“", i18n.schoolOngoing), - renderPriceChip(course), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('school', course.title, course.id) : null), - isTeacher && canClearnet ? renderClearnetSwitch('school', course.rootId || course.id, isClearnet) : null, - courseApproved ? renderStateChip("mutuals", "๐ŸŽ“", i18n.schoolApproved) : null, - params.subscription && (isTeacher || isStudent) - ? ((isTeacher || params.subscription.subscribed === true) - ? renderStateChip("mutuals", "\u2709", i18n.subscriptionOn) - : renderStateChip("closed", "\u2709", i18n.subscriptionOff)) - : null - ), + isTeacher && canClearnet ? renderClearnetSwitch('school', course.rootId || course.id, isClearnet) : null, renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + renderDetailShare(course.id, returnTo, course.title), course.description ? p({ class: "tribe-side-description" }, ...renderStyledText(course.description)) : null, - table({ class: "tribe-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.schoolCreatedAt), - td({ class: "tribe-info-value", colspan: "3" }, moment(course.createdAt).format("YYYY/MM/DD HH:mm")) - ), - course.startDate ? tr( - td({ class: "tribe-info-label" }, i18n.schoolStartDate), - td({ class: "tribe-info-value", colspan: "3" }, moment(course.startDate).format("YYYY/MM/DD HH:mm")) - ) : null, - tr( - td({ class: "tribe-info-label" }, i18n.schoolTeacher), - td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) - ) - ), + course.startDate + ? table({ class: "tribe-info-table" }, + tr( + td({ class: "tribe-info-label" }, i18n.schoolStartDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(course.startDate).format("YYYY/MM/DD HH:mm")) + ) + ) + : null, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.schoolStudents}: ${course.students.length}`) ), @@ -741,8 +744,10 @@ exports.singleCourseView = async (course, lessons = [], certificates = [], param isStudent && !isTeacher ? renderOpinionsVoting('/school/opinions', course.id, course.opinions, returnTo, course.opinions_inhabitants) : null, - null - ) + null), + Array.isArray(params.comments) + ? renderCommentsSection({ action: `/school/course/${encodeURIComponent(course.rootId || course.id)}/comments`, comments: params.comments, returnTo: `/school/course/${encodeURIComponent(course.rootId || course.id)}` }) + : null ) ) ) @@ -763,9 +768,10 @@ exports.clearnetCourseView = async (course, lessons = []) => { .cn-course-section h2{color:var(--fg);font-size:18px;text-transform:uppercase;letter-spacing:2px;margin:24px 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)} .cn-course-section p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:15px} .cn-lesson{background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:12px} -.cn-lesson h3{color:var(--fg);margin:0 0 8px 0;font-size:16px} -.cn-lesson p{color:var(--fg-soft);white-space:pre-wrap;line-height:1.6;font-size:14px;margin:0} -.cn-lesson-date{color:var(--fg-dim);font-size:12px;margin-top:8px} +.cn-lesson-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 12px;margin:0 0 8px 0} +.cn-lesson h3{color:var(--fg);margin:0;font-size:16px} +.cn-lesson-text{color:var(--fg-soft);line-height:1.6;font-size:14px;margin:0} +.cn-lesson-date{color:var(--fg-dim);font-size:12px} ` const body = `

${title}

@@ -780,9 +786,8 @@ exports.clearnetCourseView = async (course, lessons = []) => { ${desc ? `

${esc(i18n.description)}

${desc}

` : ""} ${visibleLessons.length ? `

${esc(i18n.schoolLessons)} (${visibleLessons.length})

${visibleLessons.map(lesson => `
-

${esc(lesson.title || "")}

-

${esc(lesson.text || "")}

- ${lesson.sessionDate ? `
๐Ÿ“… ${esc(new Date(lesson.sessionDate).toISOString().slice(0, 10))}
` : ""} +

${esc(lesson.title || "")}

${lesson.sessionDate ? `๐Ÿ“… ${esc(new Date(lesson.sessionDate).toISOString().slice(0, 10))}` : ""}
+
${renderRichText(lesson.text || "")}
`).join("")}
` : ""} ` return renderClearnetPage({ @@ -805,7 +810,7 @@ const renderMaterial = (material, course, isTeacher) => { else if (ref && ref.length === 4) { const kind = ref[1], name = ref[2], blobId = ref[3] const url = `/blob/${encodeURIComponent(blobId)}` - if (kind === "pdf") body = div({ class: "pdf-viewer-container", id: `pdf-${material.id.slice(1, 9)}`, "data-pdf-url": url }, a({ href: url }, `๐Ÿ“Ž ${name || "PDF"}`)) + if (kind === "pdf") body = renderPdfDocument(url, { title: name || "PDF", preview: true }) else if (kind === "image") body = img({ src: url, alt: name, class: "school-material-image" }) else if (kind === "video") body = video({ controls: true, src: url, class: "post-video", preload: 'metadata' }) else if (kind === "audio") body = audio({ controls: true, src: url }) @@ -833,31 +838,29 @@ exports.singleLessonView = async (course, lesson, materials = [], params = {}) = const lessonUrl = `/school/lesson/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` const lessonSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(lesson.id, lessonUrl, { - author: lesson.author || course.author, - reportTitle: lesson.title || course.title, - returnTo: courseUrl, - deleteAction: String(lesson.author || course.author) === String(userId) ? `/school/lesson/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` : undefined - }) - ), + renderCardTop(lesson.id, lessonUrl, { arrow: false, + date: lesson.createdAt, + chips: [renderCourseChips(course)], + author: lesson.author || course.author, + reportTitle: lesson.title || course.title, + returnTo: courseUrl, + deleteAction: String(lesson.author || course.author) === String(userId) ? `/school/lesson/delete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` : undefined + }), h2({ class: "tribe-card-title" }, a({ href: courseUrl }, course.title)), - renderCourseChips(course), renderMediaBlob(course.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), - table({ class: "tribe-info-table" }, - lesson.unit ? tr( - td({ class: "tribe-info-label" }, i18n.schoolLessonUnit), - td({ class: "tribe-info-value", colspan: "3" }, lesson.unit) - ) : null, - lesson.sessionDate ? tr( - td({ class: "tribe-info-label" }, i18n.schoolSessionDate), - td({ class: "tribe-info-value", colspan: "3" }, moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")) - ) : null, - tr( - td({ class: "tribe-info-label" }, i18n.schoolTeacher), - td({ class: "tribe-info-value", colspan: "3" }, userLink(course.author)) - ) - ), + renderDetailShare(lesson.id, lessonUrl, lesson.title || course.title), + lesson.unit || lesson.sessionDate + ? table({ class: "tribe-info-table" }, + lesson.unit ? tr( + td({ class: "tribe-info-label" }, i18n.schoolLessonUnit), + td({ class: "tribe-info-value", colspan: "3" }, lesson.unit) + ) : null, + lesson.sessionDate ? tr( + td({ class: "tribe-info-label" }, i18n.schoolSessionDate), + td({ class: "tribe-info-value", colspan: "3" }, moment(lesson.sessionDate).format("YYYY/MM/DD HH:mm")) + ) : null + ) + : null, isStudent ? div({ class: "tribe-side-actions" }, form({ method: "POST", action: `/school/lesson/complete/${encodeURIComponent(course.id)}/${encodeURIComponent(lesson.id)}` }, @@ -917,7 +920,8 @@ exports.singleLessonView = async (course, lesson, materials = [], params = {}) = button({ type: "submit" }, i18n.schoolAddMaterial) ) ) - : null + : null, + null ) const tpl = template( @@ -926,8 +930,7 @@ exports.singleLessonView = async (course, lesson, materials = [], params = {}) = section(renderModeButtons("all", false, (params && params.modesAvail) || null)), section(div({ class: "tribe-details" }, lessonSide, lessonMain)) ) - const hasPdf = safeArr(materials).some(m => !m.locked && /\[pdf:/.test(String(m.media || ""))) - return hasPdf ? `${tpl}` : tpl + return tpl } exports.schoolInvitePage = (code) => { diff --git a/src/views/search_view.js b/src/views/search_view.js index e494b3d..de02589 100644 --- a/src/views/search_view.js +++ b/src/views/search_view.js @@ -1,6 +1,6 @@ -const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); -const { template, i18n, userLink, renderContentActions, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel } = require('./main_views'); +const { template, i18n, userLink, renderCardTop, renderShareButton, renderCommentsPill, renderOpinionsPill, renderCardMetaRow, renderModuleStats, CONTENT_FAV_KIND, CONTENT_SPREADABLE, CONTENT_TORRENTABLE, paged, currentPerPage, LIST_PAGE_SIZES, pageSizeLabel, renderPdfDocument } = require('./main_views'); const moment = require("../server/node_modules/moment"); const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); const { renderStyledText } = require('../backend/renderStyledText'); @@ -138,7 +138,6 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = } }; - let hasDocument = false; const blobImg = (value) => { if (!value) return null; @@ -299,11 +298,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = br(), content.description ? div({ class: 'card-field' }, span({ class: 'card-value' }, content.description)) : null, br(), - div({ - id: `pdf-container-${content.key || content.url}`, - class: 'pdf-viewer-container', - 'data-pdf-url': `/blob/${encodeURIComponent(content.url)}` - }), + renderPdfDocument(`/blob/${encodeURIComponent(content.url)}`, { title: content.title }), br(), content.tags && content.tags.length ? div({ class: 'card-tags' }, content.tags.map(tag => @@ -627,7 +622,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = } }; - const typeLabel = (type) => String(i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)] || type || '').toUpperCase(); + const typeLabel = (type) => String((type === 'comment' ? i18n.typeComment : i18n['type' + String(type || '').charAt(0).toUpperCase() + String(type || '').slice(1)]) || type || '').toUpperCase(); const totalResults = Object.values(results).reduce((n, msgs) => n + (Array.isArray(msgs) ? msgs.length : 0), 0); const resultStats = renderModuleStats(totalResults, Object.entries(results) @@ -641,69 +636,43 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = { class: "search-result-group" }, ...msgs.map((msg) => { const content = msg.value.content || {}; - const created = moment(msg.timestamp).format("YYYY/MM/DD HH:mm"); - if (content.type === 'document') hasDocument = true; const contentHtml = renderContentHtml(content); - let author; - let authorUrl = '#'; - if (content.type === 'market') { - author = content.seller || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.seller)}`; - } else if (content.type === 'event') { - author = content.organizer || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.organizer)}`; - } else if (content.type === 'transfer') { - author = content.from || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.from)}`; - } else if (content.type === 'post' || content.type === 'about') { - author = msg.value.author || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(msg.value.author)}`; - } else if (content.type === 'report') { - author = content.author || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`; - } else if (content.type === 'votes') { - author = content.createdBy || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.createdBy || 'Anonymous')}`; - } else if (content.type === 'shop' || content.type === 'shopProduct' || content.type === 'chat' || content.type === 'gameScore') { - author = content.author || content.player || msg.value.author || i18n.anonymous || "Anonymous"; - authorUrl = `/author/${encodeURIComponent(content.author || content.player || msg.value.author || 'Anonymous')}`; - } else { - author = content.author; - authorUrl = `/author/${encodeURIComponent(content.author || 'Anonymous')}`; - } + const bylineAuthor = content.type === 'market' ? content.seller + : content.type === 'event' ? content.organizer + : content.type === 'transfer' ? content.from + : content.type === 'post' || content.type === 'about' ? msg.value.author + : content.type === 'votes' ? content.createdBy + : content.type === 'shop' || content.type === 'shopProduct' || content.type === 'chat' || content.type === 'gameScore' ? (content.author || content.player || msg.value.author) + : content.author; const contentId = msg.key; const isOwn = msg.value.author && String(msg.value.author) === String(userId); + const viewHref = getViewDetailsActionForSearch(content.type, contentId, content); + const shareTitle = content.title || content.question || content.concept || ''; return div({ class: 'trending-card search-card' + (isOwn ? ' own-content' : '') }, [ - div({ class: 'card-header activity-card-header' }, - div({ class: 'card-chips-row' }, + renderCardTop(contentId, viewHref, { + date: msg.timestamp || msg.value.timestamp, + bylineAuthor: typeof bylineAuthor === 'string' && bylineAuthor.startsWith('@') ? bylineAuthor : undefined, + chips: [div({ class: 'card-chips-row' }, span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(i18n['type' + String(content.type || '').charAt(0).toUpperCase() + String(content.type || '').slice(1)] || content.type || '').toUpperCase()) ) - ), - renderContentActions(contentId, getViewDetailsActionForSearch(content.type, contentId, content), { - author: msg.value.author, - reportTitle: content.title || content.question || content.concept || '', - spread: CONTENT_SPREADABLE.has(content.type) ? ((spreadMap instanceof Map && spreadMap.get(contentId)) || null) : undefined, - ...(CONTENT_TORRENTABLE.has(content.type) && typeof content.url === 'string' && content.url.startsWith('&') - ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } - : {}), - ...(CONTENT_FAV_KIND[content.type] - ? { favKind: CONTENT_FAV_KIND[content.type], isFavorite: favIndex instanceof Map ? favIndex.get(String(contentId)) === CONTENT_FAV_KIND[content.type] : false, returnTo: `/search?query=${encodeURIComponent(hashtag ? `#${hashtag}` : String(query || ''))}` } - : {}) - }) - ), + )], + author: msg.value.author, + reportTitle: shareTitle, + spread: CONTENT_SPREADABLE.has(content.type) ? ((spreadMap instanceof Map && spreadMap.get(contentId)) || null) : undefined, + ...(CONTENT_TORRENTABLE.has(content.type) && typeof content.url === 'string' && content.url.startsWith('&') + ? { torrentFrom: { blobId: content.url, name: content.title || content.fileName || '' } } + : {}), + ...(key !== 'comment' && CONTENT_FAV_KIND[content.type] + ? { favKind: CONTENT_FAV_KIND[content.type], isFavorite: favIndex instanceof Map ? favIndex.get(String(contentId)) === CONTENT_FAV_KIND[content.type] : false, returnTo: `/search?query=${encodeURIComponent(hashtag ? `#${hashtag}` : String(query || ''))}` } + : {}) + }), div({ class: 'card-section search-card-body' }, - contentHtml, - author - ? p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${created}`), - (authorUrl && authorUrl !== '#' && authorUrl.startsWith('/author/')) - ? userLink(decodeURIComponent(authorUrl.replace(/^\/author\//, ''))) - : a({ href: authorUrl, class: 'user-link' }, `${author}`) - ): null - ) + contentHtml + ), + renderCardMetaRow(renderOpinionsPill(contentId, viewHref), renderCommentsPill(contentId, viewHref), renderShareButton(contentId, viewHref, shareTitle)) ]); }) ) @@ -727,12 +696,6 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = ) ); - if (hasDocument) { - html += ` - - `; - } - return html; }; diff --git a/src/views/settings_view.js b/src/views/settings_view.js index 6e1f6f0..2ae0c33 100644 --- a/src/views/settings_view.js +++ b/src/views/settings_view.js @@ -1,8 +1,10 @@ const { form, button, div, h2, h3, p, section, select, option, input, br, a, label, span, img, strong, table, tr, td, ul, li } = require("../server/node_modules/hyperaxe"); const fs = require('fs'); const path = require('path'); -const { getConfig } = require('../configs/config-manager.js'); -const { template, selectedLanguage, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, MOBILE_THEMES, userLink } = require('./main_views'); +const { getConfig, CONNECTIONS_MIN, CONNECTIONS_MAX } = require('../configs/config-manager.js'); +const mainViews = require('./main_views'); +const { template, i18n, setLanguage, INBOX_BOT_ORDER, inboxBotLabel, THEMES, userLink } = mainViews; +const { MOBILE_THEMES } = mainViews; const i18nBase = require("../client/assets/translations/i18n"); const moment = require("../server/node_modules/moment"); const { WORKFLOWS, currentWorkflow } = require('../models/workflows_model'); @@ -93,7 +95,7 @@ const settingsView = (params) => { ); const languageOption = (longName, shortName) => { - return shortName === selectedLanguage + return shortName === mainViews.selectedLanguage ? option({ value: shortName, selected: true }, longName) : option({ value: shortName }, longName); }; @@ -312,6 +314,17 @@ const settingsView = (params) => { ) ) ), + section({ id: "connections" }, + div({ class: "tags-header" }, + h2(i18n.settingsConnectionsTitle), + p(i18n.settingsConnectionsDesc), + form( + { action: "/settings/connections", method: "POST" }, + input({ type: "number", id: "connections", name: "connections", min: CONNECTIONS_MIN, max: CONNECTIONS_MAX, step: 1, required: true, value: currentConfig.connections }), br(), br(), + button({ type: "submit" }, i18n.saveSettings) + ) + ) + ), modOn('phone') ? section({ id: "phone" }, div({ class: "tags-header" }, h2(i18n.phoneTitle), diff --git a/src/views/shops_view.js b/src/views/shops_view.js index ce324ef..59b69f3 100644 --- a/src/views/shops_view.js +++ b/src/views/shops_view.js @@ -1,10 +1,10 @@ const { div, h2, p, section, button, form, a, span, textarea, br, input, label, select, option, img, progress, video, audio, table, tr, td, hr } = require("../server/node_modules/hyperaxe") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderSpreadButton, renderOpinionsVoting, renderEngagement, renderInviteQrCard , renderSpreadEditWarning, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged } = require("./main_views") +const { renderCallButton, clearnetItemHref, template, i18n, userLink, renderStateChip, renderLifespanChip, renderOpinionsVoting, renderEngagement, renderInviteQrCard, renderSpreadEditWarning, renderCardTop, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") const { renderStyledText } = require("../backend/renderStyledText") -const { renderMapLocationUrl, renderMapEmbed, renderMapLocationVisitLabel } = require("./maps_view") +const { renderMapEmbed } = require("./maps_view") const opinionCategories = require("../backend/opinion_categories") const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderClearnetUrlBlock, renderClearnetPage, renderClearnetSearchForm, renderEncryptedChip, blobUrl: cnBlobUrl, escapeHtml: cnEscapeHtml, renderRichText: cnRichText, renderKindTag: cnKindTag } = require("./clearnet_view") @@ -72,20 +72,12 @@ const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { const returnTo = buildReturnTo(filter, params) const isAuthor = String(shop.author) === String(userId) + const shopHref = `/shops/${encodeURIComponent(shop.key)}` + return div({ class: "tribe-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreadMap && params.spreadMap.get(shop.key) || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) - ), - div({ class: "tribe-card-image-wrapper" }, - a({ href: `/shops/${encodeURIComponent(shop.key)}` }, - renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) - ) - ), - div({ class: "tribe-card-body" }, - div({ class: "shop-title-row" }, - h2({ class: "tribe-card-title" }, a({ href: `/shops/${encodeURIComponent(shop.key)}` }, shop.title || i18n.shopUntitled)) - ), - div({ class: "card-chips-row" }, + renderCardTop(shop.key, shopHref, { + date: shop.createdAt, + chips: [ shop.visibility === "CLOSED" ? renderStateChip("closed", "โœ—", i18n.shopClosed) : renderStateChip("mutuals", "โœ“", i18n.shopOpen), @@ -94,10 +86,22 @@ const renderShopCard = exports.renderShopCard = (shop, filter, params = {}) => { shop.subscriptionIn === true ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) : (shop.subscriptionIn === false ? renderStateChip("closed", "โœ‰", i18n.subscriptionOff) : null) + ], + spread: params.spreadMap && params.spreadMap.get(shop.key) || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined + }), + div({ class: "tribe-card-image-wrapper" }, + a({ href: shopHref }, + renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-card-hero-image' }) + ) + ), + div({ class: "tribe-card-body" }, + div({ class: "shop-title-row" }, + h2({ class: "tribe-card-title" }, a({ href: shopHref }, shop.title || i18n.shopUntitled)) ), div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.shopProducts}: ${shop.productCount || 0}`) - ) + ), + null ) ) } @@ -109,9 +113,7 @@ const renderProductCard = (product, shopId, returnTo, params = {}) => { const productUrl = `/shops/product/${encodeURIComponent(product.key)}?shopId=${encodeURIComponent(shopId)}` return div({ class: "shop-product-card" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreadMap && params.spreadMap.get(product.key) || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, reportTitle: product.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) - ), + renderCardTop(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { date: product.createdAt, spread: params.spreadMap && params.spreadMap.get(product.key) || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, reportTitle: product.title, returnTo, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }), product.image ? div({ class: "shop-product-media" }, a({ href: productUrl }, renderMediaBlob(product.image))) : null, div({ class: "shop-product-body" }, product.shopTitle ? p(a({ href: `/shops/${encodeURIComponent(shopId)}`, class: "user-link" }, product.shopTitle)) : null, @@ -132,7 +134,8 @@ const renderProductCard = (product, shopId, returnTo, params = {}) => { actions.push(a({ href: productUrl, class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy)); } return actions.length ? div({ class: "shop-product-actions" }, ...actions) : null; - })() + })(), + null ) ) } @@ -212,7 +215,7 @@ const renderProductForm = (shopId, product = {}, isEdit = false, returnTo = "", label(i18n.shopProductStock), br, input({ type: "number", name: "stock", min: "0", value: product.stock !== undefined ? product.stock : 1 }), br(), br(), label(i18n.blogImage || "Upload media (max-size: 75MB)"), br, - input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), + (isEdit && renderMediaLockNote(product)) || input({ type: "file", name: "image", accept: "image/*,video/*" }), br(), br(), input({ type: "hidden", name: "featured", value: "0" }), label(i18n.shopProductFeatured), input({ id: "featured", type: "checkbox", name: "featured", value: "1", class: "meme-checkbox", ...(product.featured ? { checked: true } : {}) }), @@ -289,33 +292,31 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para products = safeArr(products) const shopSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { spread: params.spreads || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined }) - ), + renderCardTop(shop.key, `/shops/${encodeURIComponent(shop.key)}`, { arrow: false, + date: shop.createdAt, + chips: [ + shop.visibility === "CLOSED" + ? renderStateChip("closed", "โœ—", i18n.shopClosed) + : renderStateChip("mutuals", "โœ“", i18n.shopOpen), + shop.encrypted ? renderStateChip("encrypted", "๐Ÿ”’", i18n.encryptedChipLabel || "E2E") : null, + renderLifespanChip(shop.lifetime, i18n), + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('shops', shop.title, shop.key) : null), + shop.subscription + ? ((isAuthor || shop.subscription.subscribed === true) + ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) + : renderStateChip("closed", "โœ‰", i18n.subscriptionOff)) + : null + ], + spread: params.spreads || null, author: shop.author, favKind: 'shops', isFavorite: shop.isFavorite, reportTitle: shop.title, deleteAction: isAuthor ? contentDeleteAction('shop', shop.key) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, shop.title || i18n.shopUntitled) ), - div({ class: "card-chips-row" }, - shop.visibility === "CLOSED" - ? renderStateChip("closed", "โœ—", i18n.shopClosed) - : renderStateChip("mutuals", "โœ“", i18n.shopOpen), - shop.encrypted ? renderStateChip("encrypted", "๐Ÿ”’", i18n.encryptedChipLabel || "E2E") : null, - renderLifespanChip(shop.lifetime, i18n), - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('shops', shop.title, shop.key) : null), - isAuthor && canClearnet ? renderClearnetSwitch('shops', shop.rootId || shop.key, isClearnet) : null, - shop.subscription - ? ((isAuthor || shop.subscription.subscribed === true) - ? renderStateChip("mutuals", "โœ‰", i18n.subscriptionOn) - : renderStateChip("closed", "โœ‰", i18n.subscriptionOff)) - : null - ), + isAuthor && canClearnet ? renderClearnetSwitch('shops', shop.rootId || shop.key, isClearnet) : null, renderMediaBlob(shop.image, '/assets/images/default-avatar.png', { class: 'tribe-detail-image' }), + renderDetailShare(shop.key, `/shops/${encodeURIComponent(shop.key)}`, shop.title), shop.description ? p({ class: "tribe-side-description" }, ...renderStyledText(shop.description)) : null, table({ class: "tribe-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), - td({ class: "tribe-info-value", colspan: "3" }, moment(shop.createdAt).format("YYYY/MM/DD HH:mm")) - ), shop.location ? tr( td({ class: "tribe-info-label" }, i18n.shopLocation), td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.location)) @@ -327,10 +328,7 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para shop.url ? tr( td({ class: "tribe-info-label" }, i18n.shopUrl), td({ class: "tribe-info-value", colspan: "3" }, ...renderStyledText(shop.url)) - ) : null, - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(shop.author)) - ) + ) : null ), renderMapEmbed(params.mapData, shop.mapUrl), div({ class: "tribe-card-members" }, @@ -399,7 +397,8 @@ exports.singleShopView = async (shop, filter, products = [], comments = [], para : null, safeArr(shop.tags).length ? div({ class: "tribe-side-tags" }, safeArr(shop.tags).map(tag => a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`))) - : null + : null, + null ) const shopMain = div({ class: "tribe-main" }, @@ -434,15 +433,13 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => const stock = Number(product.stock) || 0 const productSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { spread: params.spreads || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, returnTo, reportTitle: product.title, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }) - ), + renderCardTop(product.key, `/shops/product/${encodeURIComponent(product.key)}`, { arrow: false, date: product.createdAt, bylineExtra: [renderEditedChip(product.key, product.rootId)], spread: params.spreads || null, author: product.author, favKind: 'shopProducts', isFavorite: product.isFavorite, returnTo, reportTitle: product.title, deleteAction: isAuthor ? contentDeleteAction('shopProduct', product.key) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, product.title || i18n.shopProductUntitled) ), renderStarRating(product.opinions, safeArr(product.opinions_inhabitants).length), product.image ? renderMediaBlob(product.image, null, { class: "tribe-detail-image" }) : null, - div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`), + renderDetailShare(product.key, `/shops/product/${encodeURIComponent(product.key)}`, product.title), div({ class: "confirmations-block stock-block" }, div({ class: "card-field" }, span({ class: "card-label" }, `${i18n.shopProductStock}: `), @@ -450,15 +447,6 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => ), progress({ class: "confirmations-progress stock-progress", value: Math.min(stock, 100), max: 100 }) ), - table({ class: "tribe-info-table jobs-info-table" }, - tr( - td({ class: "tribe-info-label" }, i18n.shopCreatedAt || "CREATED"), - td({ class: "tribe-info-value", colspan: "3" }, moment(product.createdAt).format("YYYY/MM/DD HH:mm")) - ), - tr( - td({ class: "tribe-info-value", colspan: "4" }, userLink(product.author)) - ) - ), div({ class: "tribe-side-actions" }, form({ method: "GET", action: `/shops/${encodeURIComponent(product.shopId)}` }, button({ type: "submit", class: "tribe-action-btn" }, `โ† ${i18n.shopBackToShop}`) @@ -475,6 +463,8 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => : null ) + const canBuy = !isAuthor && stock > 0 + const priceChip = div({ class: "price-chip" }, `${Number(product.price || 0).toFixed(6)} ECO`) const productMain = div({ class: "tribe-main" }, product.description ? div({ class: "job-section" }, @@ -482,13 +472,12 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => p({ class: "tribe-side-description" }, ...renderStyledText(product.description)) ) : null, - !isAuthor && stock > 0 - ? div({ class: "job-section" }, + canBuy ? null : priceChip, + canBuy + ? div({ class: "job-section buy-box" }, form({ method: "POST", action: `/shops/product/buy/${encodeURIComponent(product.key)}`, class: "shop-buy-form" }, input({ type: "hidden", name: "returnTo", value: returnTo }), - p({ class: "shop-buy-form-note" }, i18n.shopBuyEncryptedNote || "Your delivery details are sent encrypted only to the shop owner."), - label(i18n.shopBuyDeliveryAddress || "Delivery address"), - br(), + div({ class: "title-with-chip" }, label(i18n.shopBuyDeliveryAddress || "Delivery address"), renderEncryptedChip(i18n)), textarea({ maxlength: "5000", name: "deliveryAddress", required: true, rows: 3, placeholder: i18n.shopBuyDeliveryAddressPlaceholder || "" }), br(), br(), @@ -501,6 +490,8 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => br(), textarea({ maxlength: "5000", name: "notes", rows: 2, placeholder: i18n.shopBuyNotesPlaceholder || "" }), br(), + priceChip, + br(), br(), button({ type: "submit", class: "buy-btn" }, i18n.marketActionsBuy || i18n.shopBuy) ) @@ -510,8 +501,7 @@ exports.singleProductView = async (product, shop, comments = [], params = {}) => !isAuthor && params.canRate ? renderOpinionsVoting('/shops/product/opinions', product.key, product.opinions, returnTo, product.opinions_inhabitants) : null, - renderCommentsSection(product.key, returnTo, comments) - ) + renderCommentsSection(product.key, returnTo, comments)) ) return template( @@ -641,10 +631,12 @@ exports.clearnetShopView = async (shop, products = []) => { const n = Number(v); return Number.isFinite(n) ? n.toFixed(6) : '0.000000'; }; + const shopHref = clearnetItemHref('shops', shop.title, shop.key); const productCards = (products || []).filter(p => Number(p.stock) > 0 || p.featured).map(prod => { const pImg = cnBlobUrl(prod.image); const pKind = /^\[video:/.test(String(prod.image || '')) ? 'video' : /^\[audio:/.test(String(prod.image || '')) ? 'audio' : 'img'; return `
+ ${pImg ? (pKind === 'img' ? `` : `<${pKind} class="cn-product-img" src="${pImg}" controls preload="metadata">`) : ''}

${cnEscapeHtml(prod.title || '')}

${prod.description ? `

${cnRichText(prod.description)}

` : ''} @@ -663,7 +655,9 @@ exports.clearnetShopView = async (shop, products = []) => { .cn-shop-meta{display:flex;gap:12px;flex-wrap:wrap;background:var(--bg-sub);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:13px;color:var(--fg-soft)} .cn-shop-meta-item{display:inline-flex;align-items:center;gap:6px} .cn-products{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px} -.cn-product{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:16px;transition:border-color .15s ease} +.cn-product{position:relative;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:16px;transition:border-color .15s ease} +.cn-product-cover{position:absolute;inset:0;z-index:2;border-radius:8px} +.cn-product video,.cn-product audio{position:relative;z-index:3} .cn-product:hover{border-color:var(--fg)} .cn-product-img{width:100%;height:180px;object-fit:cover;border-radius:6px;margin-bottom:10px;background:#000;border:1px solid var(--border)} .cn-product-title{color:var(--fg);font-size:16px;margin:0 0 8px 0;font-weight:600} @@ -699,6 +693,48 @@ exports.clearnetShopView = async (shop, products = []) => { }); }; +exports.clearnetProductView = async (shop, product) => { + const shopHref = clearnetItemHref('shops', shop.title, shop.key); + const price = Number.isFinite(Number(product.price)) ? Number(product.price).toFixed(6) : '0.000000'; + const media = cnBlobUrl(product.image); + const kind = /^\[video:/.test(String(product.image || '')) ? 'video' : /^\[audio:/.test(String(product.image || '')) ? 'audio' : 'img'; + const mediaHtml = !media ? '' : kind === 'img' + ? `${cnEscapeHtml(product.title || '')}` + : `<${kind} class="cn-product-page-img" src="${media}" controls preload="metadata">`; + const extraCss = ` +.cn-product-back{display:inline-block;margin-bottom:16px;color:var(--fg-soft);text-decoration:none} +.cn-product-back:hover{color:var(--fg)} +.cn-product-page{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start} +.cn-product-page-img{display:block;max-width:420px;width:100%;border:1px solid var(--border);border-radius:8px;background:#000} +.cn-product-page-body{flex:1 1 320px;min-width:0} +.cn-product-page-title{color:var(--fg);font-size:26px;font-weight:700;margin:0 0 12px 0} +.cn-product-page-desc{color:var(--fg-soft);line-height:1.6;margin:0 0 16px 0} +.cn-product-page-price{color:var(--fg);background:var(--bg-sub);border:1px solid var(--fg);display:inline-block;padding:6px 12px;border-radius:4px;font-weight:bold;font-size:16px} +.cn-product-page-stock{color:var(--fg-dim);font-size:13px;margin:10px 0 0 0;text-transform:uppercase;letter-spacing:1px} +`; + const body = ` + โ† ${cnEscapeHtml(shop.title || i18n.shopBackToShop)} +
+ ${mediaHtml} +
+

${cnEscapeHtml(product.title || '')}

+ ${product.description ? `
${cnRichText(product.description)}
` : ''} + ${price} ECO +

${cnEscapeHtml(i18n.shopProductStock)}: ${Number(product.stock) || 0}

+
+
+`; + return renderClearnetPage({ + title: `${product.title || 'Product'} | ${shop.title || 'Shop'} | Oasis`, + ogTitle: product.title || 'Oasis', + ogDescription: product.description || '', + ogImage: kind === 'img' ? media : null, + extraCss, + body, + hubFeedId: shop.author || null + }); +}; + exports.renderShopInvitePage = (code) => { const pageContent = div({ class: "invite-page" }, h2(i18n.tribeInviteCodeText, code), diff --git a/src/views/task_view.js b/src/views/task_view.js index 089e534..d35ce2a 100644 --- a/src/views/task_view.js +++ b/src/views/task_view.js @@ -1,7 +1,7 @@ -const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span, table, tr, td, img, video } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, input, select, option, a, br, textarea, label, span, table, tr, td, video } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); const moment = require("../server/node_modules/moment"); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderSpreadEditWarning, renderContentActions, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderEcoTax, renderSpreadEditWarning, renderCardTop, renderCardByline, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderEditedChip, renderDetailShare } = require("./main_views"); const { renderPhotoGallery, renderGalleryFields, imagesOf, renderZoomableImage } = require("./gallery_view"); const { renderTribeOriginChip } = require("./clearnet_view"); const { config } = require("../server/SSB_server.js"); @@ -131,19 +131,14 @@ const renderTaskItem = exports.renderTaskItem = (task, filter, spreadInfo, opts const isPrivate = String(task.isPublic || "").toUpperCase() === "PRIVATE"; const isAuthor = String(task.author) === String(userId); const origin = task.tribeOrigin || null; - const href = opts.href !== undefined ? opts.href : (origin ? origin.href : `/tasks/${encodeURIComponent(task.id)}`); + const detailHref = `/tasks/${encodeURIComponent(task.id)}`; + const href = opts.href !== undefined ? opts.href : (origin ? origin.href : detailHref); const cover = origin ? null : imagesOf(task)[0] const heroNode = cover ? div({ class: "tribe-card-image-wrapper" }, - a({ href: `/tasks/${encodeURIComponent(task.id)}` }, + a({ href: detailHref }, renderTaskMediaBlob(cover, { class: "tribe-card-hero-image" }) - ), - div({ class: "tribe-visit-btn-wrapper" }, - form({ method: "GET", action: `/tasks/${encodeURIComponent(task.id)}` }, - input({ type: "hidden", name: "filter", value: currentFilter }), - button({ type: "submit", class: "filter-btn" }, i18n.viewTask || "View Task") - ) ) ) : null; @@ -157,12 +152,15 @@ const renderTaskItem = exports.renderTaskItem = (task, filter, spreadInfo, opts ...(Array.isArray(opts.extraChips) ? opts.extraChips : []) ].filter(Boolean); - const headerActions = opts.headerActions !== undefined - ? opts.headerActions - : origin ? null : renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: spreadInfo || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, returnTo: `/tasks?filter=${encodeURIComponent(currentFilter)}`, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }); + const topOpts = { author: task.author, date: task.createdAt, chips }; + const cardTop = opts.headerActions !== undefined + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(topOpts), opts.headerActions) + : origin + ? renderCardTop(task.id, origin.href, { ...topOpts, spread: spreadInfo || null, reportTitle: task.title }) + : renderCardTop(task.id, detailHref, { ...topOpts, spread: spreadInfo || null, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, returnTo: `/tasks?filter=${encodeURIComponent(currentFilter)}`, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }); return div({ class: "tribe-card task-card" + (isAuthor ? " own-content" : "") }, - headerActions ? div({ class: "card-header activity-card-header" }, span(), headerActions) : null, + cardTop, heroNode, div({ class: "tribe-card-body" }, div({ class: "shop-title-row" }, @@ -170,11 +168,11 @@ const renderTaskItem = exports.renderTaskItem = (task, filter, spreadInfo, opts href ? a({ href }, task.title || i18n.tasksTitle) : (task.title || i18n.tasksTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.taskAssignedTo}: ${assignees.length}`) ), - ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []), + null ) ); }; @@ -413,15 +411,13 @@ exports.singleTaskView = async (task, filter, comments = [], params = {}) => { if (task.location && String(task.location).trim()) pushRow(i18n.taskLocationLabel, task.location); const taskSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(task.id, `/tasks/${encodeURIComponent(task.id)}`, { spread: params.spreads || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }) - ), + renderCardTop(task.id, `/tasks/${encodeURIComponent(task.id)}`, { arrow: false, date: task.createdAt, chips, bylineExtra: [renderEditedChip(task.id, task.rootId)], spread: params.spreads || null, author: task.author, favKind: 'tasks', isFavorite: task.isFavorite, reportTitle: task.title, deleteAction: isAuthor ? contentDeleteAction('task', task.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, task.title) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(task.id, `/tasks/${encodeURIComponent(task.id)}`, task.title), renderPhotoGallery(task, 'task'), - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(task.createdAt, task.author)), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), assignNode ? div({ class: "tribe-side-actions housing-status-row task-assign-row" }, ...assignNode) : null, isAuthor ? renderTaskStatusRow(task, returnToSelf) : null, tagsNode, diff --git a/src/views/torrents_view.js b/src/views/torrents_view.js index 7f50d31..a7a98dd 100644 --- a/src/views/torrents_view.js +++ b/src/views/torrents_view.js @@ -1,8 +1,8 @@ -const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, strong } = require("../server/node_modules/hyperaxe"); +const { form, button, div, h2, h3, p, section, input, br, a, span, textarea, select, label, option, table, tr, th, td, progress, details, summary, ul, li } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); +const { renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip, renderLicenseSelect, renderLicenseChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip , renderSpreadEditWarning, renderContentActions, renderModuleStats, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, renderTorrentSourceDownload, contentDeleteAction, paged } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderSpreadButton, renderEcoTax, renderLifespanChip, renderSpreadEditWarning, renderCardTop, renderModuleStats, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, renderTorrentSourceDownload, contentDeleteAction, paged, formatSize, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText"); @@ -35,20 +35,17 @@ const renderTags = (tags) => { const renderTorrentOwnerActions = (filter, torrentObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(torrentObj.author) === String(userId); - const hasOpinions = Object.keys(torrentObj.opinions || {}).length > 0; if (!isAuthor) return []; const items = []; - if (!hasOpinions) { - items.push( - form( - { method: "GET", action: `/torrents/edit/${encodeURIComponent(torrentObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.torrentUpdateButton) - ) - ); - } + items.push( + form( + { method: "GET", action: `/torrents/edit/${encodeURIComponent(torrentObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.torrentUpdateButton) + ) + ); return items; }; @@ -67,13 +64,6 @@ const blobSha256Hex = (blobId) => { try { return Buffer.from(m[1], "base64").toString("hex"); } catch (_) { return ""; } }; -const formatSize = (bytes) => { - const n = Number(bytes) || 0; - if (n === 0) return "โ€”"; - if (n < 1024) return n + " B"; - if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB"; - return (n / (1024 * 1024)).toFixed(1) + " MB"; -}; const spreadInfoOf = (t, spreadMap) => (spreadMap instanceof Map && spreadMap.get(t.key)) || null; const renderTorrentSeeds = (t, spreadMap) => { @@ -158,7 +148,7 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { : null, fromBlob ? null : span(i18n.torrentFileLabel), fromBlob ? null : br(), - fromBlob ? null : input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" }), + fromBlob ? null : ((filter === "edit" && renderMediaLockNote(torrentToEdit)) || input({ type: "file", name: "torrent", accept: ".torrent", required: filter !== "edit" })), fromBlob ? null : br(), fromBlob ? null : br(), span(i18n.torrentTitleLabel), @@ -179,6 +169,7 @@ const renderTorrentForm = (filter, torrentId, torrentToEdit, params = {}) => { }), br(), br(), + ...renderLicenseSelect(torrentToEdit?.license, i18n), ...(clearnetAllowed ? [renderClearnetSelector(filter === "edit" ? !!torrentToEdit?.clearnet : false, i18n), br()] : []), button({ type: "submit" }, filter === "edit" ? i18n.torrentUpdateButton : i18n.torrentCreateButton) ) @@ -364,28 +355,38 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(torrentObj.tags); + const href = `/torrents/${encodeURIComponent(torrentObj.key)}`; + const createdTs = torrentObj.createdAt ? new Date(torrentObj.createdAt).getTime() : NaN; + const updatedTs = torrentObj.updatedAt ? new Date(torrentObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(torrentObj.key, `/torrents/${encodeURIComponent(torrentObj.key)}`, { - spreadTitle: i18n.seedAction, + const torrentSide = div({ class: "tribe-side" }, + renderCardTop(torrentObj.key, href, { arrow: false, author: torrentObj.author, + date: torrentObj.createdAt, + chips: [ + torrentObj.tribeId && torrentObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), + clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('torrents', torrentObj.title, torrentObj.key) : null) : null, + torrentObj.tribeId ? null : renderLicenseChip(torrentObj.license), + ...chips + ], + bylineExtra: [ + renderEditedChip(torrentObj.key, torrentObj.rootId), + showUpdated ? span({ class: "votations-comment-date" }, `${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) : null + ], + spreadTitle: i18n.seedAction, favKind: 'torrents', isFavorite: torrentObj.isFavorite, spread: params.spreads || null, returnTo, reportTitle: torrentObj.title, deleteAction: String(torrentObj.author) === String(userId) ? contentDeleteAction('torrent', torrentObj.key) : undefined - }) - ); - - const torrentSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - torrentObj.tribeId && torrentObj.cipher ? renderEncryptedChip(i18n) : renderTransportChip(i18n), - clearnetAllowed ? renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('torrents', torrentObj.title, torrentObj.key) : null) : null, clearnetAllowed && isAuthor ? renderClearnetSwitch('torrents', torrentObj.rootId || torrentObj.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(torrentObj.key, href, torrentObj.title), safeText(torrentObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(torrentObj.description)) : null, @@ -394,7 +395,6 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa ); const torrentMain = div({ class: "tribe-main" }, - detailActions, torrentObj.url && torrentObj.url.startsWith("&") ? div({ class: "torrent-download" }, renderOasisDownload(torrentObj, params), @@ -402,36 +402,29 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa h3({ class: "torrent-section-title" }, "Torrent"), table({ class: "tribe-info-table torrent-file-info" }, tr(td({ class: "tribe-info-label" }, i18n.fileShareFileLabel), td({ class: "tribe-info-value" }, `${String(torrentObj.title || "download").replace(/\.torrent$/i, "")}.torrent`)), - torrentObj.source ? null : tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(torrentObj.size))), + torrentObj.source ? null : tr(td({ class: "tribe-info-label" }, i18n.torrentSizeLabel), td({ class: "tribe-info-value" }, formatSize(params.torrentInfo ? params.torrentInfo.length : torrentObj.size))), + params.torrentInfo ? tr(td({ class: "tribe-info-label" }, i18n.torrentInfoHash), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, params.torrentInfo.infoHash))) : null, + params.torrentInfo ? tr(td({ class: "tribe-info-label" }, i18n.torrentFilesLabel), td({ class: "tribe-info-value" }, String(params.torrentInfo.fileCount))) : null, torrentObj.cipher ? tr(td({ class: "tribe-info-label" }, i18n.encryptedChipLabel || "E2E"), td({ class: "tribe-info-value" }, "AES-256-GCM")) : tr(td({ class: "tribe-info-label" }, "SHA-256"), td({ class: "tribe-info-value" }, span({ class: "bank-address-code" }, blobSha256Hex(torrentObj.url)))) ), - renderTorrentDownload(torrentObj.cipher ? `/torrents/${encodeURIComponent(torrentObj.key)}/file` : torrentDownloadHref(torrentObj.url, torrentObj.title)) + params.torrentInfo && params.torrentInfo.fileCount > 1 + ? details({ class: "torrent-file-list" }, + summary(`${i18n.torrentFilesLabel} (${params.torrentInfo.fileCount})`), + ul(...params.torrentInfo.files.map(f => li(span(f.path), " ยท ", span({ class: "votations-comment-date" }, formatSize(f.length))))) + ) + : null, + renderTorrentDownload(torrentObj.cipher ? `/torrents/${encodeURIComponent(torrentObj.key)}/file` : torrentDownloadHref(torrentObj.url, torrentObj.title)), + params.torrentInfo && params.torrentInfo.magnet + ? a({ href: params.torrentInfo.magnet, class: "filter-btn torrent-magnet-link", rel: "noopener" }, `\u2B07 ${i18n.torrentMagnet}`) + : null ) ) : p(i18n.torrentNoFile), - (() => { - const createdTs = torrentObj.createdAt ? new Date(torrentObj.createdAt).getTime() : NaN; - const updatedTs = torrentObj.updatedAt ? new Date(torrentObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(torrentObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(torrentObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.torrentUpdatedAt}: ${moment(torrentObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), - torrentObj.tribeId ? null : renderEngagement(torrentObj.key, - renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), - renderTorrentCommentsSection(torrentObj.key, comments, returnTo) - ) + renderEngagement(torrentObj.key, + torrentObj.tribeId ? null : renderOpinionsVoting('/torrents/opinions', torrentObj.key, torrentObj.opinions, returnTo, torrentObj.opinions_inhabitants), + torrentObj.tribeId ? null : renderTorrentCommentsSection(torrentObj.key, comments, returnTo)) ); return template( @@ -446,14 +439,16 @@ exports.singleTorrentView = async (torrentObj, filter = "all", comments = [], pa form( { method: "GET", action: "/torrents", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterAll).toUpperCase()), ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterRecent).toUpperCase())] : []), ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterFavorites).toUpperCase() )] : []), button({ type: "submit", name: "filter", value: "top", class: filter === "top" ? "filter-btn active" : "filter-btn" }, String(i18n.torrentFilterTop).toUpperCase()), + ...(params.isPublic ? [] : [button({ type: "submit", name: "filter", value: "downloads", class: "filter-btn" }, String(i18n.torrentDownloads).toUpperCase())]), button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.torrentCreateButton) ) ), diff --git a/src/views/transfer_view.js b/src/views/transfer_view.js index f712fe6..bb34942 100644 --- a/src/views/transfer_view.js +++ b/src/views/transfer_view.js @@ -1,6 +1,6 @@ const { hr, div, h2, p, section, button, form, a, input, br, span, label, select, option, progress, table, tr, td } = require("../server/node_modules/hyperaxe") const { getConfig } = require('../configs/config-manager.js'); -const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderCreatedRows } = require("./main_views") +const { template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderStateChip, renderLifespanChip, renderCardTop, renderModuleStatsBy, moduleIsEmpty, renderEcoValueChip, renderWalletChip, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views") const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view") const moment = require("../server/node_modules/moment") const { config } = require("../server/SSB_server.js") @@ -171,25 +171,22 @@ const generateTransferCard = (transfer, filter, params = {}) => { const isOwn = transfer.from && String(transfer.from) === String(userId) const canDelete = isOwn && String(transfer.status || "").toUpperCase() === "UNCONFIRMED" && !isExpired && confirmedCount < required + const transferHref = `/transfers/${encodeURIComponent(transfer.id)}` return div({ class: "trending-card transfer-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { spread: params.spreadMap && params.spreadMap.get(transfer.id) || null, author: transfer.from, favKind: 'transfers', isFavorite: transfer.isFavorite, reportTitle: transfer.concept, returnTo, deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined }) - ), + renderCardTop(transfer.id, transferHref, { date: transfer.createdAt, chips, spread: params.spreadMap && params.spreadMap.get(transfer.id) || null, author: transfer.from, favKind: 'transfers', isFavorite: transfer.isFavorite, reportTitle: transfer.concept, returnTo, deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined }), div({ class: "card-section transfer-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, - a({ href: `/transfers/${encodeURIComponent(transfer.id)}` }, transferTitle(transfer)) + a({ href: transferHref }, transferTitle(transfer)) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, !isUbi && dl && dl.isValid() ? p({ class: "time-chip" }, dl.format("YYYY/MM/DD HH:mm")) : null, cat !== "TRUST" ? div({ class: "tribe-card-members" }, span({ class: "price-chip" }, fmtAmountWithUnit(transfer))) - : null + : null, + null ) ) } @@ -272,6 +269,8 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { const isForm = normalizedFilter === "create" || normalizedFilter === "edit" const transferToEdit = normalizedFilter === "edit" ? (list.find(t => t.id === transferId) || {}) : (params.prefill || {}) const returnToForForm = buildReturnTo("all", {}) + const opinionLock = normalizedFilter === "edit" ? renderMediaLockNote(transferToEdit) : null + const ro = opinionLock ? { readonly: true } : {} const validCategories = ["ECONOMIC", "TIME", "TRUST"] const paramCategoryRaw = safeText(params.category || "").toUpperCase() const selectedCategory = @@ -352,7 +351,8 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { normalizedFilter === "create" ? input({ type: "hidden", name: "category", value: selectedCategory }) : null, label(i18n.transfersToUser), br(), - input({ type: "text", name: "to", required: true, pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", title: i18n.transfersToUserValidation, value: transferToEdit.to || "" }), + input({ type: "text", name: "to", required: true, pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$", title: i18n.transfersToUserValidation, value: transferToEdit.to || "", ...ro }), + opinionLock, br(), label(i18n.transfersConcept), br(), @@ -376,7 +376,7 @@ exports.transferView = async (transfers, filter, transferId, params = {}) => { : [ label(`${i18n.transfersAmount} (${amountUnitLabel})`), br(), - input({ type: "number", name: "amount", step: "0.000001", required: true, min: "0.000001", value: transferToEdit.amount || "" }), + input({ type: "number", name: "amount", step: "0.000001", required: true, min: "0.000001", value: transferToEdit.amount || "", ...ro }), renderEcoValueChip(), br(), br() ], @@ -494,23 +494,30 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { if (!isUbi) pushRow(i18n.transfersDeadline, dl && dl.isValid() ? dl.format("YYYY/MM/DD HH:mm") : "") pushRow(i18n.transfersStatus, i18n[statusKey(transfer.status)] || String(transfer.status || "")) + const updatedAt = transfer.updatedAt ? moment(transfer.updatedAt) : null + const showUpdated = updatedAt && updatedAt.isValid() && updatedAt.valueOf() !== new Date(transfer.createdAt).getTime() + const transferSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { - spread: params.spreads || null, - author: transfer.from, - favKind: 'transfers', - isFavorite: transfer.isFavorite, - returnTo, - reportTitle: transfer.concept, - deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined - }) - ), + renderCardTop(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, { arrow: false, + date: transfer.createdAt, + chips, + bylineExtra: [ + renderEditedChip(transfer.id, transfer.rootId), + showUpdated ? span({ class: "card-byline-time" }, `${i18n.jobsUpdatedAt}: ${updatedAt.format("YYYY/MM/DD HH:mm")}`) : null + ], + spread: params.spreads || null, + author: transfer.from, + favKind: 'transfers', + isFavorite: transfer.isFavorite, + returnTo, + reportTitle: transfer.concept, + deleteAction: canDelete ? contentDeleteAction('transfer', transfer.id) : undefined + }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, transferTitle(transfer)) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - table({ class: "tribe-info-table" }, ...renderCreatedRows(transfer.createdAt, transfer.from, transfer.updatedAt)), + renderDetailShare(transfer.id, `/transfers/${encodeURIComponent(transfer.id)}`, transfer.concept), + table({ class: "tribe-info-table" }, ...infoRows), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.transfersConfirmations}: ${confirmedCount}/${required}`) @@ -545,8 +552,7 @@ exports.singleTransferView = async (transfer, filter, params = {}) => { action: `/transfers/${encodeURIComponent(transfer.id)}/comments`, comments: params.comments || [], returnTo - }) - ) + })) ) return template( diff --git a/src/views/trending_view.js b/src/views/trending_view.js index 9a6a518..52ec7b0 100644 --- a/src/views/trending_view.js +++ b/src/views/trending_view.js @@ -1,7 +1,7 @@ -const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, img, video: videoHyperaxe, audio: audioHyperaxe, span, details, summary} = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, br, input, table, tr, th, td, video: videoHyperaxe, audio: audioHyperaxe, span, details, summary } = require("../server/node_modules/hyperaxe"); const RAW_HTML = Symbol.for('oasis.rawHtml'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderSpreadButton, renderContentActions, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged } = require('./main_views'); +const { template, i18n, renderCardTop, renderShareButton, renderVotesSummary, renderModuleStats, renderCardMetaRow, renderCommentsSummary, commentsOpenHref, renderTorrentDownload, renderTorrentSourceDownload, renderFileDownloads, torrentDownloadHref, CONTENT_FAV_KIND, CONTENT_TORRENTABLE, paged, renderPdfDocument } = require('./main_views'); const { renderStyledHtml, safeExternalHref } = require('../backend/renderStyledText'); const { renderZoomableImage } = require('./gallery_view'); const { config } = require('../server/SSB_server.js'); @@ -12,7 +12,7 @@ const { sanitizeHtml } = require('../backend/sanitizeHtml'); const userId = config.keys.id; const capFirst = (s) => String(s || '').charAt(0).toUpperCase() + String(s || '').slice(1); -const typeLabel = (type) => i18n['type' + capFirst(type)] || type; +const typeLabel = (type) => i18n['type' + capFirst(type)] || require('./activity_view').rootTypeLabel(type) || type; const filterButton = (mode, currentFilter) => form({ method: 'GET', action: '/trending' }, @@ -28,7 +28,6 @@ const voteLabelFor = (cat) => const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new Map(), params = {}) => { const c = item.value.content; - const created = moment(item.value.timestamp).format("YYYY/MM/DD HH:mm"); let contentHtml; @@ -111,7 +110,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new div({ class: 'card-section document' }, t ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.documentTitleLabel + ':'), span({ class: 'card-value' }, t)) : "", description ? [span({ class: 'card-label' }, i18n.documentDescriptionLabel + ":"), p(...renderStyledText(description))] : null, - div({ id: `pdf-container-${item.key}`, class: 'pdf-viewer-container', 'data-pdf-url': `/blob/${encodeURIComponent(url)}` }) + renderPdfDocument(`/blob/${encodeURIComponent(url)}`, { title: t }) ) ); } else if (c.type === 'industry') { @@ -251,32 +250,35 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new logisticsRoute: 'logistics', poll: 'polls', schoolCourse: 'school/course', + wikiPage: 'wiki', + map: 'maps', + job: 'jobs', industryBlueprint: 'industry/blueprint' }; const detailHref = detailPaths[c.type] ? `/${detailPaths[c.type]}/${encodeURIComponent(item.key)}` : `/thread/${encodeURIComponent(item.key)}`; const isOwn = item.value.author && String(item.value.author) === String(userId); + const commentTotal = params.commentCounts instanceof Map ? (params.commentCounts.get(item.key) || 0) : 0; + const shareTitle = c.title || c.question || c.concept || c.contentWarning || ''; return div( { class: 'trending-card' + (isOwn ? ' own-content' : '') }, - div( - { class: 'card-header activity-card-header' }, - span({ class: 'pm-exposition-chip pm-exposition-whole' }, + renderCardTop(item.key, detailHref, { + date: item.value.timestamp, + chips: [span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(typeLabel(c.type) || '').toUpperCase()) - ), - renderContentActions(item.key, detailHref, { - spread: spreadMap.get(item.key) || null, - author: item.value.author, - reportTitle: c.title || c.question || c.concept || c.contentWarning || '', - ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') - ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } - : {}), - ...(CONTENT_FAV_KIND[c.type] - ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(item.key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: params.returnTo } - : {}) - }) - ), + )], + spread: spreadMap.get(item.key) || null, + author: item.value.author, + reportTitle: shareTitle, + ...(CONTENT_TORRENTABLE.has(c.type) && typeof c.url === 'string' && c.url.startsWith('&') + ? { torrentFrom: { blobId: c.url, name: c.title || c.fileName || '' } } + : {}), + ...(CONTENT_FAV_KIND[c.type] + ? { favKind: CONTENT_FAV_KIND[c.type], isFavorite: params.favIndex instanceof Map ? params.favIndex.get(String(item.key)) === CONTENT_FAV_KIND[c.type] : false, returnTo: params.returnTo } + : {}) + }), div( { class: 'card-section trending-card-body' }, contentHtml @@ -284,7 +286,7 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new renderVotesSummary(c.opinions), renderCardMetaRow( details({ class: 'opinions-voting-collapse' }, - summary({ class: 'opinions-summary' }, + summary({ class: Object.values(c.opinions || {}).some(n => Number(n) > 0) ? 'opinions-summary engage-on' : 'opinions-summary' }, span({ class: 'opinions-summary-icon' }, '๊”'), span({ class: 'opinions-summary-count' }, `(${Object.values(c.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0)})`)), div( @@ -299,11 +301,8 @@ const renderTrendingCard = (item, votes, categories, seenTitles, spreadMap = new ) ) ), - p( - { class: 'card-footer' }, - span({ class: 'date-link' }, `${created}`), - userLink(item.value.author) - ) + renderCommentsSummary(commentsOpenHref(detailHref, commentTotal), commentTotal), + renderShareButton(item.key, detailHref, shareTitle) ) ); }; @@ -317,7 +316,8 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap ['votes', 'event', 'task', 'report'], ['feed', 'post', 'project', 'industry', 'shopProduct', 'transfer'], ['audio', 'bookmark', 'document', 'file', 'image', 'torrent', 'video'], - ['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'] + ['podcast', 'podcastEpisode', 'campaign', 'logisticsRoute'], + ['wikiPage', 'map', 'industryBlueprint', 'job'] ]; let filteredItems = items.filter(item => { @@ -369,12 +369,11 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap categories, seenDocumentTitles, spreadMap, - { favIndex: params.favIndex, returnTo: `/trending?filter=${encodeURIComponent(filter)}` } + { favIndex: params.favIndex, commentCounts: params.commentCounts, returnTo: `/trending?filter=${encodeURIComponent(filter)}` } ) ) .filter(Boolean); - const hasDocument = filteredItems.some(item => item.value.content.type === 'document'); let html = template( title, @@ -406,12 +405,6 @@ exports.trendingView = (items, filter, categories = opinionCategories, spreadMap ) ); - if (hasDocument) { - html += ` - - `; - } - return html; }; diff --git a/src/views/tribes_view.js b/src/views/tribes_view.js index a0f8087..5ecc38b 100644 --- a/src/views/tribes_view.js +++ b/src/views/tribes_view.js @@ -1,10 +1,10 @@ const { div, h2, h3, p, section, button, form, a, input, img, label, select, option, br, textarea, h1, span, nav, ul, li, video, audio, table, tr, td, thead, tbody, th } = require("../server/node_modules/hyperaxe"); const { TEXT_CAP } = require('../backend/long_text'); const moment = require("../server/node_modules/moment"); -const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS } = require('./main_views'); +const { template, i18n, userLink, renderStateChip, renderPrivacyChip, renderLifespanChip, renderModeChip, renderInviteQrCard, renderContentActions, renderCardTop, renderSubscriptionBox, renderModuleStats, renderModuleStatsBy, moduleIsEmpty, renderTorrentDownload, torrentDownloadHref, contentDeleteAction, clearnetSlugFor, renderEngagement, renderOpinionsVoting, renderVotesSummary, paged, CLEARNET_HUB_CSS, renderPdfDocument, renderDetailShare } = require('./main_views'); const { renderTribeWikiSection } = require('./wiki_view'); const mobileUi = require('./mobile_ui'); -const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS } = require('./clearnet_view'); +const { renderEncryptedChip: renderTribeEncryptedChip, renderLicenseSelect, renderReachChip, renderClearnetPage, escapeHtml, renderRichText, blobUrl: cnBlobUrl, paginateClearnet, renderClearnetPager, CLEARNET_PAGER_CSS, renderClearnetPdfFrame, CLEARNET_PDF_CSS } = require('./clearnet_view'); const { renderPollCard } = require('./polls_view'); const { renderFeedCard } = require('./feed_view'); const { renderEventItem } = require('./event_view'); @@ -77,7 +77,7 @@ const renderTribeReachListChip = (tribe, item, threadsById) => { const parent = item.contentType === 'forum-reply' && threadsById ? threadsById.get(item.parentId) : null; const level = reachLevelOf(parent || item); if (!tribe.reachAllowed && level === 'tribe') return null; - return div({ class: 'card-chips-row' }, renderTribeReachChip(level, item.tribeName ? null : tribe)); + return renderTribeReachChip(level, item.tribeName ? null : tribe); }; const DEFAULT_HASH_ENC = "%260000000000000000000000000000000000000000000%3D.sha256"; @@ -329,29 +329,30 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul const parentTribe = t.parentTribeId ? allT.find(p => p.id === t.parentTribeId) : null; const isOwn = String(t.author) === String(userId); + const tribeHref = `/tribe/${encodeURIComponent(t.id)}`; return div({ class: 'trending-card tribes-card' + (isOwn ? ' own-content' : '') }, - div({ class: 'card-header activity-card-header' }, - span(), - renderContentActions(t.id, `/tribe/${encodeURIComponent(t.id)}`, { author: t.author, reportTitle: t.title, deleteAction: isOwn && !isLarpHouseTribe(t) ? contentDeleteAction('tribe', t.id) : undefined }) - ), + renderCardTop(t.id, tribeHref, { + date: t.createdAt, + chips: [ + t.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), + t.isAnonymous ? renderTribeEncryptedChip(i18n) : null, + renderModeChip(t.inviteMode, i18n), + renderLifespanChip(t.lifetime, i18n), + t.subscriptionIn === true + ? renderStateChip('mutuals', '\u2709', i18n.subscriptionOn) + : (t.subscriptionIn === false ? renderStateChip('closed', '\u2709', i18n.subscriptionOff) : null) + ], + author: t.author, reportTitle: t.title, deleteAction: isOwn && !isLarpHouseTribe(t) ? contentDeleteAction('tribe', t.id) : undefined + }), div({ class: 'card-section tribes-card-body' }, div({ class: 'tribe-card-image-wrapper' }, - a({ href: `/tribe/${encodeURIComponent(t.id)}` }, + a({ href: tribeHref }, renderMediaBlob(t.image, '/assets/images/default-tribe.png', { class: 'tribe-card-hero-image' }) ) ), div({ class: 'tribe-card-body' }, div({ class: 'shop-title-row' }, - h2({ class: 'tribe-card-title' }, a({ href: `/tribe/${encodeURIComponent(t.id)}` }, t.title)) - ), - div({ class: 'card-chips-row' }, - t.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), - t.isAnonymous ? renderTribeEncryptedChip(i18n) : null, - renderModeChip(t.inviteMode, i18n), - renderLifespanChip(t.lifetime, i18n), - t.subscriptionIn === true - ? renderStateChip('mutuals', '\u2709', i18n.subscriptionOn) - : (t.subscriptionIn === false ? renderStateChip('closed', '\u2709', i18n.subscriptionOff) : null) + h2({ class: 'tribe-card-title' }, a({ href: tribeHref }, t.title)) ), t.description ? p({ class: 'tribe-card-description' }, ...renderStyledText(t.description)) : null, renderMapLocationVisitLabel(t.mapUrl), @@ -363,7 +364,8 @@ exports.tribesView = async (tribes, filter, tribeId, query = {}, allTribes = nul ) : null, div({ class: 'tribe-card-members' }, span({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${t.memberCount != null ? t.memberCount : t.members.length}`) - ) + ), + null ) ) ); @@ -492,22 +494,17 @@ const activityMediaTypeName = (mt) => { const renderTribeActivitySection = (tribe, sectionData) => { const { activities } = sectionData || { activities: [] }; if (activities.length === 0) return div({ class: 'tribe-content-list' }, p(i18n.tribeActivityEmpty)); - const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; const threadsById = reachIndexOf(activities); return div({ class: 'tribe-content-list tribe-content-list-spaced' }, paged(activities).map(item => { if (item.encrypted) return div({ class: 'card card-rpg' }, div({ class: 'tribe-card-body' }, p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content'))); if (item.contentType === 'chatThread') { const replies = Array.isArray(item.replies) ? item.replies : []; - const tDate = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; const headerText = item.tribeName - ? `[${String(i18n.typeChat || 'CHAT').toUpperCase()} ยท THREAD ยท ${item.tribeName}]` - : `[${String(i18n.typeChat || 'CHAT').toUpperCase()} ยท THREAD]`; + ? `${String(i18n.typeChat || 'CHAT').toUpperCase()} ยท THREAD ยท ${item.tribeName}` + : `${String(i18n.typeChat || 'CHAT').toUpperCase()} ยท THREAD`; return div({ class: 'card card-rpg tribe-card-padded chat-thread' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, headerText), - a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') - ), + renderCardTop(item.id, item.directUrl, { ...tribeItemOpts(tribe, item, isTribeMember(tribe), item.title || item.description), author: item.author, authorName: item.authorName, date: item.timestamp, chips: [tribeTypeChip(headerText)] }), div({ class: 'tribe-card-body' }, item.title ? div({ class: 'card-field' }, span({ class: 'card-value' }, item.title)) : null, tribeExcerpt(tribe, item.description), @@ -519,20 +516,16 @@ const renderTribeActivitySection = (tribe, sectionData) => { ) )) ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${tDate}`), - userLink(item.author, item.authorName) - ) + null ); } - const date = item.timestamp ? moment(item.timestamp).format("YYYY/MM/DD HH:mm") : ''; const typeLabel = item.contentType === 'media' && item.mediaType ? activityMediaTypeName(item.mediaType) : contentTypeName(item.contentType); const headerText = item.tribeName - ? `[${String(typeLabel).toUpperCase()} ยท ${item.tribeName}]` - : `[${String(typeLabel).toUpperCase()}]`; - const targetSection = activitySectionForItem(item); + ? `${String(typeLabel).toUpperCase()} ยท ${item.tribeName}` + : String(typeLabel).toUpperCase(); + const itemHref = tribeItemHref(tribe, item); const blobUrl = item.contentType === 'media' ? toBlobUrl(item.image) : null; const mediaContent = item.contentType === 'media' && item.mediaType === 'image' && blobUrl @@ -542,32 +535,20 @@ const renderTribeActivitySection = (tribe, sectionData) => { : item.contentType === 'media' && item.mediaType === 'video' && blobUrl ? video({ src: blobUrl, controls: true, class: 'tribe-media-thumb', preload: 'metadata' }) : item.contentType === 'media' && item.mediaType === 'document' && blobUrl - ? a({ href: blobUrl, target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, i18n.readDocument || 'Read Document') + ? renderPdfDocument(blobUrl, { title: item.title }) : item.contentType === 'media' && item.mediaType === 'bookmark' && (item.url || item.description) ? a({ href: safeExternalHref(item.url || item.description), target: '_blank', rel: 'noopener noreferrer', class: 'tribe-action-btn' }, item.url || item.description) : null; return div({ class: 'card card-rpg tribe-card-padded' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, headerText), - item.directUrl - ? a({ href: item.directUrl, class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') - : form({ method: 'GET', action: tribeUrl }, - input({ type: 'hidden', name: 'section', value: targetSection }), - button({ type: 'submit', class: 'tribe-action-btn' }, i18n.viewDetails || 'View Details') - ) - ), + renderCardTop(item.id, itemHref, { ...tribeItemOpts(tribe, item, isTribeMember(tribe), item.title || item.description), author: item.author, authorName: item.authorName, date: item.timestamp, chips: [div({ class: 'card-chips-row' }, tribeTypeChip(headerText), renderTribeReachListChip(tribe, item, threadsById))] }), div({ class: 'tribe-card-body' }, - renderTribeReachListChip(tribe, item, threadsById), item.title ? div({ class: 'card-field' }, - span({ class: 'card-value' }, item.title) + a({ href: itemHref, class: 'card-value' }, item.title) ) : null, mediaContent, item.description && !(item.contentType === 'media' && item.mediaType === 'bookmark' && item.description === item.url) ? tribeExcerpt(tribe, item.description) : null ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, `${date}`), - userLink(item.author, item.authorName) - ) + null ); }) ); @@ -580,25 +561,25 @@ const renderTribeTrendingSection = (tribe, sectionData, query) => { const tribeUrl = `/tribe/${encodeURIComponent(tribe.id)}`; const periodBtn = (key, label) => form({ method: 'GET', action: tribeUrl }, input({ type: 'hidden', name: 'section', value: 'trending' }), input({ type: 'hidden', name: 'period', value: key }), button({ type: 'submit', class: period === key ? 'filter-btn active' : 'filter-btn' }, label)); return div({ class: 'tribe-content-list' }, - div({ class: 'tribe-content-header' }, h2(i18n.tribeSectionTrending)), div({ class: 'tribe-filter-bar' }, periodBtn('day', i18n.tribeTrendingPeriodDay), periodBtn('week', i18n.tribeTrendingPeriodWeek), periodBtn('all', i18n.tribeTrendingPeriodAll)), items.length === 0 ? p(i18n.tribeTrendingEmpty) : paged(items.map((item, idx) => [item, idx])).map(([item, idx]) => { if (item.encrypted) return div({ class: 'tribe-content-card' }, div({ class: 'tribe-content-meta' }, span(`#${idx + 1}`)), p({ class: 'tribe-meta-label' }, i18n.tribeContentEncrypted || 'Encrypted content')); + const itemHref = tribeItemHref(tribe, item); + const itemTitle = item.title || item.description; return div({ class: 'tribe-content-card' }, + renderCardTop(item.id, itemHref, { ...tribeItemOpts(tribe, item, isTribeMember(tribe), item.title || item.description), author: item.author, date: item.createdAt, chips: [div({ class: 'card-chips-row' }, tribeTypeChip(contentTypeName(item.contentType)), renderTribeReachListChip(tribe, item, null))] }), div({ class: 'tribe-content-meta' }, span(`#${idx + 1}`), - span({ class: 'tribe-badge' }, contentTypeName(item.contentType)), span(`${i18n.tribeTrendingEngagement}: ${engagementScore(item)}`), (() => { const entries = Object.entries(item.opinions || {}); if (!entries.length) return null; const top = entries.reduce((a, b) => b[1] > a[1] ? b : a); return span(`| ${i18n.tribeTopCategory}: ${i18n['opinionCat' + top[0].charAt(0).toUpperCase() + top[0].slice(1)] || top[0]}`); })() ), - renderTribeReachListChip(tribe, item, null), - item.title ? h2(item.title) : item.description ? h2(item.description) : null, + itemTitle ? h2(a({ href: itemHref }, itemTitle)) : null, div({ class: 'tribe-content-meta' }, item.refeeds ? span(`${i18n.tribeActivityRefeed}: ${item.refeeds}`) : null, Array.isArray(item.attendees) && item.attendees.length ? span(`${i18n.tribeEventAttendees}: ${item.attendees.length}`) : null ), - p({ class: 'tribe-meta-label' }, userLink(item.author)) + null ); }) ); }; @@ -608,10 +589,6 @@ const renderTribeTagsSection = (tribe, sectionData, query) => { const sortedTags = tagsList.slice().sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); const tagHref = tribeTagHref(tribe); return section( - div({ class: 'tags-header module-header-line' }, - h2(i18n.tribeSectionTags || i18n.tagsTitle || 'TAGS'), - p(i18n.tagsDescription || '') - ), div({ class: 'tags-list' }, tagsList.length === 0 ? p(i18n.tagsNoItems || i18n.tribeTagsEmpty || 'No tags yet') @@ -644,29 +621,25 @@ const renderTribeSearchSection = (tribe, sectionData, query) => { } const stats = renderModuleStats(results.length, [...typeCounts].map(([label, count]) => ({ label, count })).sort((x, y) => y.count - x.count || x.label.localeCompare(y.label))); return div({ class: 'tribe-content-list' }, - div({ class: 'tags-header module-header-line' }, h2(i18n.tribeSectionSearch), p(hashtag ? i18n.hashtagDescription : i18n.searchDescriptionLabel)), renderSearchForm({ action: tribeUrl, query: sq, queryName: 'q', hidden: [['section', 'search']] }), sq.length > 0 && sq.length < 2 ? p(i18n.tribeSearchMinChars) : null, sq.length >= 2 ? div( stats ? div({ class: 'filters activity-filter-chips activity-toolbar-row search-stats-row' }, stats) : null, results.length === 0 ? p(i18n.tribeSearchEmpty) : - paged(results).map(item => div({ class: 'card card-rpg' }, - div({ class: 'card-header' }, - h2({ class: 'card-label' }, `[${String(contentTypeName(item.contentType)).toUpperCase()}]`) - ), - div({ class: 'card-body' }, - renderTribeReachListChip(tribe, item, threadsById), - item.title ? div({ class: 'card-field' }, - span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), - span({ class: 'card-value' }, item.title) - ) : null, - tribeExcerpt(tribe, item.description) - ), - p({ class: 'card-footer' }, - span({ class: 'date-link' }, moment(item.createdAt).format("YYYY/MM/DD HH:mm")), - userLink(item.author) - ) - )) + paged(results).map(item => { + const itemHref = tribeItemHref(tribe, item); + return div({ class: 'card card-rpg' }, + renderCardTop(item.id, itemHref, { ...tribeItemOpts(tribe, item, isTribeMember(tribe), item.title || item.description), author: item.author, date: item.createdAt, chips: [div({ class: 'card-chips-row' }, tribeTypeChip(contentTypeName(item.contentType)), renderTribeReachListChip(tribe, item, threadsById))] }), + div({ class: 'card-body' }, + item.title ? div({ class: 'card-field' }, + span({ class: 'card-label' }, (i18n.title || 'Title') + ':'), + a({ href: itemHref, class: 'card-value' }, item.title) + ) : null, + tribeExcerpt(tribe, item.description) + ), + null + ); + }) ) : null ); }; @@ -766,9 +739,6 @@ const openClosedModes = (isClosed) => ({ ...BASE_MODES, open: it => !isClosed(it const openClosedStats = (items, isClosed, openLabel, closedLabel) => renderModuleStatsBy(items, it => isClosed(it) ? 'CLOSED' : 'OPEN', [{ value: 'OPEN', label: openLabel }, { value: 'CLOSED', label: closedLabel }]); -const renderTribeSectionHeader = (title, description) => - div({ class: 'tags-header module-header-line' }, h2(title), description ? p(description) : null); - const renderTribeModeChips = (tribe, sectionKey, { modes = [], filter = 'all', labelOf = (m) => m, visible = null, emptyMod = false, create = null }) => div({ class: 'mode-buttons-row' }, ...(emptyMod ? [] : modes.filter(m => !visible || visible(m)).map(m => @@ -814,20 +784,32 @@ const renderTribeReachControls = (tribe, item, returnTo) => { return levelSwitch || openedBy ? div({ class: 'card-chips-row' }, levelSwitch, openedBy) : null; }; -const tribeItemActions = (tribe, item, member, title) => - renderContentActions(item.id, null, { - author: item.author, - reportTitle: title || item.title || item.description, - deleteAction: member && isMineItem(item) && item.id ? `${tribeUrlOf(tribe)}/content/delete/${encodeURIComponent(item.id)}` : undefined - }); +const tribeItemOpts = (tribe, item, member, title) => ({ + author: item.author, + reportTitle: title || item.title || item.description, + deleteAction: member && isMineItem(item) && item.id ? `${tribeUrlOf(tribe)}/content/delete/${encodeURIComponent(item.id)}` : undefined +}); -const tribeEngagement = (tribe, item, member, returnTo) => { +const tribeItemActions = (tribe, item, member, title) => renderContentActions(item.id, null, tribeItemOpts(tribe, item, member, title)); + +const tribeCardActions = (tribe, item, member, title) => renderContentActions(item.id, null, { ...tribeItemOpts(tribe, item, member, title), menu: true }); + +const tribeItemHref = (tribe, item) => item.directUrl + || (item.contentType === 'forum' ? sectionHref(tribe, 'forum', { thread: item.id }) + : item.contentType === 'forum-reply' ? sectionHref(tribe, 'forum', { thread: item.parentId }) + : sectionHref(tribe, activitySectionForItem(item))); + +const tribeTypeChip = (text) => span({ class: 'pm-exposition-chip pm-exposition-whole' }, span({ class: 'pm-exposition-text' }, String(text).toUpperCase())); + +const forumCategoryChip = (item) => item.category ? span({ class: 'forum-category' }, `[${forumCatLabel(item.category)}]`) : null; + +const tribeEngagement = (tribe, item, member, returnTo, withShare = true) => { + const share = withShare ? { href: tribeItemHref(tribe, item), title: item.title || item.description } : null; if (member) { return renderEngagement(item.id, renderOpinionsVoting(null, item.id, item.opinions, returnTo, item.opinions_inhabitants, - (id, category) => `${tribeUrlOf(tribe)}/content/${encodeURIComponent(id)}/opinion/${category}`)); + (id, category) => `${tribeUrlOf(tribe)}/content/${encodeURIComponent(id)}/opinion/${category}`), null, share); } - const summary = renderVotesSummary(item.opinions); - return summary ? renderEngagement(item.id, summary) : null; + return renderEngagement(item.id, renderVotesSummary(item.opinions), null, share); }; const toFeedShape = (m) => ({ @@ -852,11 +834,10 @@ const renderFeedTribeView = (feedItems, tribe, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(isCreate ? i18n.createFeedTitle : i18n.tribeSectionFeed, i18n.FeedshareYourOpinions); const emptyMod = !isCreate && moduleIsEmpty(feed, filter, 'ALL', q); const visible = tribeChipVisible(feed, filter, { RECENT: isRecentItem, MINE: isMineItem, ALL: () => true, TOP: () => true }); const chips = renderTribeModeChips(tribe, 'feed', { modes: FEED_MODES, filter: isCreate ? 'CREATE' : filter, labelOf: m => i18n[`${m}Button`], visible, emptyMod, create: member ? { label: i18n.createFeedTitle || 'Create Feed' } : null }); - if (isCreate) return div({ class: 'tribe-content-list' }, header, chips, renderFeedComposeForm(tribe)); + if (isCreate) return div({ class: 'tribe-content-list' }, chips, renderFeedComposeForm(tribe)); const items = applyTribeFilter(feed, filter.toLowerCase(), q, BASE_MODES, it => Number(it.refeeds || 0)); const returnTo = sectionHref(tribe, 'feed', { filter, q }); const banner = query.sent ? div({ class: 'feed-success-msg' }, p('โœ“ ' + (i18n.feedPublishedSuccess || i18n.tribeFeedSent))) : null; @@ -867,13 +848,12 @@ const renderFeedTribeView = (feedItems, tribe, query = {}) => { hashtagHref: tribeTagHref(tribe), refeedAction: `${tribeUrl}/refeed/${encodeURIComponent(m.id)}`, canRefeed: member, - headerActions: tribeItemActions(tribe, m, member, m.description), + headerActions: tribeCardActions(tribe, m, member, m.description), chips: chip || controls ? [chip ? div({ class: 'card-chips-row' }, chip) : null, controls] : null, engagement: tribeEngagement(tribe, m, member, returnTo) }); }).filter(Boolean); return div({ class: 'tribe-content-list' }, - header, banner, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'feed', filter, q, renderModuleStats(items.length), i18n.feedSearchPlaceholder), @@ -897,7 +877,7 @@ const renderTribeEventCard = (tribe, e, member, returnTo) => { const attending = asList(e.attendees).includes(userId); return renderEventItem({ ...e, organizer: e.author, isPublic: reachLevelOf(e) === 'tribe' ? 'private' : 'public' }, 'all', null, { href: null, - headerActions: tribeItemActions(tribe, e, member, e.title), + headerActions: tribeCardActions(tribe, e, member, e.title), extraChips: [tribeReachChipFor(tribe, e)], bodyExtra: [ tribeDescription(e.description), @@ -921,12 +901,11 @@ const renderEventsSection = (tribe, items, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n.tribeSectionEvents, i18n.eventsDescription); const emptyMod = !isCreate && moduleIsEmpty(events, filter, 'all', q); const chips = renderTribeModeChips(tribe, 'events', { modes: EVENT_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`eventFilter${capFirst(m)}`], visible: tribeChipVisible(events, filter, EVENT_MODES), emptyMod, create: member ? { label: i18n.eventCreateButton } : null }); if (isCreate) { const today = new Date().toISOString().split('T')[0]; - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, div({ class: 'event-form' }, form({ method: 'POST', action: `${tribeUrl}/events/create` }, label(i18n.eventTitleLabel), br(), @@ -946,7 +925,6 @@ const renderEventsSection = (tribe, items, query = {}) => { const list = applyTribeFilter(events, filter, q, EVENT_MODES); const returnTo = sectionHref(tribe, 'events', { filter, q }); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'events', filter, q, openClosedStats(list, e => tribeStatusOf(e) === 'CLOSED', i18n.eventStatusOpen, i18n.eventStatusClosed), i18n.eventSearchPlaceholder), list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(e => renderTribeEventCard(tribe, e, member, returnTo))) : renderTribeEmpty(i18n.noevents || i18n.tribeEventsEmpty) @@ -976,7 +954,7 @@ const renderTribeTaskCard = (tribe, t, member, returnTo) => { const assignRow = member ? renderTaskAssignAction(t, assigned, returnTo, `${tribeUrl}/tasks/assign/${encodeURIComponent(t.id)}`) : null; return renderTaskItem({ ...t, isPublic: reachLevelOf(t) === 'tribe' ? 'PRIVATE' : 'PUBLIC' }, 'all', null, { href: null, - headerActions: tribeItemActions(tribe, t, member, t.title), + headerActions: tribeCardActions(tribe, t, member, t.title), extraChips: [tribeReachChipFor(tribe, t)], bodyExtra: [ tribeDescription(t.description), @@ -996,12 +974,11 @@ const renderTasksSection = (tribe, items, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n.tribeSectionTasks, i18n.tasksDescription); const emptyMod = !isCreate && moduleIsEmpty(tasks, filter, 'all', q); const chips = renderTribeModeChips(tribe, 'tasks', { modes: TASK_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: taskModeLabel, visible: tribeChipVisible(tasks, filter, TASK_MODES), emptyMod, create: member ? { label: i18n.taskCreateButton } : null }); if (isCreate) { const today = new Date().toISOString().split('T')[0]; - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, div({ class: 'task-form' }, form({ method: 'POST', action: `${tribeUrl}/tasks/create` }, label(i18n.taskTitleLabel), br(), @@ -1023,7 +1000,6 @@ const renderTasksSection = (tribe, items, query = {}) => { const list = applyTribeFilter(tasks, filter, q, TASK_MODES); const returnTo = sectionHref(tribe, 'tasks', { filter, q }); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'tasks', filter, q, renderModuleStatsBy(list, tribeStatusOf, [{ value: 'OPEN', label: i18n.taskStatusOpen }, { value: 'IN-PROGRESS', label: i18n.taskStatusInProgress }, { value: 'CLOSED', label: i18n.taskStatusClosed }]), i18n.taskSearchPlaceholder), list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(t => renderTribeTaskCard(tribe, t, member, returnTo))) : renderTribeEmpty(i18n.notasks || i18n.tribeTasksEmpty) @@ -1048,7 +1024,7 @@ const renderTribeVotationCard = (tribe, v, member, returnTo) => { const hasVoted = Object.values(v.votes || {}).some(arr => asList(arr).includes(userId)); return renderVoteListItem(shaped, shaped.options, 'all', null, { href: null, - headerActions: tribeItemActions(tribe, v, member, v.title), + headerActions: tribeCardActions(tribe, v, member, v.title), extraChips: [tribeReachChipFor(tribe, v)], bodyExtra: [ tribeDescription(v.description), @@ -1073,12 +1049,11 @@ const renderVotationsSection = (tribe, items, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n.tribeSectionVotations, i18n.votationsDescription); const emptyMod = !isCreate && moduleIsEmpty(votations, filter, 'all', q); const chips = renderTribeModeChips(tribe, 'votations', { modes: VOTE_MODE_LIST, filter: isCreate ? 'create' : filter, labelOf: m => i18n[`voteFilter${capFirst(m)}`], visible: tribeChipVisible(votations, filter, VOTE_MODES), emptyMod, create: member ? { label: i18n.voteCreateButton } : null }); if (isCreate) { const today = new Date().toISOString().split('T')[0]; - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, div({ class: 'vote-form' }, form({ method: 'POST', action: `${tribeUrl}/votations/create` }, h2(i18n.voteQuestionLabel), @@ -1101,7 +1076,6 @@ const renderVotationsSection = (tribe, items, query = {}) => { const list = applyTribeFilter(votations, filter, q, VOTE_MODES); const returnTo = sectionHref(tribe, 'votations', { filter, q }); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'votations', filter, q, openClosedStats(list, v => tribeStatusOf(v) === 'CLOSED', i18n.voteStatusOpen, i18n.voteStatusClosed), i18n.votesSearchPlaceholder), list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(v => renderTribeVotationCard(tribe, v, member, returnTo))) : renderTribeEmpty(i18n.novotes || i18n.tribeVotationsEmpty) @@ -1123,11 +1097,10 @@ const renderTribePollsSection = (tribe, items, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n.pollsTitle, i18n.pollsDescription); const emptyMod = !isCreate && moduleIsEmpty(polls, filter, 'ALL', q); const chips = renderTribeModeChips(tribe, 'polls', { modes: POLL_MODE_LIST, filter: isCreate ? 'CREATE' : filter, labelOf: pollModeLabel, visible: tribeChipVisible(polls, filter, POLL_MODES), emptyMod, create: member ? { label: i18n.pollCreateButton } : null }); if (isCreate) { - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, div({ class: 'div-center audio-form' }, h2(i18n.pollCreateTitle), form({ method: 'POST', action: `${tribeUrl}/polls/create` }, @@ -1153,7 +1126,6 @@ const renderTribePollsSection = (tribe, items, query = {}) => { const kept = polls.filter(pl => pred(pl) && matchesTribeQuery(pl, q)); const list = filter === 'TOP' ? [...kept].sort((a, b) => (b.totalVoters || 0) - (a.totalVoters || 0) || tsOf(b) - tsOf(a)) : sortRecentFirst(kept); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'polls', filter, q, openClosedStats(list, pl => tribeStatusOf(pl) === 'CLOSED', i18n.pollStatusOpen, i18n.pollStatusClosed), i18n.pollSearchPlaceholder), list.length ? div({ class: 'jobs-grid' }, ...paged(list).map(pl => renderPollCard(pl, filter, null))) : renderTribeEmpty(i18n.pollsNoItems) @@ -1204,7 +1176,7 @@ const renderForumThreadDetail = (tribe, allItems, threadId, member) => { span({ class: 'date-link' }, `${moment(thread.createdAt).format("YYYY/MM/DD HH:mm")}`), userLink(thread.author) ), - tribeEngagement(tribe, thread, member, returnTo) + tribeEngagement(tribe, thread, member, returnTo, false) ) ), member @@ -1244,7 +1216,6 @@ const renderForumSection = (tribe, items, query = {}) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n.tribeSectionForum, i18n.forumDescription); const replyCount = new Map(); allItems.filter(i => i.contentType === 'forum-reply').forEach(r => replyCount.set(r.parentId, (replyCount.get(r.parentId) || 0) + 1)); const forumScore = (t) => (t.refeeds || 0) + (replyCount.get(t.id) || 0); @@ -1256,7 +1227,7 @@ const renderForumSection = (tribe, items, query = {}) => { const chips = renderTribeModeChips(tribe, 'forum', { modes: ['all', 'recent', 'mine', 'top', ...FORUM_CATEGORIES], filter: isCreate ? 'create' : filter, labelOf, visible, emptyMod, create: member ? { label: i18n.forumCreateButton } : null }); if (isCreate) { - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, div({ class: 'forum-form' }, form({ method: 'POST', action: `${tribeUrl}/forum/create` }, label(i18n.forumCategoryLabel), br(), @@ -1273,7 +1244,7 @@ const renderForumSection = (tribe, items, query = {}) => { } if (query.thread) { - return div({ class: 'tribe-content-list' }, header, chips, + return div({ class: 'tribe-content-list' }, chips, form({ method: 'GET', action: tribeUrl, class: 'forum-back-form' }, input({ type: 'hidden', name: 'section', value: 'forum' }), button({ type: 'submit', class: 'tribe-action-btn' }, i18n.walletBack) @@ -1284,23 +1255,20 @@ const renderForumSection = (tribe, items, query = {}) => { const list = applyTribeFilter(threads, filter, q, modes, forumScore); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, 'forum', filter, q, renderModuleStats(list.length), i18n.forumSearchPlaceholder), list.length ? ul({ class: 'mailing-archive' }, ...paged(list).map(t => { const participants = new Set([t.author, ...allItems.filter(i => i.contentType === 'forum-reply' && i.parentId === t.id).map(r => r.author)].filter(Boolean)); + const threadHref = sectionHref(tribe, 'forum', { thread: t.id }); return li({ class: 'mailing-archive-item' }, - div({ class: 'emergency-update-head mailing-archive-head' }, - div({ class: 'mailing-archive-meta' }, - a({ href: sectionHref(tribe, 'forum', { thread: t.id }), class: 'user-link' }, t.title || 'โ€”'), - t.category ? span({ class: 'forum-category' }, `[${forumCatLabel(t.category)}]`) : null, - span({ class: 'card-label activity-update-counts mailing-counts' }, `๐Ÿ‘ฅ: ${participants.size} ยท ๐Ÿ’ฌ ${replyCount.get(t.id) || 0} ยท โ–ฒ ${t.refeeds || 0}`), - tribeReachChipFor(tribe, t) - ), - tribeItemActions(tribe, t, member, t.title) - ) + renderCardTop(t.id, null, { ...tribeItemOpts(tribe, t, member, t.title), date: t.createdAt, chips: [forumCategoryChip(t), tribeReachChipFor(tribe, t)] }), + div({ class: 'mailing-archive-meta' }, + a({ href: threadHref, class: 'user-link' }, t.title || 'โ€”'), + span({ class: 'card-label activity-update-counts mailing-counts' }, `๐Ÿ‘ฅ: ${participants.size} ยท ๐Ÿ’ฌ ${replyCount.get(t.id) || 0} ยท โ–ฒ ${t.refeeds || 0}`) + ), + null ); }) ) @@ -1325,7 +1293,6 @@ const renderTribeMediaTypeSection = (tribe, items, query = {}, mediaType) => { const q = String(query.q || '').trim(); const isCreate = query.action === 'create' && member; const tribeUrl = tribeUrlOf(tribe); - const header = renderTribeSectionHeader(i18n[cfg.title], i18n[cfg.description]); const emptyMod = !isCreate && moduleIsEmpty(media, filter, 'all', q); const chips = renderTribeModeChips(tribe, cfg.key, { modes: ['all', 'recent', 'mine', 'top'], filter: isCreate ? 'create' : filter, labelOf: m => i18n[`${cfg.filter}${capFirst(m)}`], visible: tribeChipVisible(media, filter, BASE_MODES), emptyMod, create: member ? { label: i18n[cfg.create] } : null }); @@ -1358,7 +1325,7 @@ const renderTribeMediaTypeSection = (tribe, items, query = {}, mediaType) => { tribe.reachAllowed ? renderTribeReachSelector() : null, br(), button({ type: 'submit' }, i18n[cfg.create]) ); - return div({ class: 'tribe-content-list' }, header, chips, div({ class: `div-center ${cfg.formClass}` }, formNode)); + return div({ class: 'tribe-content-list' }, chips, div({ class: `div-center ${cfg.formClass}` }, formNode)); } const list = applyTribeFilter(media, filter, q, BASE_MODES).map(toMediaShape); @@ -1366,13 +1333,12 @@ const renderTribeMediaTypeSection = (tribe, items, query = {}, mediaType) => { const params = { q, hideLastVisit: true, - headerActions: (m) => tribeItemActions(tribe, m, member, m.title), + headerActions: (m) => tribeCardActions(tribe, m, member, m.title), titleChips: (m) => [tribeReachChipFor(tribe, m)], bodyExtra: (m) => [tribeDescription(m.description && m.description !== m.url ? m.description : null), renderTribeReachControls(tribe, m, returnTo)], engagement: (m) => tribeEngagement(tribe, m, member, returnTo) }; return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, cfg.key, filter, q, renderModuleStats(list.length), i18n[cfg.search]), list.length ? div({ class: cfg.wrapper }, cfg.list(paged(list), filter, params)) : renderTribeEmpty(i18n[cfg.empty] || i18n.tribeMediaEmpty) @@ -1386,7 +1352,6 @@ const renderStandaloneSection = (tribe, sectionKey, items, query, cfg) => { const q = String(query.q || '').trim(); const modes = cfg.modes || BASE_MODES; const modeList = cfg.modeList || ['all', 'recent', 'mine']; - const header = renderTribeSectionHeader(cfg.title, cfg.description); const emptyMod = moduleIsEmpty(list, filter, 'all', q); const chips = renderTribeModeChips(tribe, sectionKey, { modes: modeList, filter, labelOf: cfg.labelOf, visible: tribeChipVisible(list, filter, modes), emptyMod, @@ -1394,7 +1359,6 @@ const renderStandaloneSection = (tribe, sectionKey, items, query, cfg) => { }); const shown = applyTribeFilter(list, filter, q, modes, cfg.score || (() => 0)); return div({ class: 'tribe-content-list' }, - header, chips, emptyMod ? null : renderTribeSearchRow(tribe, sectionKey, filter, q, cfg.stats ? cfg.stats(shown) : renderModuleStats(shown.length), cfg.placeholder), shown.length ? cfg.render(paged(shown), member) : renderTribeEmpty(cfg.empty) @@ -1485,7 +1449,6 @@ const renderTribeWikiSectionPage = (tribe, pages, query = {}) => { const emptyMod = moduleIsEmpty(list, filter, 'all', q); const shown = applyTribeFilter(list, filter, q, BASE_MODES, () => 0); const toolbar = div({ class: 'tribe-content-list' }, - renderTribeSectionHeader(i18n.tribeSectionWiki || 'WIKI', i18n.wikiDescription), div({ class: 'mode-buttons-row' }, ...(emptyMod ? [] : [ ...['all', 'recent', 'mine'].filter(tribeChipVisible(list, filter, BASE_MODES)).map(m => @@ -1526,7 +1489,6 @@ const renderTribeRoomsSectionPage = (tribe, sectionData, query = {}) => { const emptyMod = moduleIsEmpty(rooms, filter, 'all', q); const shown = applyTribeFilter(rooms, filter, q, modes, () => 0); const toolbar = div({ class: 'tribe-content-list' }, - renderTribeSectionHeader(i18n.tribeSectionRooms || 'ROOMS', i18n.roomsDescription), renderTribeModeChips(tribe, 'rooms', { modes: ['all', 'recent', 'mine', 'live', 'open', 'closed'], filter, labelOf: m => i18n[`roomFilter${capFirst(m)}`], visible: tribeChipVisible(rooms, filter, modes), emptyMod, create: member ? { action: '/rooms', hidden: { filter: 'create', tribeId: tribe.id }, label: i18n.tribeRoomCreate } : null @@ -1543,7 +1505,6 @@ const renderInhabitantsSection = (tribe, members, query = {}) => { ? resolved.filter(u => [u.name, u.id, u.description].some(v => String(v || '').toLowerCase().includes(search))) : resolved; return div({ class: 'tribe-content-list' }, - renderTribeSectionHeader(i18n.tribeSectionInhabitants, i18n.discoverPeople), resolved.length ? renderTribeSearchRow(tribe, 'inhabitants', null, search, renderModuleStats(shown.length), i18n.searchInhabitantsPlaceholder, 'search') : null, shown.length ? div({ class: 'inhabitants-list inhabitants-grid' }, ...paged(shown).map(u => renderInhabitantCard(u, 'all', userId, false))) @@ -1682,12 +1643,20 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { const hasOpenInvite = !!tribe.openInviteCode; const canManageOpenInvite = hasOpenInvite && (tribe.openInviteBy === userId || isAuthorOfTribe); const hasSideActions = canGenerateInvite || showOwnerControls || canLeave || (isOutsider && !showInviteTop); + const tribeHref = `/tribe/${encodeURIComponent(tribe.id)}`; const tribeDetails = div({ class: 'tribe-details' }, div({ class: 'tribe-side' }, - div({ class: 'card-header activity-card-header' }, - renderContentActions(tribe.id, `/tribe/${encodeURIComponent(tribe.id)}`, { author: tribe.author, reportTitle: tribe.title, deleteAction: showOwnerControls ? contentDeleteAction('tribe', tribe.id) : undefined }) - ), + renderCardTop(tribe.id, tribeHref, { arrow: false, spread: tribe.isAnonymous ? undefined : (tribe.spreads || null), + date: tribe.createdAt, + chips: [ + tribe.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), + tribe.isAnonymous ? renderTribeEncryptedChip(i18n) : null, + renderModeChip(tribe.inviteMode, i18n), + renderLifespanChip(tribe.lifetime, i18n) + ], + author: tribe.author, reportTitle: tribe.title, deleteAction: showOwnerControls ? contentDeleteAction('tribe', tribe.id) : undefined + }), tribe.parentTribe ? div({ class: 'tribe-parent-box' }, h2({ class: 'tribe-info-label' }, i18n.tribeMainTribeLabel || 'MAIN TRIBE'), @@ -1696,29 +1665,17 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { ) ) : null, + renderDetailShare(tribe.id, tribeHref, tribe.title), div({ class: 'shop-title-row' }, h2({ class: 'tribe-card-title' }, tribe.title) ), - div({ class: 'card-chips-row' }, - tribe.isAnonymous ? renderPrivacyChip(true, i18n) : renderPrivacyChip(false, i18n), - tribe.isAnonymous ? renderTribeEncryptedChip(i18n) : null, - renderModeChip(tribe.inviteMode, i18n), - renderLifespanChip(tribe.lifetime, i18n) - ), renderMediaBlob(imageSrc, '/assets/images/default-tribe.png', { alt: tribe.title, class: 'tribe-detail-image' }), - table({ class: 'tribe-info-table' }, + tribe.location ? table({ class: 'tribe-info-table' }, tr( - td({ class: 'tribe-info-label' }, i18n.tribeCreatedAt || 'CREATED'), - td({ class: 'tribe-info-value', colspan: '3' }, moment(tribe.createdAt).format("YYYY/MM/DD HH:mm")) - ), - tr( - td({ class: 'tribe-info-value tribe-author-cell', colspan: '4' }, userLink(tribe.author)) - ), - tribe.location ? tr( td({ class: 'tribe-info-label' }, i18n.tribeLocationLabel || 'LOCATION'), td({ class: 'tribe-info-value', colspan: '3' }, ...renderStyledText(tribe.location)) - ) : null - ), + ) + ) : null, h2({ class: 'tribe-members-count' }, `${i18n.tribeMembersCount}: ${tribe.memberCount != null ? tribe.memberCount : tribe.members.length}`), (isOutsider && hasOpenInvite) ? div({ class: 'tribe-side-actions tribe-side-center' }, @@ -1823,6 +1780,7 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { )) : null ) : null, + null ), div({ class: 'tribe-main' }, query.inviteCode ? div({ class: 'card card-rpg tribe-banner' }, @@ -1837,7 +1795,7 @@ exports.tribeView = async (tribe, userIdParam, query, section, sectionData) => { pageTitle, tribeDetails ); - return section === 'documents' ? `${page}` : page; + return page; }; exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, filterType = '', filterAuthor = '' }) => { @@ -1883,22 +1841,26 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f : mt === 'image' ? `${title}` : mt === 'audio' ? `` : mt === 'video' ? `` - : ['document', 'file', 'torrent'].includes(mt) ? `โ‡ฉ ${title || escapeHtml(kind)}` + : mt === 'document' ? renderClearnetPdfFrame(blob, it.title) + : ['file', 'torrent'].includes(mt) ? `โ‡ฉ ${title || escapeHtml(kind)}` : ''; const url = mt === 'bookmark' ? String(it.url || '') : ''; const href = url ? safeExternalHref(url) : ''; const bookmark = !url ? '' : href ? `${escapeHtml(url)}` : `${escapeHtml(url)}`; - const details = [ + const chip = (d) => `${escapeHtml(String(d))}`; + const chips = [ + it.status ? (statusI18n()[it.status] || it.status) : '', it.date ? `${i18n.tribeEventDate || 'Date'}: ${it.date}` : '', it.deadline ? `${i18n.tribeTaskDeadline || 'Deadline'}: ${it.deadline}` : '', - it.location ? `${i18n.tribeEventLocation || 'Location'}: ${it.location}` : '', - it.status ? (statusI18n()[it.status] || it.status) : '', + it.location ? `${i18n.tribeEventLocation || 'Location'}: ${it.location}` : '' + ].filter(Boolean).map(chip).join(''); + const details = [ it.contentType === 'map' && Number.isFinite(Number(it.lat)) ? `๐Ÿ“ ${Number(it.lat).toFixed(4)}, ${Number(it.lng).toFixed(4)}` : '', ...(it.contentType === 'map' && Array.isArray(it.markers) ? it.markers.map(mk => `๐Ÿ“ ${Number(mk.lat).toFixed(4)}, ${Number(mk.lng).toFixed(4)}${mk.label ? ` ยท ${mk.label}` : ''}`) : []), ...(it.contentType === 'calendar' && Array.isArray(it.dates) ? it.dates.map(d => `๐Ÿ“… ${day(d.date)}${d.label ? ` ยท ${d.label}` : ''}`) : []) - ].filter(Boolean).map(d => `${escapeHtml(String(d))}`).join(''); + ].filter(Boolean).map(chip).join(''); const options = it.contentType === 'votation' && Array.isArray(it.options) && it.options.length ? `
    ${it.options.map(o => `
  • ${escapeHtml(String(o))}
  • `).join('')}
` : ''; @@ -1907,12 +1869,15 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f ? `
${escapeHtml(String(i18n.forumMessages || i18n.tribeForumReplies || 'MESSAGES').toUpperCase())}: ${replies.length}
${replies.map(r => `
${escapeHtml(who(r.author))}${day(r.createdAt) ? ` ยท ${escapeHtml(day(r.createdAt))}` : ''}

${renderRichText(r.description || '')}

`).join('')}
` : ''; const text = it.description && it.description !== url ? renderRichText(it.description) : ''; + const pdfFrames = (Array.isArray(it.pdfs) ? it.pdfs : []).map(p => renderClearnetPdfFrame(cnBlobUrl(p.blob), p.name)).join(''); const when = day(it.createdAt); return `
[${escapeHtml(String(kind).toUpperCase())}]${when ? `๐Ÿ“… ${escapeHtml(when)}` : ''}${it.author ? `${escapeHtml(who(it.author))}` : ''}
+ ${chips ? `
${chips}
` : ''} ${title ? `

${it.href ? `${title}` : title}

` : ''} ${media} ${text ? `

${text}

` : ''} + ${pdfFrames} ${bookmark} ${details ? `
${details}
` : ''} ${options} @@ -1956,7 +1921,7 @@ exports.clearnetTribeView = async ({ tribe, items, names = {}, slug, page = 1, f ogTitle: name, ogDescription: t.description || '', ogImage: cover, - extraCss, + extraCss: extraCss + CLEARNET_PDF_CSS, body }); }; @@ -2146,10 +2111,6 @@ const rulesBlock = (tribe, rules, isCreator) => div({}, const renderGovernance = (tribe, data) => { const { filter, term, candidatures, rules, leaders, isCreator, canPublishToGlobal, alreadyPublishedThisGlobalCycle, hasElectedCandidate } = data || {}; const f = filter || 'government'; - const header = div({ class: 'tags-header module-header-line' }, - h2(i18n.tribeSectionGovernance || 'GOVERNANCE'), - p(i18n.tribeGovernanceDesc || 'Internal governance for this tribe. Propose candidatures, debate rules, elect leaders โ€” mirrors the global Parliament.') - ); let body; if (f === 'government') body = governmentCard(tribe, term, leaders); else if (f === 'candidatures') body = candidaturesBlock(tribe, candidatures, alreadyPublishedThisGlobalCycle); @@ -2174,7 +2135,7 @@ const renderGovernance = (tribe, data) => { } const showPublish = !!(canPublishToGlobal && hasElectedCandidate && !alreadyPublishedThisGlobalCycle); - return div({ class: 'tribe-governance' }, header, governanceFilterBar(tribe.id, f, showPublish), body); + return div({ class: 'tribe-governance' }, governanceFilterBar(tribe.id, f, showPublish), body); }; exports.renderGovernance = renderGovernance; diff --git a/src/views/video_view.js b/src/views/video_view.js index 8c64b43..5da1eeb 100644 --- a/src/views/video_view.js +++ b/src/views/video_view.js @@ -11,7 +11,6 @@ const { video: videoHyperaxe, span, textarea, - label, select, option } = require("../server/node_modules/hyperaxe"); @@ -19,7 +18,8 @@ const { renderCommentsSection: renderSharedCommentsSection, renderCommentsLink } const moment = require("../server/node_modules/moment"); const { renderLicenseChip, renderLicenseSelect, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderTribeOriginChip } = require('./clearnet_view'); -const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, userLink, renderSpreadButton, renderEcoTax, renderLifespanChip, renderContentActions, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf } = require("./main_views"); +const { clearnetItemHref, template, i18n, renderOpinionsVoting, renderEngagement, renderEcoTax, renderLifespanChip, renderCardTop, renderCardByline, renderSpreadEditWarning, renderModuleStats, moduleIsEmpty, contentDeleteAction, paged, blobUrlOf, renderMediaLockNote, renderEditedChip, renderDetailShare } = require("./main_views"); +const { renderDownloadButton } = require("./gallery_view"); const { config } = require("../server/SSB_server.js"); const { renderStyledText } = require("../backend/renderStyledText") const { renderMapLocationVisitLabel, renderMapEmbed } = require("./maps_view"); @@ -64,20 +64,17 @@ const renderVideoPlayer = (videoObj) => const renderVideoOwnerActions = (filter, videoObj, params = {}) => { const returnTo = buildReturnTo(filter, params); const isAuthor = String(videoObj.author) === String(userId); - const hasOpinions = Object.keys(videoObj.opinions || {}).length > 0; if (!isAuthor) return []; const items = []; - if (!hasOpinions) { - items.push( - form( - { method: "GET", action: `/videos/edit/${encodeURIComponent(videoObj.key)}` }, - input({ type: "hidden", name: "returnTo", value: returnTo }), - button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton) - ) - ); - } + items.push( + form( + { method: "GET", action: `/videos/edit/${encodeURIComponent(videoObj.key)}` }, + input({ type: "hidden", name: "returnTo", value: returnTo }), + button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton) + ) + ); return items; }; @@ -90,6 +87,15 @@ const renderVideoCommentsSection = (videoId, comments = [], returnTo = null) => }); }; +const renderVideoUpdated = (videoObj) => { + const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN; + const updatedTs = videoObj.updatedAt ? new Date(videoObj.updatedAt).getTime() : NaN; + const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); + return showUpdated + ? span({ class: "votations-comment-date" }, `${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}`) + : null; +}; + const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) => { const returnTo = buildReturnTo(filter, params); @@ -97,53 +103,37 @@ const renderVideoList = exports.renderVideoList = (videos, filter, params = {}) ? videos.map((videoObj) => { const commentCount = typeof videoObj.commentCount === "number" ? videoObj.commentCount : 0; const title = safeText(videoObj.title); + const href = `/videos/${encodeURIComponent(videoObj.key)}`; const isOwn = videoObj.author && String(videoObj.author) === String(userId); - const headerActions = typeof params.headerActions === "function" - ? params.headerActions(videoObj) + const byline = { + author: videoObj.author, + date: videoObj.createdAt, + chips: [ + videoObj.tribeOrigin ? renderTribeOriginChip(videoObj.tribeOrigin) : videoObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('videos', videoObj.title, videoObj.key)) : null, + renderLicenseChip(videoObj.license), + ...(typeof params.titleChips === "function" ? params.titleChips(videoObj) : []), + renderLifespanChip(videoObj.lifetime, i18n) + ], + bylineExtra: [renderVideoUpdated(videoObj)] + }; + const header = typeof params.headerActions === "function" + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(byline), params.headerActions(videoObj)) : videoObj.tribeOrigin - ? renderContentActions(null, videoObj.tribeOrigin.href) - : renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads || null, author: videoObj.author, favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, isFavorite: videoObj.isFavorite, reportTitle: videoObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('video', videoObj.key) : undefined }); - const titleHref = typeof params.headerActions === "function" ? null : videoObj.tribeOrigin ? videoObj.tribeOrigin.href : `/videos/${encodeURIComponent(videoObj.key)}`; - const engagement = typeof params.engagement === "function" - ? params.engagement(videoObj) - : videoObj.tribeOrigin ? null : renderEngagement(videoObj.key, - renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), - renderCommentsLink({ href: `/videos/${encodeURIComponent(videoObj.key)}`, count: commentCount }) - ); + ? renderCardTop(videoObj.key, videoObj.tribeOrigin.href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || null, reportTitle: videoObj.title }) + : renderCardTop(videoObj.key, href, { ...byline, spread: (params.spreadMap && params.spreadMap.get(videoObj.key)) || params.spreads || null, favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, isFavorite: videoObj.isFavorite, reportTitle: videoObj.title, returnTo, deleteAction: isOwn ? contentDeleteAction('video', videoObj.key) : undefined }); + const titleHref = typeof params.headerActions === "function" ? null : videoObj.tribeOrigin ? videoObj.tribeOrigin.href : href; + const engagement = typeof params.engagement === "function" ? params.engagement(videoObj) : null; return div( { class: "trending-card video-card" + (isOwn ? " own-content" : "") }, - div( - { class: "card-header activity-card-header" }, - span(), - headerActions - ), + header, div( { class: "card-section video-card-body" }, - div({ class: "shop-title-row" }, title ? h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title) : null, videoObj.tribeOrigin ? renderTribeOriginChip(videoObj.tribeOrigin) : videoObj.clearnet === true ? renderReachChip(true, i18n, clearnetItemHref('videos', videoObj.title, videoObj.key)) : null, renderLicenseChip(videoObj.license), ...(typeof params.titleChips === "function" ? params.titleChips(videoObj) : [])), - videoObj.lifetime ? div({ class: "card-chips-row" }, renderLifespanChip(videoObj.lifetime, i18n)) : null, + title ? div({ class: "shop-title-row" }, h2(titleHref ? a({ href: titleHref, class: "card-title-link" }, title) : title)) : null, renderVideoPlayer(videoObj), ...(typeof params.bodyExtra === "function" ? params.bodyExtra(videoObj) : []), - engagement, renderMapLocationVisitLabel(videoObj.mapUrl), - br(), - (() => { - const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN; - const updatedTs = videoObj.updatedAt ? new Date(videoObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(videoObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })() + engagement ) ); }) @@ -165,7 +155,7 @@ const renderVideoForm = (filter, videoId, videoToEdit, params = {}) => { input({ type: "hidden", name: "returnTo", value: returnTo }), span(i18n.videoFileLabel), br(), - input({ type: "file", name: "video", required: filter !== "edit" }), + (filter === "edit" && renderMediaLockNote(videoToEdit)) || input({ type: "file", name: "video", accept: "video/*", required: filter !== "edit" }), br(), br(), span(i18n.videoTitleLabel), @@ -303,27 +293,30 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params for (const a of ownerActions) sideActions.push(a); const tagsNode = renderTags(videoObj.tags); + const href = `/videos/${encodeURIComponent(videoObj.key)}`; - const detailActions = div({ class: "card-header activity-card-header" }, - renderContentActions(videoObj.key, `/videos/${encodeURIComponent(videoObj.key)}`, { + const videoSide = div({ class: "tribe-side" }, + renderCardTop(videoObj.key, href, { arrow: false, author: videoObj.author, + date: videoObj.createdAt, + chips: [ + renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('videos', videoObj.title, videoObj.key) : null), + renderLicenseChip(videoObj.license), + ...chips + ], + bylineExtra: [renderEditedChip(videoObj.key, videoObj.rootId), renderVideoUpdated(videoObj)], favKind: 'videos', torrentFrom: { blobId: videoObj.url, name: videoObj.title }, isFavorite: videoObj.isFavorite, spread: params.spreads || null, returnTo, reportTitle: videoObj.title, deleteAction: String(videoObj.author) === String(userId) ? contentDeleteAction('video', videoObj.key) : undefined - }) - ); - - const videoSide = div({ class: "tribe-side" }, + }), div({ class: "shop-title-row" }, title ? h2({ class: "tribe-card-title" }, title) : null, - renderReachChip(isClearnet, i18n, isClearnet ? clearnetItemHref('videos', videoObj.title, videoObj.key) : null), - isAuthor ? renderClearnetSwitch('videos', videoObj.rootId || videoObj.key, isClearnet) : null, - renderLicenseChip(videoObj.license) + isAuthor ? renderClearnetSwitch('videos', videoObj.rootId || videoObj.key, isClearnet) : null ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, + renderDetailShare(videoObj.key, href, videoObj.title), safeText(videoObj.description) ? p({ class: "tribe-side-description" }, ...renderStyledText(videoObj.description)) : null, @@ -333,29 +326,11 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params ); const videoMain = div({ class: "tribe-main" }, - detailActions, renderVideoPlayer(videoObj), - (() => { - const createdTs = videoObj.createdAt ? new Date(videoObj.createdAt).getTime() : NaN; - const updatedTs = videoObj.updatedAt ? new Date(videoObj.updatedAt).getTime() : NaN; - const showUpdated = Number.isFinite(updatedTs) && (!Number.isFinite(createdTs) || updatedTs !== createdTs); - - return p( - { class: "card-footer" }, - span({ class: "date-link" }, `${moment(videoObj.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(videoObj.author), - showUpdated - ? span( - { class: "votations-comment-date" }, - ` | ${i18n.videoUpdatedAt}: ${moment(videoObj.updatedAt).format("YYYY/MM/DD HH:mm")}` - ) - : null - ); - })(), + videoObj?.url ? renderDownloadButton(blobUrlOf(videoObj.url), videoObj.title || "") : null, renderEngagement(videoObj.key, renderOpinionsVoting('/videos/opinions', videoObj.key, videoObj.opinions, returnTo, videoObj.opinions_inhabitants), - renderVideoCommentsSection(videoObj.key, comments, returnTo) - ) + renderVideoCommentsSection(videoObj.key, comments, returnTo)) ); return template( @@ -370,9 +345,10 @@ exports.singleVideoView = async (videoObj, filter = "all", comments = [], params form( { method: "GET", action: "/videos", class: "ui-toolbar ui-toolbar--filters" }, input({ type: "hidden", name: "sort", value: sort }), - button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("favorites") ? [button( + button({ type: "submit", name: "filter", value: "all", class: filter === "all" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterAll).toUpperCase()), ...(mediaChip("recent") ? [button({ type: "submit", name: "filter", value: "recent", class: filter === "recent" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterRecent).toUpperCase())] : []), ...(mediaChip("mine") ? [button({ type: "submit", name: "filter", value: "mine", class: filter === "mine" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterMine).toUpperCase())] : []), + ...(mediaChip("favorites") ? [button( { type: "submit", name: "filter", value: "favorites", class: filter === "favorites" ? "filter-btn active" : "filter-btn" }, String(i18n.videoFilterFavorites).toUpperCase() )] : []), diff --git a/src/views/vote_view.js b/src/views/vote_view.js index 003882a..f9cb791 100644 --- a/src/views/vote_view.js +++ b/src/views/vote_view.js @@ -1,6 +1,6 @@ -const { div, h2, p, section, button, form, a, textarea, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe"); +const { div, h2, p, section, button, form, a, br, input, table, tr, th, td, label, span } = require("../server/node_modules/hyperaxe"); const { renderCommentsSection: renderSharedCommentsSection } = require("./comments_view"); -const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderSpreadButton, renderContentActions, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderCreatedRows } = require("./main_views"); +const { template, i18n, renderOpinionsVoting, renderEngagement, renderOpenClosedChip, renderLifespanChip, renderEcoTax, renderCardTop, renderCardByline, renderSpreadEditWarning, renderDocumentActions, renderModuleStatsBy, moduleIsEmpty, contentDeleteAction, paged, renderEditedChip, renderMediaLockNote, renderDetailShare } = require("./main_views"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); const { renderTribeOriginChip } = require("./clearnet_view"); @@ -39,9 +39,9 @@ const statusLabel = (s) => { return up; }; -const { isVoteLocked } = require("../models/votes_model"); +const { isVoteEditLocked } = require("../models/votes_model"); const isVoteAuthor = (v) => !!(v && v.createdBy && String(v.createdBy) === String(userId)); -const canModifyVote = (v) => isVoteAuthor(v) && !isVoteLocked(v); +const canModifyVote = (v) => isVoteAuthor(v) && !isVoteEditLocked(v); const renderVoteOwnerActions = (v, returnTo) => { const showUpdateButton = canModifyVote(v); @@ -106,28 +106,28 @@ const renderVoteListItem = exports.renderVoteListItem = (v, voteOptionsDefault, const returnTo = `/votes?filter=${encodeURIComponent(activeFilter || "all")}`; const totalOpinions = Object.values(v.opinions || {}).reduce((s, n) => s + (Number(n) || 0), 0); const isOwn = isVoteAuthor(v); - const headerActions = opts.headerActions !== undefined - ? opts.headerActions - : origin ? null : renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: spreadInfo || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, returnTo, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }); + const topOpts = { author: v.createdBy, date: v.createdAt, chips }; + const cardTop = opts.headerActions !== undefined + ? div({ class: "card-header activity-card-header card-top" }, renderCardByline(topOpts), opts.headerActions) + : origin + ? renderCardTop(v.id, origin.href, { ...topOpts, spread: spreadInfo || null, reportTitle: v.question }) + : renderCardTop(v.id, `/votes/${encodeURIComponent(v.id)}`, { ...topOpts, spread: spreadInfo || null, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, returnTo, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }); return div({ class: "trending-card vote-card" + (isOwn ? " own-content" : "") }, - headerActions ? div({ class: "card-header activity-card-header" }, - span(), - headerActions - ) : null, + cardTop, div({ class: "card-section vote-card-body" }, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, href ? a({ href }, v.question || i18n.votationsTitle) : (v.question || i18n.votationsTitle) ) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, v.deadline ? p({ class: "time-chip" }, moment(v.deadline).format("YYYY/MM/DD HH:mm")) : null, origin ? null : div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) ), origin ? null : div({ class: `job-meta-line vote-outcome vote-outcome-${outcome.variant}` }, `${i18n.voteResults || "Results"}: ${outcome.text}`), - ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []) + ...(Array.isArray(opts.bodyExtra) ? opts.bodyExtra.filter(Boolean) : []), + null ) ); }; @@ -195,14 +195,12 @@ const renderVoteDetail = (v, voteOptionsDefault, firstRow, secondRow, mode, acti pushRow(i18n.voteStatus, statusLabel(v.status)); const voteSide = div({ class: "tribe-side" }, - div({ class: "card-header activity-card-header" }, - renderContentActions(v.id, `/votes/${encodeURIComponent(v.id)}`, { spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }) - ), + renderCardTop(v.id, `/votes/${encodeURIComponent(v.id)}`, { arrow: false, date: v.createdAt, chips, bylineExtra: [renderEditedChip(v.id, v.rootId)], spread: params.spreads || null, author: v.createdBy, favKind: 'votes', isFavorite: v.isFavorite, reportTitle: v.question, deleteAction: canModifyVote(v) ? contentDeleteAction('votes', v.id) : undefined }), div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, v.question) ), - chips.length ? div({ class: "card-chips-row" }, ...chips) : null, - table({ class: "tribe-info-table jobs-info-table" }, ...infoRows, ...renderCreatedRows(v.createdAt, v.createdBy)), + renderDetailShare(v.id, `/votes/${encodeURIComponent(v.id)}`, v.question), + table({ class: "tribe-info-table jobs-info-table" }, ...infoRows), tagsNode, div({ class: "tribe-card-members" }, span({ class: "tribe-members-count" }, `${i18n.eventAttendees}: ${totalVotesNum}`) @@ -300,6 +298,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, : (voteToEdit.deadline ? moment(voteToEdit.deadline).format("YYYY-MM-DDTHH:mm") : ""); const questionValue = draft.question !== undefined && draft.question !== "" ? String(draft.question) : (voteToEdit.question || ""); const tagsValue = draft.tags ? String(draft.tags) : editTags.join(", "); + const opinionLock = mode === "edit" ? renderMediaLockNote(voteToEdit) : null; return template( title, @@ -341,7 +340,7 @@ exports.voteView = async (votes, mode, voteId, comments = [], activeFilterParam, { action: mode === "edit" ? `/votes/update/${encodeURIComponent(voteId)}` : "/votes/create", method: "POST" }, input({ type: "hidden", name: "returnTo", value: listReturnTo }), h2(i18n.voteQuestionLabel), - input({ type: "text", name: "question", id: "question", required: true, value: questionValue }), br(), br(), + input({ type: "text", name: "question", id: "question", required: true, value: questionValue, ...(opinionLock ? { readonly: true } : {}) }), opinionLock, br(), br(), label(i18n.voteDeadlineLabel), br(), input({ type: "datetime-local", diff --git a/src/views/wallet_view.js b/src/views/wallet_view.js index 6c99485..1a8c97e 100644 --- a/src/views/wallet_view.js +++ b/src/views/wallet_view.js @@ -2,6 +2,8 @@ const { form, button, div, h2, p, section, input, span, table, thead, tbody, tr, const moment = require("../server/node_modules/moment"); const { template, i18n, userLink, renderWalletChip, renderStateChip, paged } = require('./main_views'); +const ECOIN_SOURCE_URL = 'https://github.com/epsylon/ecoin'; + const TX_CATEGORY_CHIPS = { receive: ["mutuals", "\u2B07"], send: ["whole", "\u2B06"], @@ -213,7 +215,11 @@ exports.walletErrorView = async (error) => { const header = div({ class: 'tags-header module-header-line' }, h2(i18n.walletTitle), p(i18n.walletDescription), renderWalletChip()); const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {}; if (![w.url, w.user, w.pass].every(v => String(v || '').trim())) { - return template(i18n.walletTitle, section(header, div({ class: "wallet-error" }, h2(i18n.walletStatus), p(i18n.walletNotConfigured, ' ', a({ href: '/settings#wallet' }, i18n.walletConfiguration)), process.env.OASIS_MOBILE === '1' ? div({ class: "wallet-error-actions" }, a({ href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" }, i18n.walletDownloadEcoin)) : null))); + return template(i18n.walletTitle, section(header, div({ class: "wallet-error" }, h2(i18n.walletStatus), p(i18n.walletNotConfigured), + div({ class: "wallet-error-actions" }, + a(process.env.OASIS_MOBILE === '1' ? { href: require('../backend/updater.js').ecoinWalletDownloadUrl(), class: "update-download-link" } : { href: ECOIN_SOURCE_URL, class: "update-download-link", target: "_blank", rel: "noopener noreferrer" }, i18n.walletDownloadEcoin), + a({ href: '/settings#wallet', class: "update-download-link" }, i18n.walletConfiguration) + )))); } return template( i18n.walletTitle, diff --git a/src/views/wiki_view.js b/src/views/wiki_view.js index 4caba70..902047b 100644 --- a/src/views/wiki_view.js +++ b/src/views/wiki_view.js @@ -1,8 +1,8 @@ -const { div, h2, h3, p, section, button, form, a, span, br, textarea, input, label, select, option, ul, li, img } = require("../server/node_modules/hyperaxe"); -const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderContentActions, renderSubscriptionBox, renderModuleStats, moduleIsEmpty, renderCardMetaRow, paged } = require("./main_views"); +const { div, h2, h3, p, section, button, form, a, span, br, input, label, select, option, ul, li, img } = require("../server/node_modules/hyperaxe"); +const { clearnetItemHref, template, i18n, userLink, renderStateChip, renderCardTop, renderSubscriptionBox, renderModuleStats, renderOpinionsVoting, paged, renderEngagement, renderDetailShare } = require("./main_views"); const { renderEncryptedChip, renderReachChip, renderClearnetSelector, renderClearnetSwitch, renderLicenseChip, renderLicenseSelect, renderTribeOriginChip } = require("./clearnet_view"); const { renderStyledText, richTextarea } = require("../backend/renderStyledText"); -const { WIKILINK_RE, slugify, linkTarget } = require("../models/wiki_model"); +const { renderCommentsSection } = require("./comments_view"); const moment = require("../server/node_modules/moment"); const { config } = require("../server/SSB_server.js"); @@ -89,18 +89,16 @@ const renderTags = (tags) => (Array.isArray(tags) && tags.length) const renderPageCard = (page, params = {}) => { const origin = page.tribeOrigin || null; const href = origin ? origin.href : pageHref(page.tribeId ? page.id : page.slug, page.tribeId); + const chips = [...(origin ? [renderTribeOriginChip(origin)] : pageChips(page)), !origin && page.clearnet === true ? reachChip(page) : null, renderLicenseChip(page.license)]; return div({ class: "tribe-card wiki-card" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(page.id, href, origin - ? { author: page.author, reportTitle: page.title } - : { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: (params.spreadMap && params.spreadMap.get(page.id)) || null, deleteAction: !page.tribeId && String(page.author) === String(userId) ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) - ), + renderCardTop(page.id, href, origin + ? { date: page.createdAt, chips, author: page.author, reportTitle: page.title } + : { date: page.createdAt, chips, author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: (params.spreadMap && params.spreadMap.get(page.id)) || null, deleteAction: !page.tribeId && String(page.author) === String(userId) ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }), div({ class: "tribe-card-body" }, page.image ? a({ href }, img({ loading: 'lazy', class: "wiki-card-cover", src: `/blob/${encodeURIComponent(page.image)}`, alt: page.title || "" })) : null, div({ class: "shop-title-row" }, h2({ class: "tribe-card-title" }, a({ href }, page.title || "โ€”"))), - div({ class: "card-chips-row" }, ...(origin ? [renderTribeOriginChip(origin)] : pageChips(page)), !origin && page.clearnet === true ? reachChip(page) : null, renderLicenseChip(page.license)), - origin && params.withBody && page.body ? renderWikiBody(page.body) : null + origin && params.withBody && page.body ? renderWikiBody(page.body) : null, + null ) ); }; @@ -302,9 +300,7 @@ exports.wikiPageView = async (page, params = {}) => { const subscriptionNodes = params.subscription ? renderSubscriptionBox({ target: page.id, scope: "wiki", subscribed: params.subscription.subscribed, count: params.subscription.count, isOwner: page.isOwner, returnTo: base, inline: true, compact: true }) : null; - const barChips = [reachChip(page), statusChip(page)].filter(Boolean); const actions = div({ class: "tribe-side-actions wiki-actions-top" }, - span({ class: "wiki-actions-left" }, ...barChips), page.isOwner && !tribeId && !page.encrypted ? renderClearnetSwitch("wiki", page.rootId || page.id, !!page.clearnet) : null, ...(subscriptionNodes && subscriptionNodes.length ? subscriptionNodes : []), page.canEdit && !version @@ -337,21 +333,18 @@ exports.wikiPageView = async (page, params = {}) => { section(renderHeader(params.tribe || null), renderFilters("all", "", { ...params, emptyMod: census.length === 0 }, census)), section( div({ class: "trending-card wiki-page" }, - div({ class: "card-header activity-card-header" }, - span(), - renderContentActions(page.id, base, { author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: params.spread || null, deleteAction: page.isOwner && !tribeId ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined }) - ), + renderCardTop(page.id, base, { arrow: false, + date: page.createdAt, + chips: [reachChip(page), statusChip(page), renderLicenseChip(page.license), ...pageChips(page, params, { withStatus: false })], + author: page.author, reportTitle: page.title, favKind: 'wiki', isFavorite: page.isFavorite, spread: params.spread || null, deleteAction: page.isOwner && !tribeId ? `/wiki/delete/${encodeURIComponent(page.id)}` : undefined + }), div({ class: "card-section" }, actions, - div({ class: "shop-title-row" }, h2({ class: "wiki-page-title" }, shownTitle), renderLicenseChip(page.license)), - (() => { const rest = pageChips(page, params, { withStatus: false }); return rest.length ? div({ class: "card-chips-row" }, ...rest) : null; })(), + div({ class: "shop-title-row" }, h2({ class: "wiki-page-title" }, shownTitle)), version ? p({ class: "wiki-version-notice" }, `${i18n.wikiViewingVersion} (${moment(version.createdAt).format("YYYY/MM/DD HH:mm")}) โ€” `, a({ href: base }, i18n.wikiBackToPage)) : null, diffBlock || renderWikiBody(shownBody, { missing: page.missingLinks, tribeId }), renderTags(page.tags), - renderCardMetaRow(p({ class: "card-footer" }, - span({ class: "date-link" }, `${moment(page.createdAt).format("YYYY/MM/DD HH:mm")}`), - userLink(page.author) - )) + renderDetailShare(page.id, base, page.title) ), ), div({ class: "card-section wiki-backlinks" }, @@ -359,6 +352,10 @@ exports.wikiPageView = async (page, params = {}) => { page.linkedPages && page.linkedPages.length ? ul(...page.linkedPages.map(b => li(a({ href: pageHref(b.tribeId ? b.id : b.slug, b.tribeId) }, b.title)))) : p(i18n.wikiNoLinkedPages) + ), + renderEngagement(page.id, + params.opinions ? renderOpinionsVoting('/wiki/opinions', page.id, params.opinions.opinions, `/wiki/${encodeURIComponent(page.id)}`, params.opinions.opinions_inhabitants) : null, + Array.isArray(params.comments) ? renderCommentsSection({ action: `/wiki/${encodeURIComponent(page.id)}/comments`, comments: params.comments, returnTo: `/wiki/${encodeURIComponent(page.id)}` }) : null ) ) );