diff --git a/Cargo.lock b/Cargo.lock index f93e7eec9..c94752777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1735,9 +1735,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +checksum = "bf04c5ec15464ace8355a7b440a33aece288993475556d461154d7a62ad9947c" dependencies = [ "bit-set", "regex-automata", @@ -3054,9 +3054,9 @@ dependencies = [ [[package]] name = "oxc_resolver" -version = "11.6.2" +version = "11.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d84cdcd778d15db5b21cc61baf79ac55cee97e7feb725b2664453979ba3cd76" +checksum = "0784392356fa78dd4c9047fce7c9046c141f8990736792d00310bf40935b1870" dependencies = [ "cfg-if", "indexmap 2.11.0", @@ -3407,9 +3407,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "pnp" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3167cbab15e437e9c7db8a4cf613eb4a77583d4327a8964d50fedd6cf364bd" +checksum = "e001eb06e9523653f2a88adb94e286ef5d7acfe64158b21bf57a6f6bc3ba65ca" dependencies = [ "byteorder", "clean-path", @@ -3602,9 +3602,12 @@ dependencies = [ [[package]] name = "rapidhash" -version = "3.1.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efee4b7317469c6c6e7fdeee3d094313af846a97678d6ed971d83a852d730083" +checksum = "9d126f24bc587b080d7823a831d0fe832090a606fd3fd244ecbe23c561104ab8" +dependencies = [ + "rustversion", +] [[package]] name = "rayon" diff --git a/TODO.md b/TODO.md index 48b8141d3..23896f4aa 100644 --- a/TODO.md +++ b/TODO.md @@ -10,30 +10,19 @@ - pull latest version and notify is out of date - _computer_ - **add rollback of states (in stateful)** - - remove configurable format (raw/compressed) and chose sane ones instead - - linear reads: compressed (height/date/... + txindex_to_height + txindex_to_version + ...) - - random reads: raw (outputindex_to_value + ...) - add costs basis by percentile (percentile cost basis) back - add support for per index computation - fix min feerate which is always ZERO due to coinbase transaction - before computing multiple sources check their length, panic if not equal - add oracle price dataset (https://utxo.live/oracle/UTXOracle.py) - add address counts relative to all datasets - - make decade, quarter, year datasets `computed` instead of `eager` - - add 6 months (semester) interval datasets to builder - - some datasets in `indexes` can probably be removed - add revived/sent supply datasets - add `in-sats` version of all price datasets (average and co) - add `p2pk` group (sum of `p2pk33` and `p2pk65`) - add chopiness datasets - add utxo count, address count, supply data for by reused addresses in groups by address type - add more date ranges (3-6 months and more) - - add puell multiple dataset - add pi cycle dataset - - add emas of price - - add 7d and 30d ema to sell side risk ratio and sopr - - don't compute everything for all cohorts as some datasets combinations are irrelevant - - addresses/utxos by amount don't need mvrvz for example - add all possible charts from: - https://mainnet.observer - https://glassnode.com diff --git a/crates/brk_structs/Cargo.toml b/crates/brk_structs/Cargo.toml index bbb96e93b..dbf63283e 100644 --- a/crates/brk_structs/Cargo.toml +++ b/crates/brk_structs/Cargo.toml @@ -17,7 +17,7 @@ vecdb = {workspace = true} byteview = { workspace = true } derive_deref = { workspace = true } jiff = { workspace = true } -rapidhash = "3.1.0" +rapidhash = "4.0.0" serde = { workspace = true } serde_bytes = { workspace = true } zerocopy = { workspace = true } diff --git a/websites/default/index.html b/websites/default/index.html index 86284aca5..24356a4ad 100644 --- a/websites/default/index.html +++ b/websites/default/index.html @@ -556,6 +556,10 @@ &:has(input:checked) { color: var(--color); } + + [data-screenshot="true"] &:has(input:not(:checked)) { + display: none; + } } main { @@ -971,7 +975,6 @@ flex-shrink: 0; display: flex; align-items: center; - gap: 1.5rem; margin: -0.5rem var(--negative-main-padding); padding: 0.75rem var(--main-padding); overflow-x: auto; @@ -989,13 +992,33 @@ margin-top: 2rem; margin-bottom: 1rem; + button#screenshot { + width: 100%; + text-align: left; + padding-left: 1rem; + height: 100%; + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + + [data-screenshot="true"] & { + display: none; + } + } + > legend { + gap: 1.5rem; + &:empty { display: none; } > div { + [data-screenshot="true"] &:has(input:not(:checked)) { + display: none; + } + flex: 0; + height: 100%; display: flex; align-items: center; @@ -1033,6 +1056,11 @@ padding-right: 0.375rem; margin-left: -0.375rem; margin-right: -0.375rem; + + [data-screenshot="true"] & { + width: 0; + opacity: 0; + } } } } @@ -1118,6 +1146,16 @@ width: 100%; min-height: 0; padding: var(--main-padding); + background-color: var(--background-color); + + p#domain { + position: absolute; + left: 2rem; + top: 0.75rem; + color: var(--gray); + font-size: var(--font-size-sm); + line-height: var(--line-height-sm); + } header { flex-shrink: 0; @@ -1141,6 +1179,17 @@ flex: 1; } + #interval { + > span { + display: none; + + [data-screenshot="true"] & { + color: var(--gray); + display: inline-block; + } + } + } + > .chart > legend, > fieldset { z-index: 20; diff --git a/websites/default/packages/.gitignore b/websites/default/packages/.gitignore index 806ef71cf..f388350fb 100644 --- a/websites/default/packages/.gitignore +++ b/websites/default/packages/.gitignore @@ -1,7 +1,7 @@ LICENSE *.json *webcomponent* -README.md +README*.md cli* extras/ *.cjs @@ -9,3 +9,7 @@ dev.js *.development* *.iife.* nano.* +worker.* +*.ts +*.mts +*.cts diff --git a/websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.d.ts b/websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.d.ts deleted file mode 100644 index 2dbef82e0..000000000 --- a/websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.d.ts +++ /dev/null @@ -1,215 +0,0 @@ -declare class uFuzzy { - constructor(opts?: uFuzzy.Options); - - /** search API composed of filter/info/sort, with a info/ranking threshold (1e3) and fast outOfOrder impl */ - search( - haystack: string[], - needle: string, - /** limit how many terms will be permuted, default = 0; 5 will result in up to 5! (120) search iterations. be careful with this! */ - outOfOrder?: number, - /** default = 1e3 */ - infoThresh?: number, - preFiltered?: uFuzzy.HaystackIdxs | null, - ): uFuzzy.SearchResult; - - /** initial haystack filter, can accept idxs from previous prefix/typeahead match as optimization */ - filter( - haystack: string[], - needle: string, - idxs?: uFuzzy.HaystackIdxs, - ): uFuzzy.HaystackIdxs | null; - - /** collects stats about pre-filtered matches, does additional filtering based on term boundary settings, finds highlight ranges */ - info( - idxs: uFuzzy.HaystackIdxs, - haystack: string[], - needle: string, - ): uFuzzy.Info; - - /** performs final result sorting via Array.sort(), relying on Info */ - sort( - info: uFuzzy.Info, - haystack: string[], - needle: string, - ): uFuzzy.InfoIdxOrder; - - /** utility for splitting needle into terms following defined interSplit/intraSplit opts. useful for out-of-order permutes */ - split(needle: string, keepCase?: boolean): uFuzzy.Terms; - - /** util for creating out-of-order permutations of a needle terms array */ - static permute(arr: unknown[]): unknown[][]; - - /** util for replacing common diacritics/accents */ - static latinize(strings: T): T; - - /** util for highlighting matched substr parts of a result */ - static highlight( - match: string, - ranges: number[], - - mark?: (part: string, matched: boolean) => TMarkedPart, - accum?: TAccum, - append?: (accum: TAccum, part: TMarkedPart) => TAccum | undefined, - ): TAccum; -} - -export = uFuzzy; - -declare namespace uFuzzy { - /** needle's terms */ - export type Terms = string[]; - - /** subset of idxs of a haystack array */ - export type HaystackIdxs = number[]; - - /** sorted order in which info facets should be iterated */ - export type InfoIdxOrder = number[]; - - export type AbortedResult = [null, null, null]; - - export type FilteredResult = [uFuzzy.HaystackIdxs, null, null]; - - export type RankedResult = [ - uFuzzy.HaystackIdxs, - uFuzzy.Info, - uFuzzy.InfoIdxOrder, - ]; - - export type SearchResult = FilteredResult | RankedResult | AbortedResult; - - /** partial RegExp */ - type PartialRegExp = string; - - /** what should be considered acceptable term bounds */ - export const enum BoundMode { - /** will match 'man' substr anywhere. e.g. tasmania */ - Any = 0, - /** will match 'man' at whitespace, punct, case-change, and alpha-num boundaries. e.g. mantis, SuperMan, fooManBar, 0007man */ - Loose = 1, - /** will match 'man' at whitespace, punct boundaries only. e.g. mega man, walk_man, man-made, foo.man.bar */ - Strict = 2, - } - - export const enum IntraMode { - /** allows any number of extra char insertions within a term, but all term chars must be present for a match */ - MultiInsert = 0, - /** allows for a single-char substitution, transposition, insertion, or deletion within terms (excluding first and last chars) */ - SingleError = 1, - } - - export type IntraSliceIdxs = [from: number, to: number]; - - type CompareFn = (a: string, b: string) => number; - - export interface Options { - // whether regexps use a /u unicode flag - unicode?: boolean; // false - - /** @deprecated renamed to opts.alpha */ - letters?: PartialRegExp | null; // a-z - - // regexp character class [] of chars which should be treated as letters (case insensitive) - alpha?: PartialRegExp | null; // a-z - - /** term segmentation & punct/whitespace merging */ - interSplit?: PartialRegExp; // '[^A-Za-z\\d']+' - intraSplit?: PartialRegExp | null; // '[a-z][A-Z]' - - /** inter bounds that will be used to increase lft2/rgt2 info counters */ - interBound?: PartialRegExp | null; // '[^A-Za-z\\d]' - /** intra bounds that will be used to increase lft1/rgt1 info counters */ - intraBound?: PartialRegExp | null; // '[A-Za-z][0-9]|[0-9][A-Za-z]|[a-z][A-Z]' - - /** inter-term modes, during .info() can discard matches when bounds conditions are not met */ - interLft?: BoundMode; // 0 - interRgt?: BoundMode; // 0 - - /** allowance between terms */ - interChars?: PartialRegExp; // '.' - interIns?: number; // Infinity - - /** allowance between chars within terms */ - intraChars?: PartialRegExp; // '[a-z\\d]' - intraIns?: number; // 0 - - /** contractions detection */ - intraContr?: PartialRegExp; // "'[a-z]{1,2}\\b" - - /** error tolerance mode within terms. will clamp intraIns to 1 when set to SingleError */ - intraMode?: IntraMode; // 0 - - /** which part of each term should tolerate errors (when intraMode: 1) */ - intraSlice?: IntraSliceIdxs; // [1, Infinity] - - /** max substitutions (when intraMode: 1) */ - intraSub?: 0 | 1; // 0 - /** max transpositions (when intraMode: 1) */ - intraTrn?: 0 | 1; // 0 - /** max omissions/deletions (when intraMode: 1) */ - intraDel?: 0 | 1; // 0 - - /** can dynamically adjust error tolerance rules per term in needle (when intraMode: 1) */ - intraRules?: (term: string) => { - intraSlice?: IntraSliceIdxs; - intraIns: 0 | 1; - intraSub: 0 | 1; - intraTrn: 0 | 1; - intraDel: 0 | 1; - }; - - /** post-filters matches during .info() based on cmp of term in needle vs partial match */ - intraFilt?: (term: string, match: string, index: number) => boolean; // should this also accept WIP info? - - /** default: toLocaleUpperCase() */ - toUpper?: (str: string) => string; - - /** default: toLocaleLowerCase() */ - toLower?: (str: string) => string; - - /** final sorting cmp when all other match metrics are equal */ - compare?: CompareFn; - - sort?: ( - info: Info, - haystack: string[], - needle: string, - compare?: CompareFn, - ) => InfoIdxOrder; - } - - export interface Info { - /** matched idxs from haystack */ - idx: HaystackIdxs; - - /** match offsets */ - start: number[]; - - /** number of left BoundMode.Strict term boundaries found */ - interLft2: number[]; - /** number of right BoundMode.Strict term boundaries found */ - interRgt2: number[]; - /** number of left BoundMode.Loose term boundaries found */ - interLft1: number[]; - /** number of right BoundMode.Loose term boundaries found */ - interRgt1: number[]; - - /** total number of extra chars matched within all terms. higher = matched terms have more fuzz in them */ - intraIns: number[]; - /** total number of chars found in between matched terms. higher = terms are more sparse, have more fuzz in between them */ - interIns: number[]; - - /** total number of matched contiguous chars (substrs but not necessarily full terms) */ - chars: number[]; - - /** number of exactly-matched terms (intra = 0) where both lft and rgt landed on a BoundMode.Loose or BoundMode.Strict boundary */ - terms: number[]; - - /** number of needle terms with case-sensitive partial matches */ - cases: number[]; - - /** offset ranges within match for highlighting: [startIdx0, endIdx0, startIdx1, endIdx1,...] */ - ranges: number[][]; - } -} - -export as namespace uFuzzy; diff --git a/websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.mjs b/websites/default/packages/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs similarity index 97% rename from websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.mjs rename to websites/default/packages/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs index 40afbae8c..7103bd55f 100644 --- a/websites/default/packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.mjs +++ b/websites/default/packages/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs @@ -4,7 +4,7 @@ * * uFuzzy.js (μFuzzy) * A tiny, efficient fuzzy matcher that doesn't suck -* https://github.com/leeoniya/uFuzzy (v1.0.18) +* https://github.com/leeoniya/uFuzzy (v1.0.19) */ const cmp = (a, b) => a > b ? 1 : a < b ? -1 : 0; @@ -925,40 +925,50 @@ function uFuzzy(opts) { const latinize = (() => { let accents = { - A: 'ÁÀÃÂÄĄ', - a: 'áàãâäą', - E: 'ÉÈÊËĖ', - e: 'éèêëę', - I: 'ÍÌÎÏĮ', - i: 'íìîïį', + A: 'ÁÀÃÂÄĄĂÅ', + a: 'áàãâäąăå', + E: 'ÉÈÊËĖĚ', + e: 'éèêëęě', + I: 'ÍÌÎÏĮİ', + i: 'íìîïįı', O: 'ÓÒÔÕÖ', o: 'óòôõö', - U: 'ÚÙÛÜŪŲ', - u: 'úùûüūų', + U: 'ÚÙÛÜŪŲŮŰ', + u: 'úùûüūųůű', C: 'ÇČĆ', c: 'çčć', + D: 'Ď', + d: 'ď', + G: 'Ğ', + g: 'ğ', L: 'Ł', l: 'ł', - N: 'ÑŃ', - n: 'ñń', - S: 'ŠŚ', - s: 'šś', - Z: 'ŻŹ', - z: 'żź' + N: 'ÑŃŇ', + n: 'ñńň', + S: 'ŠŚȘŞ', + s: 'šśșş', + T: 'ŢȚŤ', + t: 'ţțť', + Y: 'Ý', + y: 'ý', + Z: 'ŻŹŽ', + z: 'żźž' }; - let accentsMap = new Map(); + // str.normalize("NFD").replace(/\p{Diacritic}/gu, "") + + let accentsMap = {}; let accentsTpl = ''; for (let r in accents) { accents[r].split('').forEach(a => { accentsTpl += a; - accentsMap.set(a, r); + accentsMap[a] = r; }); } let accentsRe = new RegExp(`[${accentsTpl}]`, 'g'); - let replacer = m => accentsMap.get(m); + let replacer = m => accentsMap[m]; return strings => { if (typeof strings == 'string') diff --git a/websites/default/packages/modern-screenshot/4.6.6/.gitignore b/websites/default/packages/modern-screenshot/4.6.6/.gitignore new file mode 100644 index 000000000..a6c7c2852 --- /dev/null +++ b/websites/default/packages/modern-screenshot/4.6.6/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/websites/default/packages/modern-screenshot/4.6.6/dist/index.mjs b/websites/default/packages/modern-screenshot/4.6.6/dist/index.mjs new file mode 100644 index 000000000..e3c44f815 --- /dev/null +++ b/websites/default/packages/modern-screenshot/4.6.6/dist/index.mjs @@ -0,0 +1,1652 @@ +function changeJpegDpi(uint8Array, dpi) { + uint8Array[13] = 1; + uint8Array[14] = dpi >> 8; + uint8Array[15] = dpi & 255; + uint8Array[16] = dpi >> 8; + uint8Array[17] = dpi & 255; + return uint8Array; +} + +const _P = "p".charCodeAt(0); +const _H = "H".charCodeAt(0); +const _Y = "Y".charCodeAt(0); +const _S = "s".charCodeAt(0); +let pngDataTable; +function createPngDataTable() { + const crcTable = new Int32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; + } + crcTable[n] = c; + } + return crcTable; +} +function calcCrc(uint8Array) { + let c = -1; + if (!pngDataTable) + pngDataTable = createPngDataTable(); + for (let n = 0; n < uint8Array.length; n++) { + c = pngDataTable[(c ^ uint8Array[n]) & 255] ^ c >>> 8; + } + return c ^ -1; +} +function searchStartOfPhys(uint8Array) { + const length = uint8Array.length - 1; + for (let i = length; i >= 4; i--) { + if (uint8Array[i - 4] === 9 && uint8Array[i - 3] === _P && uint8Array[i - 2] === _H && uint8Array[i - 1] === _Y && uint8Array[i] === _S) { + return i - 3; + } + } + return 0; +} +function changePngDpi(uint8Array, dpi, overwritepHYs = false) { + const physChunk = new Uint8Array(13); + dpi *= 39.3701; + physChunk[0] = _P; + physChunk[1] = _H; + physChunk[2] = _Y; + physChunk[3] = _S; + physChunk[4] = dpi >>> 24; + physChunk[5] = dpi >>> 16; + physChunk[6] = dpi >>> 8; + physChunk[7] = dpi & 255; + physChunk[8] = physChunk[4]; + physChunk[9] = physChunk[5]; + physChunk[10] = physChunk[6]; + physChunk[11] = physChunk[7]; + physChunk[12] = 1; + const crc = calcCrc(physChunk); + const crcChunk = new Uint8Array(4); + crcChunk[0] = crc >>> 24; + crcChunk[1] = crc >>> 16; + crcChunk[2] = crc >>> 8; + crcChunk[3] = crc & 255; + if (overwritepHYs) { + const startingIndex = searchStartOfPhys(uint8Array); + uint8Array.set(physChunk, startingIndex); + uint8Array.set(crcChunk, startingIndex + 13); + return uint8Array; + } else { + const chunkLength = new Uint8Array(4); + chunkLength[0] = 0; + chunkLength[1] = 0; + chunkLength[2] = 0; + chunkLength[3] = 9; + const finalHeader = new Uint8Array(54); + finalHeader.set(uint8Array, 0); + finalHeader.set(chunkLength, 33); + finalHeader.set(physChunk, 37); + finalHeader.set(crcChunk, 50); + return finalHeader; + } +} +const b64PhysSignature1 = "AAlwSFlz"; +const b64PhysSignature2 = "AAAJcEhZ"; +const b64PhysSignature3 = "AAAACXBI"; +function detectPhysChunkFromDataUrl(dataUrl) { + let b64index = dataUrl.indexOf(b64PhysSignature1); + if (b64index === -1) { + b64index = dataUrl.indexOf(b64PhysSignature2); + } + if (b64index === -1) { + b64index = dataUrl.indexOf(b64PhysSignature3); + } + return b64index; +} + +const PREFIX = "[modern-screenshot]"; +const IN_BROWSER = typeof window !== "undefined"; +const SUPPORT_WEB_WORKER = IN_BROWSER && "Worker" in window; +const SUPPORT_ATOB = IN_BROWSER && "atob" in window; +const SUPPORT_BTOA = IN_BROWSER && "btoa" in window; +const USER_AGENT = IN_BROWSER ? window.navigator?.userAgent : ""; +const IN_CHROME = USER_AGENT.includes("Chrome"); +const IN_SAFARI = USER_AGENT.includes("AppleWebKit") && !IN_CHROME; +const IN_FIREFOX = USER_AGENT.includes("Firefox"); +const isContext = (value) => value && "__CONTEXT__" in value; +const isCssFontFaceRule = (rule) => rule.constructor.name === "CSSFontFaceRule"; +const isCSSImportRule = (rule) => rule.constructor.name === "CSSImportRule"; +const isElementNode = (node) => node.nodeType === 1; +const isSVGElementNode = (node) => typeof node.className === "object"; +const isSVGImageElementNode = (node) => node.tagName === "image"; +const isSVGUseElementNode = (node) => node.tagName === "use"; +const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== "undefined" && !isSVGElementNode(node); +const isCommentNode = (node) => node.nodeType === 8; +const isTextNode = (node) => node.nodeType === 3; +const isImageElement = (node) => node.tagName === "IMG"; +const isVideoElement = (node) => node.tagName === "VIDEO"; +const isCanvasElement = (node) => node.tagName === "CANVAS"; +const isTextareaElement = (node) => node.tagName === "TEXTAREA"; +const isInputElement = (node) => node.tagName === "INPUT"; +const isStyleElement = (node) => node.tagName === "STYLE"; +const isScriptElement = (node) => node.tagName === "SCRIPT"; +const isSelectElement = (node) => node.tagName === "SELECT"; +const isSlotElement = (node) => node.tagName === "SLOT"; +const isIFrameElement = (node) => node.tagName === "IFRAME"; +const consoleWarn = (...args) => console.warn(PREFIX, ...args); +function supportWebp(ownerDocument) { + const canvas = ownerDocument?.createElement?.("canvas"); + if (canvas) { + canvas.height = canvas.width = 1; + } + return Boolean(canvas) && "toDataURL" in canvas && Boolean(canvas.toDataURL("image/webp").includes("image/webp")); +} +const isDataUrl = (url) => url.startsWith("data:"); +function resolveUrl(url, baseUrl) { + if (url.match(/^[a-z]+:\/\//i)) + return url; + if (IN_BROWSER && url.match(/^\/\//)) + return window.location.protocol + url; + if (url.match(/^[a-z]+:/i)) + return url; + if (!IN_BROWSER) + return url; + const doc = getDocument().implementation.createHTMLDocument(); + const base = doc.createElement("base"); + const a = doc.createElement("a"); + doc.head.appendChild(base); + doc.body.appendChild(a); + if (baseUrl) + base.href = baseUrl; + a.href = url; + return a.href; +} +function getDocument(target) { + return (target && isElementNode(target) ? target?.ownerDocument : target) ?? window.document; +} +const XMLNS = "http://www.w3.org/2000/svg"; +function createSvg(width, height, ownerDocument) { + const svg = getDocument(ownerDocument).createElementNS(XMLNS, "svg"); + svg.setAttributeNS(null, "width", width.toString()); + svg.setAttributeNS(null, "height", height.toString()); + svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`); + return svg; +} +function svgToDataUrl(svg, removeControlCharacter) { + let xhtml = new XMLSerializer().serializeToString(svg); + if (removeControlCharacter) { + xhtml = xhtml.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu, ""); + } + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xhtml)}`; +} +async function canvasToBlob(canvas, type = "image/png", quality = 1) { + try { + return await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } else { + reject(new Error("Blob is null")); + } + }, type, quality); + }); + } catch (error) { + if (SUPPORT_ATOB) { + return dataUrlToBlob(canvas.toDataURL(type, quality)); + } + throw error; + } +} +function dataUrlToBlob(dataUrl) { + const [header, base64] = dataUrl.split(","); + const type = header.match(/data:(.+);/)?.[1] ?? void 0; + const decoded = window.atob(base64); + const length = decoded.length; + const buffer = new Uint8Array(length); + for (let i = 0; i < length; i += 1) { + buffer[i] = decoded.charCodeAt(i); + } + return new Blob([buffer], { type }); +} +function readBlob(blob, type) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.onabort = () => reject(new Error(`Failed read blob to ${type}`)); + if (type === "dataUrl") { + reader.readAsDataURL(blob); + } else if (type === "arrayBuffer") { + reader.readAsArrayBuffer(blob); + } + }); +} +const blobToDataUrl = (blob) => readBlob(blob, "dataUrl"); +const blobToArrayBuffer = (blob) => readBlob(blob, "arrayBuffer"); +function createImage(url, ownerDocument) { + const img = getDocument(ownerDocument).createElement("img"); + img.decoding = "sync"; + img.loading = "eager"; + img.src = url; + return img; +} +function loadMedia(media, options) { + return new Promise((resolve) => { + const { timeout, ownerDocument, onError: userOnError, onWarn } = options ?? {}; + const node = typeof media === "string" ? createImage(media, getDocument(ownerDocument)) : media; + let timer = null; + let removeEventListeners = null; + function onResolve() { + resolve(node); + timer && clearTimeout(timer); + removeEventListeners?.(); + } + if (timeout) { + timer = setTimeout(onResolve, timeout); + } + if (isVideoElement(node)) { + const currentSrc = node.currentSrc || node.src; + if (!currentSrc) { + if (node.poster) { + return loadMedia(node.poster, options).then(resolve); + } + return onResolve(); + } + if (node.readyState >= 2) { + return onResolve(); + } + const onLoadeddata = onResolve; + const onError = (error) => { + onWarn?.( + "Failed video load", + currentSrc, + error + ); + userOnError?.(error); + onResolve(); + }; + removeEventListeners = () => { + node.removeEventListener("loadeddata", onLoadeddata); + node.removeEventListener("error", onError); + }; + node.addEventListener("loadeddata", onLoadeddata, { once: true }); + node.addEventListener("error", onError, { once: true }); + } else { + const currentSrc = isSVGImageElementNode(node) ? node.href.baseVal : node.currentSrc || node.src; + if (!currentSrc) { + return onResolve(); + } + const onLoad = async () => { + if (isImageElement(node) && "decode" in node) { + try { + await node.decode(); + } catch (error) { + onWarn?.( + "Failed to decode image, trying to render anyway", + node.dataset.originalSrc || currentSrc, + error + ); + } + } + onResolve(); + }; + const onError = (error) => { + onWarn?.( + "Failed image load", + node.dataset.originalSrc || currentSrc, + error + ); + onResolve(); + }; + if (isImageElement(node) && node.complete) { + return onLoad(); + } + removeEventListeners = () => { + node.removeEventListener("load", onLoad); + node.removeEventListener("error", onError); + }; + node.addEventListener("load", onLoad, { once: true }); + node.addEventListener("error", onError, { once: true }); + } + }); +} +async function waitUntilLoad(node, options) { + if (isHTMLElementNode(node)) { + if (isImageElement(node) || isVideoElement(node)) { + await loadMedia(node, options); + } else { + await Promise.all( + ["img", "video"].flatMap((selectors) => { + return Array.from(node.querySelectorAll(selectors)).map((el) => loadMedia(el, options)); + }) + ); + } + } +} +const uuid = /* @__PURE__ */ function uuid2() { + let counter = 0; + const random = () => `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4); + return () => { + counter += 1; + return `u${random()}${counter}`; + }; +}(); +function splitFontFamily(fontFamily) { + return fontFamily?.split(",").map((val) => val.trim().replace(/"|'/g, "").toLowerCase()).filter(Boolean); +} + +let uid = 0; +function createLogger(debug) { + const prefix = `${PREFIX}[#${uid}]`; + uid++; + return { + // eslint-disable-next-line no-console + time: (label) => debug && console.time(`${prefix} ${label}`), + // eslint-disable-next-line no-console + timeEnd: (label) => debug && console.timeEnd(`${prefix} ${label}`), + warn: (...args) => debug && consoleWarn(...args) + }; +} + +function getDefaultRequestInit(bypassingCache) { + return { + cache: bypassingCache ? "no-cache" : "force-cache" + }; +} + +async function orCreateContext(node, options) { + return isContext(node) ? node : createContext(node, { ...options, autoDestruct: true }); +} +async function createContext(node, options) { + const { scale = 1, workerUrl, workerNumber = 1 } = options || {}; + const debug = Boolean(options?.debug); + const features = options?.features ?? true; + const ownerDocument = node.ownerDocument ?? (IN_BROWSER ? window.document : void 0); + const ownerWindow = node.ownerDocument?.defaultView ?? (IN_BROWSER ? window : void 0); + const requests = /* @__PURE__ */ new Map(); + const context = { + // Options + width: 0, + height: 0, + quality: 1, + type: "image/png", + scale, + backgroundColor: null, + style: null, + filter: null, + maximumCanvasSize: 0, + timeout: 3e4, + progress: null, + debug, + fetch: { + requestInit: getDefaultRequestInit(options?.fetch?.bypassingCache), + placeholderImage: "data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", + bypassingCache: false, + ...options?.fetch + }, + fetchFn: null, + font: {}, + drawImageInterval: 100, + workerUrl: null, + workerNumber, + onCloneEachNode: null, + onCloneNode: null, + onEmbedNode: null, + onCreateForeignObjectSvg: null, + includeStyleProperties: null, + autoDestruct: false, + ...options, + // InternalContext + __CONTEXT__: true, + log: createLogger(debug), + node, + ownerDocument, + ownerWindow, + dpi: scale === 1 ? null : 96 * scale, + svgStyleElement: createStyleElement(ownerDocument), + svgDefsElement: ownerDocument?.createElementNS(XMLNS, "defs"), + svgStyles: /* @__PURE__ */ new Map(), + defaultComputedStyles: /* @__PURE__ */ new Map(), + workers: [ + ...Array.from({ + length: SUPPORT_WEB_WORKER && workerUrl && workerNumber ? workerNumber : 0 + }) + ].map(() => { + try { + const worker = new Worker(workerUrl); + worker.onmessage = async (event) => { + const { url, result } = event.data; + if (result) { + requests.get(url)?.resolve?.(result); + } else { + requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`)); + } + }; + worker.onmessageerror = (event) => { + const { url } = event.data; + requests.get(url)?.reject?.(new Error(`Error receiving message from worker: ${url}`)); + }; + return worker; + } catch (error) { + context.log.warn("Failed to new Worker", error); + return null; + } + }).filter(Boolean), + fontFamilies: /* @__PURE__ */ new Map(), + fontCssTexts: /* @__PURE__ */ new Map(), + acceptOfImage: `${[ + supportWebp(ownerDocument) && "image/webp", + "image/svg+xml", + "image/*", + "*/*" + ].filter(Boolean).join(",")};q=0.8`, + requests, + drawImageCount: 0, + tasks: [], + features, + isEnable: (key) => { + if (key === "restoreScrollPosition") { + return typeof features === "boolean" ? false : features[key] ?? false; + } + if (typeof features === "boolean") { + return features; + } + return features[key] ?? true; + }, + shadowRoots: [] + }; + context.log.time("wait until load"); + await waitUntilLoad(node, { timeout: context.timeout, onWarn: context.log.warn }); + context.log.timeEnd("wait until load"); + const { width, height } = resolveBoundingBox(node, context); + context.width = width; + context.height = height; + return context; +} +function createStyleElement(ownerDocument) { + if (!ownerDocument) + return void 0; + const style = ownerDocument.createElement("style"); + const cssText = style.ownerDocument.createTextNode(` +.______background-clip--text { + background-clip: text; + -webkit-background-clip: text; +} +`); + style.appendChild(cssText); + return style; +} +function resolveBoundingBox(node, context) { + let { width, height } = context; + if (isElementNode(node) && (!width || !height)) { + const box = node.getBoundingClientRect(); + width = width || box.width || Number(node.getAttribute("width")) || 0; + height = height || box.height || Number(node.getAttribute("height")) || 0; + } + return { width, height }; +} + +async function imageToCanvas(image, context) { + const { + log, + timeout, + drawImageCount, + drawImageInterval + } = context; + log.time("image to canvas"); + const loaded = await loadMedia(image, { timeout, onWarn: context.log.warn }); + const { canvas, context2d } = createCanvas(image.ownerDocument, context); + const drawImage = () => { + try { + context2d?.drawImage(loaded, 0, 0, canvas.width, canvas.height); + } catch (error) { + context.log.warn("Failed to drawImage", error); + } + }; + drawImage(); + if (context.isEnable("fixSvgXmlDecode")) { + for (let i = 0; i < drawImageCount; i++) { + await new Promise((resolve) => { + setTimeout(() => { + context2d?.clearRect(0, 0, canvas.width, canvas.height); + drawImage(); + resolve(); + }, i + drawImageInterval); + }); + } + } + context.drawImageCount = 0; + log.timeEnd("image to canvas"); + return canvas; +} +function createCanvas(ownerDocument, context) { + const { width, height, scale, backgroundColor, maximumCanvasSize: max } = context; + const canvas = ownerDocument.createElement("canvas"); + canvas.width = Math.floor(width * scale); + canvas.height = Math.floor(height * scale); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + if (max) { + if (canvas.width > max || canvas.height > max) { + if (canvas.width > max && canvas.height > max) { + if (canvas.width > canvas.height) { + canvas.height *= max / canvas.width; + canvas.width = max; + } else { + canvas.width *= max / canvas.height; + canvas.height = max; + } + } else if (canvas.width > max) { + canvas.height *= max / canvas.width; + canvas.width = max; + } else { + canvas.width *= max / canvas.height; + canvas.height = max; + } + } + } + const context2d = canvas.getContext("2d"); + if (context2d && backgroundColor) { + context2d.fillStyle = backgroundColor; + context2d.fillRect(0, 0, canvas.width, canvas.height); + } + return { canvas, context2d }; +} + +function cloneCanvas(canvas, context) { + if (canvas.ownerDocument) { + try { + const dataURL = canvas.toDataURL(); + if (dataURL !== "data:,") { + return createImage(dataURL, canvas.ownerDocument); + } + } catch (error) { + context.log.warn("Failed to clone canvas", error); + } + } + const cloned = canvas.cloneNode(false); + const ctx = canvas.getContext("2d"); + const clonedCtx = cloned.getContext("2d"); + try { + if (ctx && clonedCtx) { + clonedCtx.putImageData( + ctx.getImageData(0, 0, canvas.width, canvas.height), + 0, + 0 + ); + } + return cloned; + } catch (error) { + context.log.warn("Failed to clone canvas", error); + } + return cloned; +} + +function cloneIframe(iframe, context) { + try { + if (iframe?.contentDocument?.body) { + return cloneNode(iframe.contentDocument.body, context); + } + } catch (error) { + context.log.warn("Failed to clone iframe", error); + } + return iframe.cloneNode(false); +} + +function cloneImage(image) { + const cloned = image.cloneNode(false); + if (image.currentSrc && image.currentSrc !== image.src) { + cloned.src = image.currentSrc; + cloned.srcset = ""; + } + if (cloned.loading === "lazy") { + cloned.loading = "eager"; + } + return cloned; +} + +async function cloneVideo(video, context) { + if (video.ownerDocument && !video.currentSrc && video.poster) { + return createImage(video.poster, video.ownerDocument); + } + const cloned = video.cloneNode(false); + cloned.crossOrigin = "anonymous"; + if (video.currentSrc && video.currentSrc !== video.src) { + cloned.src = video.currentSrc; + } + const ownerDocument = cloned.ownerDocument; + if (ownerDocument) { + let canPlay = true; + await loadMedia(cloned, { onError: () => canPlay = false, onWarn: context.log.warn }); + if (!canPlay) { + if (video.poster) { + return createImage(video.poster, video.ownerDocument); + } + return cloned; + } + cloned.currentTime = video.currentTime; + await new Promise((resolve) => { + cloned.addEventListener("seeked", resolve, { once: true }); + }); + const canvas = ownerDocument.createElement("canvas"); + canvas.width = video.offsetWidth; + canvas.height = video.offsetHeight; + try { + const ctx = canvas.getContext("2d"); + if (ctx) + ctx.drawImage(cloned, 0, 0, canvas.width, canvas.height); + } catch (error) { + context.log.warn("Failed to clone video", error); + if (video.poster) { + return createImage(video.poster, video.ownerDocument); + } + return cloned; + } + return cloneCanvas(canvas, context); + } + return cloned; +} + +function cloneElement(node, context) { + if (isCanvasElement(node)) { + return cloneCanvas(node, context); + } + if (isIFrameElement(node)) { + return cloneIframe(node, context); + } + if (isImageElement(node)) { + return cloneImage(node); + } + if (isVideoElement(node)) { + return cloneVideo(node, context); + } + return node.cloneNode(false); +} + +function getSandBox(context) { + let sandbox = context.sandbox; + if (!sandbox) { + const { ownerDocument } = context; + try { + if (ownerDocument) { + sandbox = ownerDocument.createElement("iframe"); + sandbox.id = `__SANDBOX__${uuid()}`; + sandbox.width = "0"; + sandbox.height = "0"; + sandbox.style.visibility = "hidden"; + sandbox.style.position = "fixed"; + ownerDocument.body.appendChild(sandbox); + sandbox.srcdoc = ''; + context.sandbox = sandbox; + } + } catch (error) { + context.log.warn("Failed to getSandBox", error); + } + } + return sandbox; +} + +const ignoredStyles = [ + "width", + "height", + "-webkit-text-fill-color" +]; +const includedAttributes = [ + "stroke", + "fill" +]; +function getDefaultStyle(node, pseudoElement, context) { + const { defaultComputedStyles } = context; + const nodeName = node.nodeName.toLowerCase(); + const isSvgNode = isSVGElementNode(node) && nodeName !== "svg"; + const attributes = isSvgNode ? includedAttributes.map((name) => [name, node.getAttribute(name)]).filter(([, value]) => value !== null) : []; + const key = [ + isSvgNode && "svg", + nodeName, + attributes.map((name, value) => `${name}=${value}`).join(","), + pseudoElement + ].filter(Boolean).join(":"); + if (defaultComputedStyles.has(key)) + return defaultComputedStyles.get(key); + const sandbox = getSandBox(context); + const sandboxWindow = sandbox?.contentWindow; + if (!sandboxWindow) + return /* @__PURE__ */ new Map(); + const sandboxDocument = sandboxWindow?.document; + let root; + let el; + if (isSvgNode) { + root = sandboxDocument.createElementNS(XMLNS, "svg"); + el = root.ownerDocument.createElementNS(root.namespaceURI, nodeName); + attributes.forEach(([name, value]) => { + el.setAttributeNS(null, name, value); + }); + root.appendChild(el); + } else { + root = el = sandboxDocument.createElement(nodeName); + } + el.textContent = " "; + sandboxDocument.body.appendChild(root); + const computedStyle = sandboxWindow.getComputedStyle(el, pseudoElement); + const styles = /* @__PURE__ */ new Map(); + for (let len = computedStyle.length, i = 0; i < len; i++) { + const name = computedStyle.item(i); + if (ignoredStyles.includes(name)) + continue; + styles.set(name, computedStyle.getPropertyValue(name)); + } + sandboxDocument.body.removeChild(root); + defaultComputedStyles.set(key, styles); + return styles; +} + +function getDiffStyle(style, defaultStyle, includeStyleProperties) { + const diffStyle = /* @__PURE__ */ new Map(); + const prefixs = []; + const prefixTree = /* @__PURE__ */ new Map(); + if (includeStyleProperties) { + for (const name of includeStyleProperties) { + applyTo(name); + } + } else { + for (let len = style.length, i = 0; i < len; i++) { + const name = style.item(i); + applyTo(name); + } + } + for (let len = prefixs.length, i = 0; i < len; i++) { + prefixTree.get(prefixs[i])?.forEach((value, name) => diffStyle.set(name, value)); + } + function applyTo(name) { + const value = style.getPropertyValue(name); + const priority = style.getPropertyPriority(name); + const subIndex = name.lastIndexOf("-"); + const prefix = subIndex > -1 ? name.substring(0, subIndex) : void 0; + if (prefix) { + let map = prefixTree.get(prefix); + if (!map) { + map = /* @__PURE__ */ new Map(); + prefixTree.set(prefix, map); + } + map.set(name, [value, priority]); + } + if (defaultStyle.get(name) === value && !priority) + return; + if (prefix) { + prefixs.push(prefix); + } else { + diffStyle.set(name, [value, priority]); + } + } + return diffStyle; +} + +function copyCssStyles(node, cloned, isRoot, context) { + const { ownerWindow, includeStyleProperties, currentParentNodeStyle } = context; + const clonedStyle = cloned.style; + const computedStyle = ownerWindow.getComputedStyle(node); + const defaultStyle = getDefaultStyle(node, null, context); + currentParentNodeStyle?.forEach((_, key) => { + defaultStyle.delete(key); + }); + const style = getDiffStyle(computedStyle, defaultStyle, includeStyleProperties); + style.delete("transition-property"); + style.delete("all"); + style.delete("d"); + style.delete("content"); + if (isRoot) { + style.delete("margin-top"); + style.delete("margin-right"); + style.delete("margin-bottom"); + style.delete("margin-left"); + style.delete("margin-block-start"); + style.delete("margin-block-end"); + style.delete("margin-inline-start"); + style.delete("margin-inline-end"); + style.set("box-sizing", ["border-box", ""]); + } + if (style.get("background-clip")?.[0] === "text") { + cloned.classList.add("______background-clip--text"); + } + if (IN_CHROME) { + if (!style.has("font-kerning")) + style.set("font-kerning", ["normal", ""]); + if ((style.get("overflow-x")?.[0] === "hidden" || style.get("overflow-y")?.[0] === "hidden") && style.get("text-overflow")?.[0] === "ellipsis" && node.scrollWidth === node.clientWidth) { + style.set("text-overflow", ["clip", ""]); + } + } + for (let len = clonedStyle.length, i = 0; i < len; i++) { + clonedStyle.removeProperty(clonedStyle.item(i)); + } + style.forEach(([value, priority], name) => { + clonedStyle.setProperty(name, value, priority); + }); + return style; +} + +function copyInputValue(node, cloned) { + if (isTextareaElement(node) || isInputElement(node) || isSelectElement(node)) { + cloned.setAttribute("value", node.value); + } +} + +const pseudoClasses = [ + ":before", + ":after" + // ':placeholder', TODO +]; +const scrollbarPseudoClasses = [ + ":-webkit-scrollbar", + ":-webkit-scrollbar-button", + // ':-webkit-scrollbar:horizontal', TODO + ":-webkit-scrollbar-thumb", + ":-webkit-scrollbar-track", + ":-webkit-scrollbar-track-piece", + // ':-webkit-scrollbar:vertical', TODO + ":-webkit-scrollbar-corner", + ":-webkit-resizer" +]; +function copyPseudoClass(node, cloned, copyScrollbar, context, addWordToFontFamilies) { + const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context; + if (!svgStyleElement || !ownerWindow) + return; + function copyBy(pseudoClass) { + const computedStyle = ownerWindow.getComputedStyle(node, pseudoClass); + let content = computedStyle.getPropertyValue("content"); + if (!content || content === "none") + return; + addWordToFontFamilies?.(content); + content = content.replace(/(')|(")|(counter\(.+\))/g, ""); + const klasses = [uuid()]; + const defaultStyle = getDefaultStyle(node, pseudoClass, context); + currentNodeStyle?.forEach((_, key) => { + defaultStyle.delete(key); + }); + const style = getDiffStyle(computedStyle, defaultStyle, context.includeStyleProperties); + style.delete("content"); + style.delete("-webkit-locale"); + if (style.get("background-clip")?.[0] === "text") { + cloned.classList.add("______background-clip--text"); + } + const cloneStyle = [ + `content: '${content}';` + ]; + style.forEach(([value, priority], name) => { + cloneStyle.push(`${name}: ${value}${priority ? " !important" : ""};`); + }); + if (cloneStyle.length === 1) + return; + try { + cloned.className = [cloned.className, ...klasses].join(" "); + } catch (err) { + context.log.warn("Failed to copyPseudoClass", err); + return; + } + const cssText = cloneStyle.join("\n "); + let allClasses = svgStyles.get(cssText); + if (!allClasses) { + allClasses = []; + svgStyles.set(cssText, allClasses); + } + allClasses.push(`.${klasses[0]}:${pseudoClass}`); + } + pseudoClasses.forEach(copyBy); + if (copyScrollbar) + scrollbarPseudoClasses.forEach(copyBy); +} + +const excludeParentNodes = /* @__PURE__ */ new Set([ + "symbol" + // test/fixtures/svg.symbol.html +]); +async function appendChildNode(node, cloned, child, context, addWordToFontFamilies) { + if (isElementNode(child) && (isStyleElement(child) || isScriptElement(child))) + return; + if (context.filter && !context.filter(child)) + return; + if (excludeParentNodes.has(cloned.nodeName) || excludeParentNodes.has(child.nodeName)) { + context.currentParentNodeStyle = void 0; + } else { + context.currentParentNodeStyle = context.currentNodeStyle; + } + const childCloned = await cloneNode(child, context, false, addWordToFontFamilies); + if (context.isEnable("restoreScrollPosition")) { + restoreScrollPosition(node, childCloned); + } + cloned.appendChild(childCloned); +} +async function cloneChildNodes(node, cloned, context, addWordToFontFamilies) { + let firstChild = node.firstChild; + if (isElementNode(node)) { + if (node.shadowRoot) { + firstChild = node.shadowRoot?.firstChild; + context.shadowRoots.push(node.shadowRoot); + } + } + for (let child = firstChild; child; child = child.nextSibling) { + if (isCommentNode(child)) + continue; + if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === "function") { + const nodes = child.assignedNodes(); + for (let i = 0; i < nodes.length; i++) { + await appendChildNode(node, cloned, nodes[i], context, addWordToFontFamilies); + } + } else { + await appendChildNode(node, cloned, child, context, addWordToFontFamilies); + } + } +} +function restoreScrollPosition(node, chlidCloned) { + if (!isHTMLElementNode(node) || !isHTMLElementNode(chlidCloned)) + return; + const { scrollTop, scrollLeft } = node; + if (!scrollTop && !scrollLeft) { + return; + } + const { transform } = chlidCloned.style; + const matrix = new DOMMatrix(transform); + const { a, b, c, d } = matrix; + matrix.a = 1; + matrix.b = 0; + matrix.c = 0; + matrix.d = 1; + matrix.translateSelf(-scrollLeft, -scrollTop); + matrix.a = a; + matrix.b = b; + matrix.c = c; + matrix.d = d; + chlidCloned.style.transform = matrix.toString(); +} +function applyCssStyleWithOptions(cloned, context) { + const { backgroundColor, width, height, style: styles } = context; + const clonedStyle = cloned.style; + if (backgroundColor) + clonedStyle.setProperty("background-color", backgroundColor, "important"); + if (width) + clonedStyle.setProperty("width", `${width}px`, "important"); + if (height) + clonedStyle.setProperty("height", `${height}px`, "important"); + if (styles) { + for (const name in styles) clonedStyle[name] = styles[name]; + } +} +const NORMAL_ATTRIBUTE_RE = /^[\w-:]+$/; +async function cloneNode(node, context, isRoot = false, addWordToFontFamilies) { + const { ownerDocument, ownerWindow, fontFamilies, onCloneEachNode } = context; + if (ownerDocument && isTextNode(node)) { + if (addWordToFontFamilies && /\S/.test(node.data)) { + addWordToFontFamilies(node.data); + } + return ownerDocument.createTextNode(node.data); + } + if (ownerDocument && ownerWindow && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) { + const cloned2 = await cloneElement(node, context); + if (context.isEnable("removeAbnormalAttributes")) { + const names = cloned2.getAttributeNames(); + for (let len = names.length, i = 0; i < len; i++) { + const name = names[i]; + if (!NORMAL_ATTRIBUTE_RE.test(name)) { + cloned2.removeAttribute(name); + } + } + } + const style = context.currentNodeStyle = copyCssStyles(node, cloned2, isRoot, context); + if (isRoot) + applyCssStyleWithOptions(cloned2, context); + let copyScrollbar = false; + if (context.isEnable("copyScrollbar")) { + const overflow = [ + style.get("overflow-x")?.[0], + style.get("overflow-y")?.[0] + ]; + copyScrollbar = overflow.includes("scroll") || (overflow.includes("auto") || overflow.includes("overlay")) && (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth); + } + const textTransform = style.get("text-transform")?.[0]; + const families = splitFontFamily(style.get("font-family")?.[0]); + const addWordToFontFamilies2 = families ? (word) => { + if (textTransform === "uppercase") { + word = word.toUpperCase(); + } else if (textTransform === "lowercase") { + word = word.toLowerCase(); + } else if (textTransform === "capitalize") { + word = word[0].toUpperCase() + word.substring(1); + } + families.forEach((family) => { + let fontFamily = fontFamilies.get(family); + if (!fontFamily) { + fontFamilies.set(family, fontFamily = /* @__PURE__ */ new Set()); + } + word.split("").forEach((text) => fontFamily.add(text)); + }); + } : void 0; + copyPseudoClass( + node, + cloned2, + copyScrollbar, + context, + addWordToFontFamilies2 + ); + copyInputValue(node, cloned2); + if (!isVideoElement(node)) { + await cloneChildNodes( + node, + cloned2, + context, + addWordToFontFamilies2 + ); + } + await onCloneEachNode?.(cloned2); + return cloned2; + } + const cloned = node.cloneNode(false); + await cloneChildNodes(node, cloned, context); + await onCloneEachNode?.(cloned); + return cloned; +} + +function destroyContext(context) { + context.ownerDocument = void 0; + context.ownerWindow = void 0; + context.svgStyleElement = void 0; + context.svgDefsElement = void 0; + context.svgStyles.clear(); + context.defaultComputedStyles.clear(); + if (context.sandbox) { + try { + context.sandbox.remove(); + } catch (err) { + context.log.warn("Failed to destroyContext", err); + } + context.sandbox = void 0; + } + context.workers = []; + context.fontFamilies.clear(); + context.fontCssTexts.clear(); + context.requests.clear(); + context.tasks = []; + context.shadowRoots = []; +} + +function baseFetch(options) { + const { url, timeout, responseType, ...requestInit } = options; + const controller = new AbortController(); + const timer = timeout ? setTimeout(() => controller.abort(), timeout) : void 0; + return fetch(url, { signal: controller.signal, ...requestInit }).then((response) => { + if (!response.ok) { + throw new Error("Failed fetch, not 2xx response", { cause: response }); + } + switch (responseType) { + case "arrayBuffer": + return response.arrayBuffer(); + case "dataUrl": + return response.blob().then(blobToDataUrl); + case "text": + default: + return response.text(); + } + }).finally(() => clearTimeout(timer)); +} +function contextFetch(context, options) { + const { url: rawUrl, requestType = "text", responseType = "text", imageDom } = options; + let url = rawUrl; + const { + timeout, + acceptOfImage, + requests, + fetchFn, + fetch: { + requestInit, + bypassingCache, + placeholderImage + }, + font, + workers, + fontFamilies + } = context; + if (requestType === "image" && (IN_SAFARI || IN_FIREFOX)) { + context.drawImageCount++; + } + let request = requests.get(rawUrl); + if (!request) { + if (bypassingCache) { + if (bypassingCache instanceof RegExp && bypassingCache.test(url)) { + url += (/\?/.test(url) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime(); + } + } + const canFontMinify = requestType.startsWith("font") && font && font.minify; + const fontTexts = /* @__PURE__ */ new Set(); + if (canFontMinify) { + const families = requestType.split(";")[1].split(","); + families.forEach((family) => { + if (!fontFamilies.has(family)) + return; + fontFamilies.get(family).forEach((text) => fontTexts.add(text)); + }); + } + const needFontMinify = canFontMinify && fontTexts.size; + const baseFetchOptions = { + url, + timeout, + responseType: needFontMinify ? "arrayBuffer" : responseType, + headers: requestType === "image" ? { accept: acceptOfImage } : void 0, + ...requestInit + }; + request = { + type: requestType, + resolve: void 0, + reject: void 0, + response: null + }; + request.response = (async () => { + if (fetchFn && requestType === "image") { + const result = await fetchFn(rawUrl); + if (result) + return result; + } + if (!IN_SAFARI && rawUrl.startsWith("http") && workers.length) { + return new Promise((resolve, reject) => { + const worker = workers[requests.size & workers.length - 1]; + worker.postMessage({ rawUrl, ...baseFetchOptions }); + request.resolve = resolve; + request.reject = reject; + }); + } + return baseFetch(baseFetchOptions); + })().catch((error) => { + requests.delete(rawUrl); + if (requestType === "image" && placeholderImage) { + context.log.warn("Failed to fetch image base64, trying to use placeholder image", url); + return typeof placeholderImage === "string" ? placeholderImage : placeholderImage(imageDom); + } + throw error; + }); + requests.set(rawUrl, request); + } + return request.response; +} + +async function replaceCssUrlToDataUrl(cssText, baseUrl, context, isImage) { + if (!hasCssUrl(cssText)) + return cssText; + for (const [rawUrl, url] of parseCssUrls(cssText, baseUrl)) { + try { + const dataUrl = await contextFetch( + context, + { + url, + requestType: isImage ? "image" : "text", + responseType: "dataUrl" + } + ); + cssText = cssText.replace(toRE(rawUrl), `$1${dataUrl}$3`); + } catch (error) { + context.log.warn("Failed to fetch css data url", rawUrl, error); + } + } + return cssText; +} +function hasCssUrl(cssText) { + return /url\((['"]?)([^'"]+?)\1\)/.test(cssText); +} +const URL_RE = /url\((['"]?)([^'"]+?)\1\)/g; +function parseCssUrls(cssText, baseUrl) { + const result = []; + cssText.replace(URL_RE, (raw, quotation, url) => { + result.push([url, resolveUrl(url, baseUrl)]); + return raw; + }); + return result.filter(([url]) => !isDataUrl(url)); +} +function toRE(url) { + const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g"); +} + +const properties = [ + "background-image", + "border-image-source", + "-webkit-border-image", + "-webkit-mask-image", + "list-style-image" +]; +function embedCssStyleImage(style, context) { + return properties.map((property) => { + const value = style.getPropertyValue(property); + if (!value || value === "none") { + return null; + } + if (IN_SAFARI || IN_FIREFOX) { + context.drawImageCount++; + } + return replaceCssUrlToDataUrl(value, null, context, true).then((newValue) => { + if (!newValue || value === newValue) + return; + style.setProperty( + property, + newValue, + style.getPropertyPriority(property) + ); + }); + }).filter(Boolean); +} + +function embedImageElement(cloned, context) { + if (isImageElement(cloned)) { + const originalSrc = cloned.currentSrc || cloned.src; + if (!isDataUrl(originalSrc)) { + return [ + contextFetch(context, { + url: originalSrc, + imageDom: cloned, + requestType: "image", + responseType: "dataUrl" + }).then((url) => { + if (!url) + return; + cloned.srcset = ""; + cloned.dataset.originalSrc = originalSrc; + cloned.src = url || ""; + }) + ]; + } + if (IN_SAFARI || IN_FIREFOX) { + context.drawImageCount++; + } + } else if (isSVGElementNode(cloned) && !isDataUrl(cloned.href.baseVal)) { + const originalSrc = cloned.href.baseVal; + return [ + contextFetch(context, { + url: originalSrc, + imageDom: cloned, + requestType: "image", + responseType: "dataUrl" + }).then((url) => { + if (!url) + return; + cloned.dataset.originalSrc = originalSrc; + cloned.href.baseVal = url || ""; + }) + ]; + } + return []; +} + +function embedSvgUse(cloned, context) { + const { ownerDocument, svgDefsElement } = context; + const href = cloned.getAttribute("href") ?? cloned.getAttribute("xlink:href"); + if (!href) + return []; + const [svgUrl, id] = href.split("#"); + if (id) { + const query = `#${id}`; + const definition = context.shadowRoots.reduce( + (res, root) => { + return res ?? root.querySelector(`svg ${query}`); + }, + ownerDocument?.querySelector(`svg ${query}`) + ); + if (svgUrl) { + cloned.setAttribute("href", query); + } + if (svgDefsElement?.querySelector(query)) + return []; + if (definition) { + svgDefsElement?.appendChild(definition.cloneNode(true)); + return []; + } else if (svgUrl) { + return [ + contextFetch(context, { + url: svgUrl, + responseType: "text" + }).then((svgData) => { + svgDefsElement?.insertAdjacentHTML("beforeend", svgData); + }) + ]; + } + } + return []; +} + +function embedNode(cloned, context) { + const { tasks } = context; + if (isElementNode(cloned)) { + if (isImageElement(cloned) || isSVGImageElementNode(cloned)) { + tasks.push(...embedImageElement(cloned, context)); + } + if (isSVGUseElementNode(cloned)) { + tasks.push(...embedSvgUse(cloned, context)); + } + } + if (isHTMLElementNode(cloned)) { + tasks.push(...embedCssStyleImage(cloned.style, context)); + } + cloned.childNodes.forEach((child) => { + embedNode(child, context); + }); +} + +async function embedWebFont(clone, context) { + const { + ownerDocument, + svgStyleElement, + fontFamilies, + fontCssTexts, + tasks, + font + } = context; + if (!ownerDocument || !svgStyleElement || !fontFamilies.size) { + return; + } + if (font && font.cssText) { + const cssText = filterPreferredFormat(font.cssText, context); + svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText} +`)); + } else { + const styleSheets = Array.from(ownerDocument.styleSheets).filter((styleSheet) => { + try { + return "cssRules" in styleSheet && Boolean(styleSheet.cssRules.length); + } catch (error) { + context.log.warn(`Error while reading CSS rules from ${styleSheet.href}`, error); + return false; + } + }); + await Promise.all( + styleSheets.flatMap((styleSheet) => { + return Array.from(styleSheet.cssRules).map(async (cssRule, index) => { + if (isCSSImportRule(cssRule)) { + let importIndex = index + 1; + const baseUrl = cssRule.href; + let cssText = ""; + try { + cssText = await contextFetch(context, { + url: baseUrl, + requestType: "text", + responseType: "text" + }); + } catch (error) { + context.log.warn(`Error fetch remote css import from ${baseUrl}`, error); + } + const replacedCssText = cssText.replace( + URL_RE, + (raw, quotation, url) => raw.replace(url, resolveUrl(url, baseUrl)) + ); + for (const rule of parseCss(replacedCssText)) { + try { + styleSheet.insertRule( + rule, + rule.startsWith("@import") ? importIndex += 1 : styleSheet.cssRules.length + ); + } catch (error) { + context.log.warn("Error inserting rule from remote css import", { rule, error }); + } + } + } + }); + }) + ); + const cssRules = styleSheets.flatMap((styleSheet) => Array.from(styleSheet.cssRules)); + cssRules.filter((cssRule) => isCssFontFaceRule(cssRule) && hasCssUrl(cssRule.style.getPropertyValue("src")) && splitFontFamily(cssRule.style.getPropertyValue("font-family"))?.some((val) => fontFamilies.has(val))).forEach((value) => { + const rule = value; + const cssText = fontCssTexts.get(rule.cssText); + if (cssText) { + svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText} +`)); + } else { + tasks.push( + replaceCssUrlToDataUrl( + rule.cssText, + rule.parentStyleSheet ? rule.parentStyleSheet.href : null, + context + ).then((cssText2) => { + cssText2 = filterPreferredFormat(cssText2, context); + fontCssTexts.set(rule.cssText, cssText2); + svgStyleElement.appendChild(ownerDocument.createTextNode(`${cssText2} +`)); + }) + ); + } + }); + } +} +const COMMENTS_RE = /(\/\*[\s\S]*?\*\/)/g; +const KEYFRAMES_RE = /((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi; +function parseCss(source) { + if (source == null) + return []; + const result = []; + let cssText = source.replace(COMMENTS_RE, ""); + while (true) { + const matches = KEYFRAMES_RE.exec(cssText); + if (!matches) + break; + result.push(matches[0]); + } + cssText = cssText.replace(KEYFRAMES_RE, ""); + const IMPORT_RE = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi; + const UNIFIED_RE = new RegExp( + // eslint-disable-next-line + "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})", + "gi" + ); + while (true) { + let matches = IMPORT_RE.exec(cssText); + if (!matches) { + matches = UNIFIED_RE.exec(cssText); + if (!matches) { + break; + } else { + IMPORT_RE.lastIndex = UNIFIED_RE.lastIndex; + } + } else { + UNIFIED_RE.lastIndex = IMPORT_RE.lastIndex; + } + result.push(matches[0]); + } + return result; +} +const URL_WITH_FORMAT_RE = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g; +const FONT_SRC_RE = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g; +function filterPreferredFormat(str, context) { + const { font } = context; + const preferredFormat = font ? font?.preferredFormat : void 0; + return preferredFormat ? str.replace(FONT_SRC_RE, (match) => { + while (true) { + const [src, , format] = URL_WITH_FORMAT_RE.exec(match) || []; + if (!format) + return ""; + if (format === preferredFormat) + return `src: ${src};`; + } + }) : str; +} + +async function domToForeignObjectSvg(node, options) { + const context = await orCreateContext(node, options); + if (isElementNode(context.node) && isSVGElementNode(context.node)) + return context.node; + const { + ownerDocument, + log, + tasks, + svgStyleElement, + svgDefsElement, + svgStyles, + font, + progress, + autoDestruct, + onCloneNode, + onEmbedNode, + onCreateForeignObjectSvg + } = context; + log.time("clone node"); + const clone = await cloneNode(context.node, context, true); + if (svgStyleElement && ownerDocument) { + let allCssText = ""; + svgStyles.forEach((klasses, cssText) => { + allCssText += `${klasses.join(",\n")} { + ${cssText} +} +`; + }); + svgStyleElement.appendChild(ownerDocument.createTextNode(allCssText)); + } + log.timeEnd("clone node"); + await onCloneNode?.(clone); + if (font !== false && isElementNode(clone)) { + log.time("embed web font"); + await embedWebFont(clone, context); + log.timeEnd("embed web font"); + } + log.time("embed node"); + embedNode(clone, context); + const count = tasks.length; + let current = 0; + const runTask = async () => { + while (true) { + const task = tasks.pop(); + if (!task) + break; + try { + await task; + } catch (error) { + context.log.warn("Failed to run task", error); + } + progress?.(++current, count); + } + }; + progress?.(current, count); + await Promise.all([...Array.from({ length: 4 })].map(runTask)); + log.timeEnd("embed node"); + await onEmbedNode?.(clone); + const svg = createForeignObjectSvg(clone, context); + svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]); + svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]); + autoDestruct && destroyContext(context); + await onCreateForeignObjectSvg?.(svg); + return svg; +} +function createForeignObjectSvg(clone, context) { + const { width, height } = context; + const svg = createSvg(width, height, clone.ownerDocument); + const foreignObject = svg.ownerDocument.createElementNS(svg.namespaceURI, "foreignObject"); + foreignObject.setAttributeNS(null, "x", "0%"); + foreignObject.setAttributeNS(null, "y", "0%"); + foreignObject.setAttributeNS(null, "width", "100%"); + foreignObject.setAttributeNS(null, "height", "100%"); + foreignObject.append(clone); + svg.appendChild(foreignObject); + return svg; +} + +async function domToCanvas(node, options) { + const context = await orCreateContext(node, options); + const svg = await domToForeignObjectSvg(context); + const dataUrl = svgToDataUrl(svg, context.isEnable("removeControlCharacter")); + if (!context.autoDestruct) { + context.svgStyleElement = createStyleElement(context.ownerDocument); + context.svgDefsElement = context.ownerDocument?.createElementNS(XMLNS, "defs"); + context.svgStyles.clear(); + } + const image = createImage(dataUrl, svg.ownerDocument); + return await imageToCanvas(image, context); +} + +async function domToBlob(node, options) { + const context = await orCreateContext(node, options); + const { log, type, quality, dpi } = context; + const canvas = await domToCanvas(context); + log.time("canvas to blob"); + const blob = await canvasToBlob(canvas, type, quality); + if (["image/png", "image/jpeg"].includes(type) && dpi) { + const arrayBuffer = await blobToArrayBuffer(blob.slice(0, 33)); + let uint8Array = new Uint8Array(arrayBuffer); + if (type === "image/png") { + uint8Array = changePngDpi(uint8Array, dpi); + } else if (type === "image/jpeg") { + uint8Array = changeJpegDpi(uint8Array, dpi); + } + log.timeEnd("canvas to blob"); + return new Blob([uint8Array, blob.slice(33)], { type }); + } + log.timeEnd("canvas to blob"); + return blob; +} + +async function domToDataUrl(node, options) { + const context = await orCreateContext(node, options); + const { log, quality, type, dpi } = context; + const canvas = await domToCanvas(context); + log.time("canvas to data url"); + let dataUrl = canvas.toDataURL(type, quality); + if (["image/png", "image/jpeg"].includes(type) && dpi && SUPPORT_ATOB && SUPPORT_BTOA) { + const [format, body] = dataUrl.split(","); + let headerLength = 0; + let overwritepHYs = false; + if (type === "image/png") { + const b64Index = detectPhysChunkFromDataUrl(body); + if (b64Index >= 0) { + headerLength = Math.ceil((b64Index + 28) / 3) * 4; + overwritepHYs = true; + } else { + headerLength = 33 / 3 * 4; + } + } else if (type === "image/jpeg") { + headerLength = 18 / 3 * 4; + } + const stringHeader = body.substring(0, headerLength); + const restOfData = body.substring(headerLength); + const headerBytes = window.atob(stringHeader); + const uint8Array = new Uint8Array(headerBytes.length); + for (let i = 0; i < uint8Array.length; i++) { + uint8Array[i] = headerBytes.charCodeAt(i); + } + const finalArray = type === "image/png" ? changePngDpi(uint8Array, dpi, overwritepHYs) : changeJpegDpi(uint8Array, dpi); + const base64Header = window.btoa(String.fromCharCode(...finalArray)); + dataUrl = [format, ",", base64Header, restOfData].join(""); + } + log.timeEnd("canvas to data url"); + return dataUrl; +} + +async function domToSvg(node, options) { + const context = await orCreateContext(node, options); + const { width, height, ownerDocument } = context; + const dataUrl = await domToDataUrl(context); + const svg = createSvg(width, height, ownerDocument); + const svgImage = svg.ownerDocument.createElementNS(svg.namespaceURI, "image"); + svgImage.setAttributeNS(null, "href", dataUrl); + svgImage.setAttributeNS(null, "height", "100%"); + svgImage.setAttributeNS(null, "width", "100%"); + svg.appendChild(svgImage); + return svgToDataUrl(svg, context.isEnable("removeControlCharacter")); +} + +async function domToImage(node, options) { + const context = await orCreateContext(node, options); + const { ownerDocument, width, height, scale, type } = context; + const url = type === "image/svg+xml" ? await domToSvg(context) : await domToDataUrl(context); + const image = createImage(url, ownerDocument); + image.width = Math.floor(width * scale); + image.height = Math.floor(height * scale); + image.style.width = `${width}px`; + image.style.height = `${height}px`; + return image; +} + +async function domToJpeg(node, options) { + return domToDataUrl( + await orCreateContext(node, { ...options, type: "image/jpeg" }) + ); +} + +async function domToPixel(node, options) { + const context = await orCreateContext(node, options); + const canvas = await domToCanvas(context); + return canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data; +} + +async function domToPng(node, options) { + return domToDataUrl( + await orCreateContext(node, { ...options, type: "image/png" }) + ); +} + +async function domToWebp(node, options) { + return domToDataUrl( + await orCreateContext(node, { ...options, type: "image/webp" }) + ); +} + +export { createContext, destroyContext, domToBlob, domToCanvas, domToDataUrl, domToForeignObjectSvg, domToImage, domToJpeg, domToPixel, domToPng, domToSvg, domToWebp, loadMedia, waitUntilLoad }; diff --git a/websites/default/packages/modern-screenshot/wrapper.js b/websites/default/packages/modern-screenshot/wrapper.js new file mode 100644 index 000000000..60e696307 --- /dev/null +++ b/websites/default/packages/modern-screenshot/wrapper.js @@ -0,0 +1,13 @@ +import { domToBlob } from "./4.6.6/dist/index.mjs"; + +/** + * @param {Element} element + */ +export async function screenshot(element) { + const blob = await domToBlob(element, { + scale: 2, + }); + const url = URL.createObjectURL(blob); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 100); +} diff --git a/websites/default/packages/unpkg.sh b/websites/default/packages/unpkg.sh index 8ec1f1387..3670ccf49 100755 --- a/websites/default/packages/unpkg.sh +++ b/websites/default/packages/unpkg.sh @@ -386,10 +386,10 @@ main() { # Check if the directory already exists and has content if [[ -d "$output_dir" ]] && [[ -n "$(ls -A "$output_dir" 2>/dev/null)" ]]; then - print_error "Directory already exists and is not empty: $output_dir" - print_error "Package $package_name@$resolved_version appears to already be downloaded." - print_error "Remove the directory or choose a different output location to proceed." - exit 1 + print_warning "Directory already exists and is not empty: $output_dir" + print_warning "Package $package_name@$resolved_version appears to already be downloaded." + print_warning "Remove the directory or choose a different output location to proceed." + return fi # Create the base output directory @@ -409,3 +409,4 @@ main "@solidjs/signals" main "@leeoniya/ufuzzy" main "lean-qr" main "lightweight-charts" +main "modern-screenshot" diff --git a/websites/default/scripts/chart.js b/websites/default/scripts/chart.js index 3fb99605e..043c57881 100644 --- a/websites/default/scripts/chart.js +++ b/websites/default/scripts/chart.js @@ -21,6 +21,7 @@ const CANDLE = "candle"; * @param {Elements} args.elements * @param {VecsResources} args.vecsResources * @param {VecIdToIndexes} args.vecIdToIndexes + * @param {Packages} args.packages */ export function init({ colors, @@ -32,6 +33,7 @@ export function init({ webSockets, vecsResources, vecIdToIndexes, + packages, }) { elements.charts.append(utils.dom.createShadow("left")); elements.charts.append(utils.dom.createShadow("right")); @@ -95,6 +97,33 @@ export function init({ }, }); + const chartBottomRightCanvas = Array.from( + chart.inner.chartElement().getElementsByTagName("tr"), + ).at(-1)?.lastChild?.firstChild?.firstChild; + if (chartBottomRightCanvas) { + const charts = elements.charts; + const domain = window.document.createElement("p"); + domain.innerText = `${window.location.host}`; + domain.id = "domain"; + const screenshotButton = window.document.createElement("button"); + screenshotButton.id = "screenshot"; + const camera = "[ ◉¯]"; + screenshotButton.innerHTML = camera; + screenshotButton.title = "Screenshot"; + chartBottomRightCanvas.replaceWith(screenshotButton); + screenshotButton.addEventListener("click", () => { + packages.modernScreenshot().then(async ({ screenshot }) => { + elements.body.dataset.screenshot = "true"; + charts.append(domain); + seriesTypeField.hidden = true; + await screenshot(charts); + charts.removeChild(domain); + seriesTypeField.hidden = false; + elements.body.dataset.screenshot = "false"; + }); + }); + } + chart.inner.timeScale().subscribeVisibleLogicalRangeChange( utils.debounce((t) => { if (t) { @@ -525,6 +554,12 @@ function createIndexSelector({ option, vecIdToIndexes, signals, utils }) { }); const fieldset = window.document.createElement("fieldset"); + fieldset.id = "interval"; + + const screenshotSpan = window.document.createElement("span"); + screenshotSpan.innerText = "interval:"; + fieldset.append(screenshotSpan); + fieldset.append(field); fieldset.dataset.size = "sm"; diff --git a/websites/default/scripts/main.js b/websites/default/scripts/main.js index 11a77c1f0..4f8b9156e 100644 --- a/websites/default/scripts/main.js +++ b/websites/default/scripts/main.js @@ -3,7 +3,7 @@ /** * @import { Option, PartialChartOption, ChartOption, AnyPartialOption, ProcessedOptionAddons, OptionsTree, SimulationOption, AnySeriesBlueprint, SeriesType } from "./options" * @import { Valued, SingleValueData, CandlestickData, OHLCTuple, Series, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions } from "../packages/lightweight-charts/wrapper" - * @import * as _ from "../packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.d.ts" + * @import * as _ from "../packages/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.d.ts" * @import { SerializedChartableIndex } from "./chart"; * @import { Signal, Signals, Accessor } from "../packages/solidjs-signals/wrapper"; * @import { DateIndex, DecadeIndex, DifficultyEpoch, Index, HalvingEpoch, Height, MonthIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex, P2SHAddressIndex, P2MSOutputIndex, P2AAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, TxIndex, InputIndex, OutputIndex, VecId, WeekIndex, SemesterIndex, YearIndex, VecIdToIndexes, QuarterIndex, EmptyOutputIndex, OpReturnIndex, UnknownOutputIndex } from "./vecid-to-indexes" @@ -78,10 +78,13 @@ function initPackages() { return import("../packages/lean-qr/2.5.0/index.mjs").then((d) => d); }, async ufuzzy() { - return import("../packages/leeoniya-ufuzzy/1.0.18/dist/uFuzzy.mjs").then( + return import("../packages/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs").then( ({ default: d }) => d, ); }, + async modernScreenshot() { + return import("../packages/modern-screenshot/wrapper.js").then((d) => d); + }, }; /** @@ -106,6 +109,7 @@ function initPackages() { lightweightCharts: importPackage("lightweightCharts"), leanQr: importPackage("leanQr"), ufuzzy: importPackage("ufuzzy"), + modernScreenshot: importPackage("modernScreenshot"), }; } /** @@ -2282,6 +2286,7 @@ function main() { webSockets, vecsResources, vecIdToIndexes, + packages, }), ), ),