├── .gitignore ├── README.md ├── index.html ├── lib ├── index.scss ├── index.ts ├── markdown.css ├── renderable.ts ├── util │ ├── colors.ts │ ├── index.ts │ └── urls.ts └── views │ ├── auto_stack.ts │ ├── base.ts │ ├── button.ts │ ├── header.ts │ ├── image.ts │ ├── index.ts │ ├── label.ts │ ├── layer.ts │ ├── markdown.ts │ ├── min_version.ts │ ├── rating.ts │ ├── review.ts │ ├── screenshots.ts │ ├── separator.ts │ ├── spacer.ts │ ├── stack.ts │ ├── subheader.ts │ ├── tab.ts │ ├── table_button.ts │ ├── table_text.ts │ ├── video.ts │ └── web.ts ├── package.json ├── test └── test.json ├── tsconfig.json ├── vite.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Test files 27 | test/* 28 | !test/test.json 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kennel 2 | 3 | A complete implementation of Sileo's native depictions in TypeScript. 4 | 5 | --- 6 | 7 | ### Get Started 8 | 9 | ```shell script 10 | $ npm i @parcility/kennel 11 | ``` 12 | 13 | ### API 14 | 15 | Kennel was written to be as easy to interact with as possible. 16 | 17 | `render(depiction: any, options?: Parital): Promise` 18 | 19 | > Render a depiction to either a HTMLElement or a string. 20 | > 21 | > `depiction`: An object that stores the native depiction's contents. 22 | > 23 | > `options`: The settings used for rendering. 24 | > `options.ssr`: Output a string instead of a DOM element. 25 | > `options.defaultTintColor`: The css color used for the tint. 26 | 27 | `hydrate(target?: ParentNode): void` 28 | 29 | > Runs the hydrate function on views that need to be hydrate. Can only be ran on the client side. 30 | > `target`: The root element for hydration. Defaults to `document.body`. 31 | 32 | #### Example 33 | 34 | ```ts 35 | // Import Kennel 36 | import { render, hydrate } from "@parcility/kennel"; 37 | 38 | // Assumes the `depiction` variables exists elsewhere. The second argument (options) can be omitted. 39 | let output = await render(depiction, { ssr: true }); 40 | 41 | // sometime on the client. 42 | hydrate(); 43 | ``` 44 | 45 | A full demo is available by running `yarn dev`. 46 | 47 | --- 48 | 49 | ### Development 50 | 51 | ### Testing 52 | 53 | Run the test page, which loads depictions from the `test/` directory. 54 | 55 | ```shell script 56 | yarn dev 57 | ``` 58 | 59 | ### Building 60 | 61 | This is not required if you installed Kennel through NPM. 62 | 63 | 1: Install dependencies 64 | 65 | ```shell script 66 | yarn install 67 | ``` 68 | 69 | 2: Build module 70 | 71 | ```shell script 72 | yarn build 73 | ``` 74 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kennel Testing 7 | 21 | 22 | 23 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/index.scss: -------------------------------------------------------------------------------- 1 | .nd-root { 2 | box-sizing: border-box; 3 | max-width: 100%; 4 | 5 | p { 6 | margin: 0; 7 | } 8 | 9 | * { 10 | box-sizing: border-box; 11 | } 12 | 13 | // Layouting 14 | 15 | .nd-separator { 16 | margin: 0 1rem; 17 | margin-bottom: 0.25rem; 18 | display: block; 19 | height: 1px; 20 | border: none; 21 | background-color: rgba(121, 121, 121, 0.5); 22 | } 23 | 24 | .nd-spacer { 25 | display: block; 26 | content: " "; 27 | } 28 | 29 | .nd-stack { 30 | display: flex; 31 | flex-direction: column; 32 | &.nd-stack-landscape { 33 | gap: 1rem; 34 | justify-content: space-between; 35 | flex-direction: row; 36 | align-items: center; 37 | 38 | & > * { 39 | flex: 1; 40 | } 41 | } 42 | } 43 | 44 | .nd-auto-stack { 45 | display: grid; 46 | } 47 | 48 | .nd-layer { 49 | position: relative; 50 | 51 | & > * { 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | } 56 | } 57 | 58 | // Headers 59 | 60 | .nd-header { 61 | font-size: 1.375rem; 62 | } 63 | 64 | .nd-subheader { 65 | font-size: 0.875rem; 66 | } 67 | 68 | .nd-header, 69 | .nd-subheader { 70 | white-space: nowrap; 71 | overflow: hidden; 72 | text-overflow: ellipsis; 73 | } 74 | 75 | .nd-subheader.nd-subheader-bold, 76 | .nd-header.nd-header-bold { 77 | font-weight: bold; 78 | } 79 | 80 | .nd-header.nd-header-margins, 81 | .nd-subheader.nd-subheader-margins { 82 | margin-top: 0.5rem; 83 | margin-left: 1rem; 84 | margin-right: 1rem; 85 | } 86 | 87 | .nd-header.nd-header-bottom-margin, 88 | .nd-subheader.nd-subheader-bottom-margin { 89 | margin-bottom: 0.5rem; 90 | } 91 | 92 | // Ratings & Reviews 93 | 94 | .nd-rating { 95 | display: inline-block; 96 | 97 | span { 98 | background: linear-gradient( 99 | to right, 100 | #ff8a00 var(--kennel-rating-progress), 101 | rgba(121, 121, 121, 0.15) var(--kennel-rating-progress) 102 | ); 103 | background-clip: text; 104 | -webkit-background-clip: text; 105 | -webkit-text-fill-color: transparent; 106 | } 107 | } 108 | 109 | .nd-review { 110 | padding: 1rem 1rem; 111 | background-color: rgba(121, 121, 121, 0.05); 112 | margin-bottom: 1rem; 113 | border-radius: 0.5rem; 114 | 115 | .nd-review-title { 116 | font-weight: 600; 117 | font-size: 1.1rem; 118 | margin: 0; 119 | } 120 | 121 | .nd-review-author { 122 | margin: 0; 123 | opacity: 0.8; 124 | } 125 | 126 | .nd-review-subtitle { 127 | margin-top: 0.25rem; 128 | display: flex; 129 | justify-content: space-between; 130 | align-items: center; 131 | } 132 | 133 | .nd-review-content { 134 | margin: 0; 135 | } 136 | } 137 | 138 | // Button 139 | 140 | .nd-button { 141 | box-sizing: border-box; 142 | margin: 0.5rem 0; 143 | display: block; 144 | text-decoration: none; 145 | 146 | & > p { 147 | margin: 0; 148 | } 149 | 150 | &.nd-button-link { 151 | color: var(--kennel-tint-color); 152 | margin: 0; 153 | } 154 | 155 | &.nd-button-not-link { 156 | box-sizing: border-box; 157 | text-align: center; 158 | display: block; 159 | border: none; 160 | -webkit-appearance: none; 161 | -moz-appearance: none; 162 | padding: 0.5rem 1rem; 163 | margin: 0.5rem 0.5rem; 164 | font: inherit; 165 | border-radius: 0.5rem; 166 | appearance: none; 167 | width: 100%; 168 | } 169 | } 170 | 171 | // Table Text 172 | 173 | .nd-table-text { 174 | display: flex; 175 | align-items: center; 176 | height: 44px; 177 | margin: 0 1rem; 178 | font-size: 1rem; 179 | 180 | .nd-table-text-title { 181 | flex: 1; 182 | color: rgb(175, 175, 175); 183 | margin: 0; 184 | } 185 | 186 | .nd-table-text-text { 187 | flex: 1; 188 | margin: 0; 189 | text-align: right; 190 | } 191 | } 192 | 193 | // Table Button 194 | 195 | .nd-table-button { 196 | display: flex; 197 | align-items: center; 198 | height: 44px; 199 | margin: 0 1rem; 200 | font-size: 1rem; 201 | justify-content: space-between; 202 | color: var(--kennel-tint-color); 203 | text-decoration: none; 204 | .nd-table-button-text { 205 | flex: 1 1 auto; 206 | } 207 | 208 | .nd-table-button-chevron { 209 | width: 0.45em; 210 | height: 0.45em; 211 | 212 | &::before { 213 | content: ""; 214 | border-style: solid; 215 | border-width: 0.1em 0.1em 0 0; 216 | display: inline-block; 217 | height: 0.45em; 218 | left: 0.15em; 219 | position: relative; 220 | top: 0.15em; 221 | transform: rotate(45deg); 222 | vertical-align: top; 223 | width: 0.45em; 224 | } 225 | } 226 | } 227 | 228 | // Tabs 229 | 230 | .nd-tabs { 231 | position: relative; 232 | max-width: 100%; 233 | display: grid; 234 | grid-template-columns: repeat(var(--kennel-tab-page-count), 1fr); 235 | grid-template-areas: var(--kennel-tab-areas); 236 | 237 | & > input { 238 | display: none; 239 | } 240 | 241 | & > .nd-tab-control { 242 | cursor: pointer; 243 | grid-area: tab; 244 | text-align: center; 245 | padding: 0.5rem 1rem; 246 | } 247 | 248 | & > input:checked + label { 249 | color: var(--kennel-tint-color); 250 | border-bottom: solid 1px; 251 | } 252 | 253 | & > input:checked + label + .nd-tab-page { 254 | display: block; 255 | } 256 | 257 | .nd-tab-page { 258 | grid-area: content; 259 | display: none; 260 | max-width: 100%; 261 | padding-top: 1rem; 262 | grid-column: 1 / -1; 263 | } 264 | } 265 | 266 | // Assets 267 | 268 | .nd-video, 269 | .nd-image { 270 | max-width: 100%; 271 | } 272 | 273 | .nd-video { 274 | background-color: #000; 275 | } 276 | 277 | .nd-screenshots { 278 | display: flex; 279 | overflow-x: scroll; 280 | flex-wrap: nowrap; 281 | scroll-snap-type: x mandatory; 282 | padding-bottom: 1rem; 283 | padding-left: 1rem; 284 | 285 | &::after { 286 | content: " "; 287 | flex-shrink: 0; 288 | display: block; 289 | height: 1px; 290 | width: calc(1rem); 291 | } 292 | 293 | & > * { 294 | margin-right: 1rem; 295 | scroll-snap-align: center; 296 | flex-shrink: 0; 297 | border-radius: var(--screenshot-item-radius); 298 | width: var(--screenshot-item-width); 299 | height: var(--screenshot-item-height); 300 | 301 | &:last-child { 302 | margin-right: 0; 303 | scroll-snap-align: start; 304 | } 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import "./index.scss"; 3 | import { createElement, renderElement, setStyles } from "./renderable"; 4 | import { constructView, constructViews, defaultIfNotType, KennelError, makeViews } from "./util"; 5 | import { DepictionBaseView, mountable } from "./views"; 6 | 7 | interface RenderOptions { 8 | ssr: boolean; 9 | defaultTintColor: string; 10 | } 11 | 12 | export async function render, U extends T["ssr"] extends true ? string : HTMLElement>( 13 | depiction: any, 14 | options?: T 15 | ): Promise { 16 | let tintColor = defaultIfNotType(depiction["tintColor"], "color", options?.defaultTintColor as string) as 17 | | string 18 | | undefined; 19 | 20 | // process the depiction 21 | let processed: DepictionBaseView[] | undefined; 22 | if (Array.isArray(depiction.tabs)) { 23 | depiction.className = "DepictionTabView"; 24 | let view = constructView(depiction); 25 | if (view) { 26 | processed = [view]; 27 | } 28 | } else if (Array.isArray(depiction.views)) { 29 | processed = constructViews(depiction.views); 30 | } 31 | if (!processed) throw new KennelError("Unable to process depiction. No child was found."); 32 | 33 | // build an element to render 34 | let el = createElement("form", { class: "nd-root" }); 35 | if (tintColor) { 36 | setStyles(el, { 37 | "--kennel-tint-color": tintColor, 38 | }); 39 | } 40 | el.children = await makeViews(processed); 41 | 42 | // return rendered element 43 | return renderElement(el, options?.ssr || false) as unknown as U; 44 | } 45 | 46 | export async function hydrate(el?: ParentNode) { 47 | if (!("HTMLElement" in globalThis)) throw new Error("Can't mount, no DOM in this environment"); 48 | if (!el) el = document; 49 | let mountableEls = el.querySelectorAll("[data-kennel-view]"); 50 | 51 | self.customElements.define( 52 | "nd-shadowed-content", 53 | class ShadowedElement extends HTMLElement { 54 | constructor() { 55 | super(); 56 | let tmpl = this.querySelector("template"); 57 | if (!tmpl) return; 58 | let content = tmpl.content.cloneNode(true); 59 | let shadow = this.attachShadow({ mode: "open" }); 60 | shadow.appendChild(content); 61 | } 62 | } 63 | ); 64 | 65 | for (let i = 0, len = mountableEls.length; i < len; i++) { 66 | let el = mountableEls[i]; 67 | let viewName = el.dataset.kennelView; 68 | if (typeof viewName !== "string") continue; 69 | let view = mountable.get(viewName); 70 | if (!view || !view.hydrate) continue; 71 | view.hydrate(el); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/markdown.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | box-sizing: border-box; 4 | max-width: 100%; 5 | } 6 | 7 | h1 { 8 | margin-top: 0; 9 | font-size: 1.75rem; 10 | } 11 | 12 | * + h1 { 13 | margin-top: 1rem; 14 | } 15 | 16 | h2 { 17 | margin-top: 0; 18 | font-size: 1.5rem; 19 | } 20 | 21 | * + h2 { 22 | margin-top: 1rem; 23 | } 24 | 25 | h3 { 26 | margin-top: 0; 27 | font-size: 1.125rem; 28 | } 29 | 30 | * + h3 { 31 | margin-top: 1rem; 32 | } 33 | 34 | h4 { 35 | margin-top: 0; 36 | font-size: 1rem; 37 | } 38 | 39 | * + h4 { 40 | margin-top: 1rem; 41 | } 42 | 43 | h5 { 44 | margin-top: 0; 45 | font-size: 0.875rem; 46 | } 47 | 48 | * + h5 { 49 | margin-top: 1rem; 50 | } 51 | 52 | h6 { 53 | margin-top: 0; 54 | font-size: 0.75rem; 55 | } 56 | 57 | * + h6 { 58 | margin-top: 1rem; 59 | } 60 | 61 | p { 62 | font-size: 1rem; 63 | margin: 0; 64 | } 65 | 66 | p + p { 67 | margin-top: 1rem; 68 | } 69 | 70 | code { 71 | font-size: 1rem; 72 | } 73 | 74 | a { 75 | color: var(--kennel-tint-color); 76 | } 77 | -------------------------------------------------------------------------------- /lib/renderable.ts: -------------------------------------------------------------------------------- 1 | import createDOMPurify, { DOMPurifyI } from "dompurify"; 2 | import { escapeHTML } from "./util"; 3 | 4 | const PURIFY_OPTIONS: createDOMPurify.Config = { 5 | RETURN_DOM_FRAGMENT: false, 6 | RETURN_DOM: false, 7 | FORCE_BODY: true, 8 | ADD_TAGS: ["iframe", "template", "style", "video"], 9 | ADD_ATTR: ["frameborder", "style"], 10 | CUSTOM_ELEMENT_HANDLING: { 11 | tagNameCheck: /^nd-shadowed-content$/, 12 | }, 13 | }; 14 | 15 | let DOMPurify: Promise = (async function () { 16 | if (!("window" in globalThis)) { 17 | const { JSDOM } = await import("jsdom"); 18 | const window = new JSDOM("").window; 19 | return createDOMPurify(window as any); 20 | } 21 | return createDOMPurify; 22 | })(); 23 | 24 | export interface RenderableElement { 25 | tag: string; 26 | attributes: Record; 27 | children: (RenderableElement | RenderableNode | string | undefined)[]; 28 | } 29 | 30 | export interface RenderableNode { 31 | raw: boolean; 32 | contents: string; 33 | } 34 | 35 | export async function createRawNode(contents: string): Promise { 36 | return { 37 | raw: true, 38 | contents: (await DOMPurify).sanitize(contents, PURIFY_OPTIONS) as string, 39 | }; 40 | } 41 | 42 | export function createElement( 43 | tag: RenderableElement["tag"], 44 | attributes?: RenderableElement["attributes"], 45 | children?: RenderableElement["children"] 46 | ): RenderableElement { 47 | return { tag, attributes: attributes ?? {}, children: children ?? [] }; 48 | } 49 | 50 | export function createShadowedElement( 51 | attributes: RenderableElement["attributes"], 52 | children: RenderableElement["children"] 53 | ): RenderableElement { 54 | return createElement("nd-shadowed-content", attributes, [createElement("template", {}, children)]); 55 | } 56 | 57 | export function renderElementDOM(el: RenderableElement): HTMLElement { 58 | const element = document.createElement(el.tag); 59 | for (const [key, value] of Object.entries(el.attributes)) { 60 | if (typeof value === "boolean") element.toggleAttribute(key, value); 61 | else element.setAttribute(key, escapeHTML(value, true)); 62 | } 63 | for (const child of el.children) { 64 | let target = el.tag === "template" ? (element as HTMLTemplateElement).content : element; 65 | if (child === undefined) continue; 66 | if (typeof child === "string") { 67 | target.appendChild(document.createTextNode(child)); 68 | } else if ((child as RenderableNode).raw) { 69 | let el = document.createElement("div"); 70 | el.innerHTML = (child as RenderableNode).contents; 71 | target.append.apply(target, Array.from(el.children)); 72 | } else { 73 | target.appendChild(renderElementDOM(child as RenderableElement)); 74 | } 75 | } 76 | return element; 77 | } 78 | 79 | export async function renderElementString(el: RenderableElement): Promise { 80 | let result = `<${el.tag} `; 81 | result += Object.entries(el.attributes) 82 | .map(([key, value]) => 83 | typeof value === "boolean" ? `${value ? key : ""}` : `${key}="${escapeHTML(value, true)}"` 84 | ) 85 | .join(" "); 86 | let children = await Promise.all( 87 | el.children.map(async (child) => { 88 | if (!child) return ""; 89 | if (typeof child === "string") { 90 | return escapeHTML(child); 91 | } else if ((child as RenderableNode).raw) { 92 | return (child as RenderableNode).contents; 93 | } 94 | return renderElementString(child as RenderableElement); 95 | }) 96 | ); 97 | result += `>${children.join("")}`; 98 | 99 | let res = (await DOMPurify).sanitize(result, PURIFY_OPTIONS) as string; 100 | return res; 101 | } 102 | 103 | export function renderElement( 104 | el: RenderableElement, 105 | ssr: T 106 | ): Promise { 107 | if (ssr) return renderElementString(el) as unknown as Promise; 108 | return Promise.resolve(renderElementDOM(el) as unknown as U); 109 | } 110 | 111 | export function setStyles(el: RenderableElement, styles: Record, original: string = "") { 112 | let resp = [original, Object.entries(styles).map(([key, value]) => `${key}: ${value}`)] 113 | .filter(Boolean) 114 | .flat() 115 | .join(";"); 116 | el.attributes["style"] = resp; 117 | } 118 | 119 | export function setClassList(el: RenderableElement, classList: (string | boolean | undefined)[]) { 120 | el.attributes["class"] = classList.filter(Boolean).join(" "); 121 | } 122 | -------------------------------------------------------------------------------- /lib/util/colors.ts: -------------------------------------------------------------------------------- 1 | // This validates CSS colors according to Sileo, though for speed purporses we try to use the CSS.supports API. 2 | export function isValidColor(color: string): boolean { 3 | if (typeof color !== "string") return false; 4 | if ("CSS" in globalThis && "supports" in CSS) return CSS.supports("color", color); 5 | return isHex(color) || isRGB(color) || isHSL(color) || isNamed(color); 6 | } 7 | 8 | function isHex(color: string): boolean { 9 | return /^#([0-9A-F]{3}|[0-9A-F]{6}|[0-9A-F]{8})$/i.test(color); 10 | } 11 | 12 | function isRGB(color: string): boolean { 13 | return /^(rgb|rgba)\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),?\s*(\d*(?:\.\d+)?)\)$/.test(color); 14 | } 15 | 16 | function isHSL(color: string): boolean { 17 | return /^(hsl|hsla)\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,?\s*(\d*(?:\.\d+)?)\)$/.test(color); 18 | } 19 | 20 | function isNamed(color: string): boolean { 21 | return COLOR_NAMES.includes(color.toLowerCase()); 22 | } 23 | 24 | const COLOR_NAMES: string[] = [ 25 | "aliceblue", 26 | "antiquewhite", 27 | "aqua", 28 | "aquamarine", 29 | "azure", 30 | "beige", 31 | "bisque", 32 | "black", 33 | "blanchedalmond", 34 | "blue", 35 | "blueviolet", 36 | "brown", 37 | "burlywood", 38 | "cadetblue", 39 | "chartreuse", 40 | "chocolate", 41 | "coral", 42 | "cornflowerblue", 43 | "cornsilk", 44 | "crimson", 45 | "cyan", 46 | "darkblue", 47 | "darkcyan", 48 | "darkgoldenrod", 49 | "darkgray", 50 | "darkgrey", 51 | "darkgreen", 52 | "darkkhaki", 53 | "darkmagenta", 54 | "darkolivegreen", 55 | "darkorange", 56 | "darkorchid", 57 | "darkred", 58 | "darksalmon", 59 | "darkseagreen", 60 | "darkslateblue", 61 | "darkslategray", 62 | "darkslategrey", 63 | "darkturquoise", 64 | "darkviolet", 65 | "deeppink", 66 | "deepskyblue", 67 | "dimgray", 68 | "dimgrey", 69 | "dodgerblue", 70 | "firebrick", 71 | "floralwhite", 72 | "forestgreen", 73 | "fuchsia", 74 | "gainsboro", 75 | "ghostwhite", 76 | "gold", 77 | "goldenrod", 78 | "gray", 79 | "grey", 80 | "green", 81 | "greenyellow", 82 | "honeydew", 83 | "hotpink", 84 | "indianred", 85 | "indigo", 86 | "ivory", 87 | "khaki", 88 | "lavender", 89 | "lavenderblush", 90 | "lawngreen", 91 | "lemonchiffon", 92 | "lightblue", 93 | "lightcoral", 94 | "lightcyan", 95 | "lightgoldenrodyellow", 96 | "lightgray", 97 | "lightgrey", 98 | "lightgreen", 99 | "lightpink", 100 | "lightsalmon", 101 | "lightseagreen", 102 | "lightskyblue", 103 | "lightslategray", 104 | "lightslategrey", 105 | "lightsteelblue", 106 | "lightyellow", 107 | "lime", 108 | "limegreen", 109 | "linen", 110 | "magenta", 111 | "maroon", 112 | "mediumaquamarine", 113 | "mediumblue", 114 | "mediumorchid", 115 | "mediumpurple", 116 | "mediumseagreen", 117 | "mediumslateblue", 118 | "mediumspringgreen", 119 | "mediumturquoise", 120 | "mediumvioletred", 121 | "midnightblue", 122 | "mintcream", 123 | "mistyrose", 124 | "moccasin", 125 | "navajowhite", 126 | "navy", 127 | "oldlace", 128 | "olive", 129 | "olivedrab", 130 | "orange", 131 | "orangered", 132 | "orchid", 133 | "palegoldenrod", 134 | "palegreen", 135 | "paleturquoise", 136 | "palevioletred", 137 | "papayawhip", 138 | "peachpuff", 139 | "peru", 140 | "pink", 141 | "plum", 142 | "powderblue", 143 | "purple", 144 | "rebeccapurple", 145 | "red", 146 | "rosybrown", 147 | "royalblue", 148 | "saddlebrown", 149 | "salmon", 150 | "sandybrown", 151 | "seagreen", 152 | "seashell", 153 | "sienna", 154 | "silver", 155 | "skyblue", 156 | "slateblue", 157 | "slategray", 158 | "slategrey", 159 | "snow", 160 | "springgreen", 161 | "steelblue", 162 | "tan", 163 | "teal", 164 | "thistle", 165 | "tomato", 166 | "turquoise", 167 | "violet", 168 | "wheat", 169 | "white", 170 | "whitesmoke", 171 | "yellow", 172 | "yellowgreen", 173 | "transparent", 174 | ]; 175 | -------------------------------------------------------------------------------- /lib/util/index.ts: -------------------------------------------------------------------------------- 1 | import { RenderableElement, setStyles } from "../renderable"; 2 | import { DepictionBaseView, views } from "../views"; 3 | import { isValidColor } from "./colors"; 4 | import { isValidHttpUrl, isValidHttpUrlExtended } from "./urls"; 5 | 6 | export class KennelError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = "KennelError"; 10 | } 11 | } 12 | 13 | export function parseSize(str: string): number[] { 14 | let trimmed = str.replace("{", "").replace("}", ""); 15 | return trimmed 16 | .split(",") 17 | .filter(Boolean) 18 | .map((s) => parseFloat(s.trim())); 19 | } 20 | 21 | export function fontWeightParse(fontWeight: string): string { 22 | switch (fontWeight) { 23 | case "black": 24 | return "900"; 25 | case "bold": 26 | return "700"; 27 | case "heavy": 28 | return "800"; 29 | case "light": 30 | return "400"; 31 | case "medium": 32 | return "500"; 33 | case "semibold": 34 | return "600"; 35 | case "thin": 36 | return "300"; 37 | case "ultralight": 38 | return "200"; 39 | default: 40 | return "400"; 41 | } 42 | } 43 | 44 | export function buttonLinkHandler(el: RenderableElement, url: string, label?: string) { 45 | let link = url; 46 | // javascript: links should do nothing. 47 | const jsXssIndex = url.indexOf("javascript:"); 48 | if (jsXssIndex !== -1) { 49 | link = url.substring(0, jsXssIndex) + encodeURIComponent(url.substring(jsXssIndex)); 50 | // depiction- links should link to a depiction. Use Parcility's API for this. 51 | } else if (url.indexOf("depiction-") == 0) { 52 | url = url.substring(10); 53 | if (!label) label = "Depiction"; 54 | link = `https://api.parcility.co/render/headerless?url=${encodeURIComponent(url)}&name=${label}`; 55 | } else if (url.indexOf("form-") == 0) { 56 | url = url.substring(5); 57 | link = `https://api.parcility.co/render/form?url=${encodeURIComponent(url)}`; 58 | } 59 | el.attributes.href = link; 60 | } 61 | 62 | // Alignment 63 | 64 | export enum Alignment { 65 | Left, 66 | Center, 67 | Right, 68 | } 69 | 70 | export function getAlignment(value: any): Alignment { 71 | switch (value) { 72 | case 1: 73 | return Alignment.Center; 74 | case 2: 75 | return Alignment.Right; 76 | default: 77 | return Alignment.Left; 78 | } 79 | } 80 | 81 | export function textAlignment(value: any): string { 82 | switch (value) { 83 | case 1: 84 | return "center"; 85 | case 2: 86 | return "right"; 87 | default: 88 | return "left"; 89 | } 90 | } 91 | 92 | export function applyAlignmentMargin(el: RenderableElement, alignment: Alignment) { 93 | let styles; 94 | switch (alignment) { 95 | case Alignment.Left: 96 | styles = { "margin-right": "auto" }; 97 | break; 98 | case Alignment.Right: 99 | styles = { "margin-left": "auto" }; 100 | break; 101 | case Alignment.Center: 102 | styles = { "margin-left": "auto", "margin-right": "auto" }; 103 | break; 104 | } 105 | setStyles(el, styles, typeof el.attributes.style === "boolean" ? "" : el.attributes.style || ""); 106 | } 107 | 108 | // Processing 109 | 110 | export function constructView(view: any): DepictionBaseView | undefined { 111 | let v = views.get(view.class); 112 | try { 113 | if (v) return new v(view); 114 | } catch (error) { 115 | console.error(error); 116 | } 117 | return undefined; 118 | } 119 | 120 | export function constructViews(views: any[]): DepictionBaseView[] { 121 | return views.map(constructView).filter(Boolean) as DepictionBaseView[]; 122 | } 123 | 124 | export async function makeView(view: DepictionBaseView): Promise { 125 | let madeView = await view.make(); 126 | if ("hydrate" in view.constructor) madeView.attributes["data-kennel-view"] = (view.constructor as any).viewName; 127 | return madeView; 128 | } 129 | 130 | export function makeViews(views: DepictionBaseView[]): Promise { 131 | return Promise.all(views.map(async (view) => makeView(view))); 132 | } 133 | 134 | // Type handling & validation 135 | 136 | export interface ValidTypes { 137 | undefined: undefined; 138 | object: null | ArrayLike | Record; 139 | array: any[]; 140 | boolean: boolean; 141 | number: number; 142 | bigint: bigint; 143 | string: string; 144 | symbol: symbol; 145 | function: Function; 146 | color: string; 147 | url: string; 148 | urlExtended: string; 149 | } 150 | 151 | export function isType(value: any, type: T): boolean { 152 | return ( 153 | (type === "array" && Array.isArray(value)) || 154 | (type === "color" && isValidColor(value)) || 155 | (type === "url" && isValidHttpUrl(value)) || 156 | (type === "urlExtended" && isValidHttpUrlExtended(value)) || 157 | typeof value === type 158 | ); 159 | } 160 | 161 | export function undefIfNotType( 162 | value: any, 163 | type: T 164 | ): U | undefined { 165 | if (isType(value, type)) return value; 166 | return undefined; 167 | } 168 | 169 | export function defaultIfNotType( 170 | value: any, 171 | type: T, 172 | defaultValue: U 173 | ): U { 174 | if (isType(value, type)) return value; 175 | return defaultValue; 176 | } 177 | 178 | export function guardIfNotType(value: any, type: T): U { 179 | if (!isType(value, type)) throw new KennelError(`Expected type ${type} but got ${typeof value}`); 180 | return value; 181 | } 182 | 183 | const ESCAPE_HTML_MAP: Record = { 184 | "&": "&", 185 | "<": "<", 186 | ">": ">", 187 | '"': """, 188 | "'": "'", 189 | "/": "/", 190 | "`": "`", 191 | "=": "=", 192 | }; 193 | 194 | const ESCAPE_ATTRIBUTE_MAP: Record = { 195 | "<": "<", 196 | ">": ">", 197 | '"': """, 198 | // "'": "'", 199 | "`": "`", 200 | }; 201 | 202 | export function escapeHTML(potentialHTML: string, isAttribute: boolean = false): string { 203 | // if we're in the browser, we can use the Option object to escape HTML. 204 | if (!isAttribute && "HTMLOptionElement" in globalThis && "Option" in globalThis) { 205 | return new Option(potentialHTML).innerHTML; 206 | } 207 | 208 | let map = isAttribute ? ESCAPE_ATTRIBUTE_MAP : ESCAPE_HTML_MAP; 209 | let charset = isAttribute ? /[<>"`]/g : /[&<>"'`=\/]/g; 210 | return potentialHTML.replace(charset, function (s) { 211 | return map[s]; 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /lib/util/urls.ts: -------------------------------------------------------------------------------- 1 | export function isValidHttpUrl(string: string): boolean { 2 | let url; 3 | try { 4 | url = new URL(string); 5 | } catch (_) { 6 | return false; 7 | } 8 | return url.protocol === "http:" || url.protocol === "https:"; 9 | } 10 | 11 | export function isValidHttpUrlExtended(string: string): boolean { 12 | if(isValidHttpUrl(string)) { 13 | return true; 14 | } 15 | let url; 16 | try { 17 | url = new URL(string); 18 | } catch (_) { 19 | return false; 20 | } 21 | return url.protocol == "depiction-http:" || url.protocol == "depiction-https:" || 22 | url.protocol == "form-http:" || url.protocol == "form-https:"; 23 | } -------------------------------------------------------------------------------- /lib/views/auto_stack.ts: -------------------------------------------------------------------------------- 1 | import { createElement, RenderableElement, setStyles } from "../renderable"; 2 | import { constructView, guardIfNotType, makeViews, undefIfNotType } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionAutoStackView extends DepictionBaseView { 6 | views: DepictionBaseView[] = []; 7 | viewWidths: number[]; 8 | horizontalSpacing: number; 9 | backgroundColor?: string; 10 | static viewName = "DepictionAutoStackView"; 11 | 12 | constructor(depiction: any) { 13 | super(depiction); 14 | 15 | let views = guardIfNotType(depiction["views"], "array"); 16 | this.horizontalSpacing = guardIfNotType(depiction["horizontalSpacing"], "number"); 17 | for (let view of views) { 18 | guardIfNotType(view["class"], "string"); 19 | guardIfNotType(view["preferredWidth"], "number"); 20 | let v = constructView(view); 21 | if (!v) throw new Error("Invalid view"); 22 | this.views.push(v); 23 | } 24 | 25 | this.viewWidths = views.map((view) => view["preferredWidth"] as number); 26 | this.backgroundColor = undefIfNotType(depiction["backgroundColor"], "color"); 27 | } 28 | 29 | async make(): Promise { 30 | let children = await makeViews(this.views); 31 | let el = createElement("div", { class: "nd-auto-stack" }, children); 32 | let styles: any = { 33 | "grid-template-columns": this.viewWidths.map((v) => v + "px").join(" "), 34 | "column-gap": this.horizontalSpacing, 35 | }; 36 | if (this.backgroundColor) styles["background-color"] = this.backgroundColor; 37 | setStyles(el, styles); 38 | return el; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/views/base.ts: -------------------------------------------------------------------------------- 1 | import { RenderableElement } from "../renderable"; 2 | import { undefIfNotType } from "../util"; 3 | 4 | export default abstract class DepictionBaseView { 5 | tintColor?: string; 6 | static viewName = "DepictionBaseView"; 7 | 8 | constructor(depiction: any) { 9 | if (depiction) { 10 | this.tintColor = undefIfNotType(depiction["tintColor"], "color"); 11 | } 12 | } 13 | 14 | abstract make(): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /lib/views/button.ts: -------------------------------------------------------------------------------- 1 | import { createElement, RenderableElement, setClassList, setStyles } from "../renderable"; 2 | import { buttonLinkHandler, constructView, defaultIfNotType, guardIfNotType, makeView, undefIfNotType } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionButtonView extends DepictionBaseView { 6 | text?: string; 7 | children?: DepictionBaseView; 8 | action: string; 9 | isLink: boolean; 10 | yPadding: number; 11 | openExternal: boolean; 12 | static viewName = "DepictionButtonView"; 13 | 14 | constructor(dictionary: any) { 15 | super(dictionary); 16 | this.isLink = defaultIfNotType(dictionary["isLink"], "boolean", false); 17 | this.yPadding = defaultIfNotType(dictionary["yPadding"], "number", 0); 18 | let action = undefIfNotType(dictionary["action"], "urlExtended"); 19 | if(typeof action !== "string") { 20 | this.action = guardIfNotType(dictionary["backupAction"], "urlExtended"); 21 | } else { 22 | this.action = action; 23 | } 24 | this.openExternal = defaultIfNotType(dictionary["openExternal"], "boolean", false); 25 | 26 | let dict = dictionary["view"]; 27 | if (typeof dict === "object") { 28 | this.children = constructView(dict); 29 | } 30 | 31 | if (!this.children) { 32 | let text = dictionary["text"]; 33 | if (typeof text === "string") { 34 | this.text = text; 35 | } 36 | } 37 | } 38 | 39 | async make(): Promise { 40 | let el = createElement("a"); 41 | setClassList(el, ["nd-button", this.isLink ? "nd-button-link" : "nd-button-not-link"]); 42 | let styles: any = {}; 43 | if (this.tintColor) styles["--kennel-tint-color"] = this.tintColor; 44 | buttonLinkHandler(el, this.action, this.text); 45 | if (this.isLink) { 46 | styles.color = "var(--kennel-tint-color)"; 47 | } else { 48 | styles["background-color"] = "var(--kennel-tint-color)"; 49 | styles["color"] = "white"; 50 | } 51 | 52 | if (this.openExternal) { 53 | el.attributes.target = "_blank"; 54 | } 55 | 56 | if (this.children) { 57 | this.children; 58 | let child = await makeView(this.children); 59 | child.attributes.pointerEvents = "none"; 60 | el.children = [child]; 61 | } else if (this.text) { 62 | el.children = [this.text]; 63 | } 64 | setStyles(el, styles); 65 | return el; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/views/header.ts: -------------------------------------------------------------------------------- 1 | import { createElement, setClassList, setStyles } from "../renderable"; 2 | import { defaultIfNotType, textAlignment } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionHeaderView extends DepictionBaseView { 6 | title?: string; 7 | useMargins: boolean; 8 | useBottomMargin: boolean; 9 | bold: boolean; 10 | textColor?: string; 11 | alignment: string; 12 | static viewName = "DepictionHeaderView"; 13 | 14 | constructor(dictionary: any) { 15 | super(dictionary); 16 | if (typeof dictionary["title"] === "string") { 17 | this.title = dictionary.title; 18 | } 19 | this.useMargins = defaultIfNotType(dictionary["useMargins"], "boolean", true); 20 | this.useBottomMargin = defaultIfNotType(dictionary["useBottomMargin"], "boolean", true); 21 | this.bold = defaultIfNotType(dictionary["useBoldText"], "boolean", true); 22 | if (!this.bold) { 23 | this.textColor = "rgb(175, 175, 175)"; 24 | } 25 | 26 | this.alignment = textAlignment(dictionary["alignment"]); 27 | } 28 | 29 | async make() { 30 | const el = createElement("p", {}, [this.title]); 31 | setClassList(el, [ 32 | "nd-header", 33 | this.bold && "nd-header-bold", 34 | this.useMargins && "nd-header-margins", 35 | this.useBottomMargin && "nd-header-bottom-margin", 36 | ]); 37 | let styles: any = { "text-align": this.alignment }; 38 | if (this.textColor) styles["color"] = this.textColor; 39 | setStyles(el, styles); 40 | return el; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/views/image.ts: -------------------------------------------------------------------------------- 1 | import { createElement, RenderableElement, setStyles } from "../renderable"; 2 | import { Alignment, applyAlignmentMargin, defaultIfNotType, getAlignment, guardIfNotType, KennelError } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionImageView extends DepictionBaseView { 6 | alignment: Alignment; 7 | url: string; 8 | width: number; 9 | height: number; 10 | xPadding: number; 11 | borderRadius: number; 12 | static viewName = "DepictionImageView"; 13 | 14 | constructor(dictionary: any) { 15 | super(dictionary); 16 | this.url = guardIfNotType(dictionary["URL"], "url"); 17 | this.width = defaultIfNotType(dictionary["width"], "number", 0); 18 | this.height = defaultIfNotType(dictionary["height"], "number", 0); 19 | 20 | if (this.width === 0 || this.height === 0) throw new KennelError("Invalid image size"); 21 | 22 | this.borderRadius = guardIfNotType(dictionary["cornerRadius"], "number"); 23 | this.alignment = getAlignment(defaultIfNotType(dictionary["alignment"], "number", 0)); 24 | this.xPadding = defaultIfNotType(dictionary["xPadding"], "number", 0); 25 | } 26 | 27 | async make(): Promise { 28 | const el = createElement("img", { class: "nd-image", src: this.url, loading: "lazy" }); 29 | setStyles(el, { 30 | width: `${this.width}px`, 31 | height: `${this.height}px`, 32 | "border-radius": `${this.borderRadius}px`, 33 | "object-fit": "cover", 34 | padding: `0 ${this.xPadding}px`, 35 | }); 36 | applyAlignmentMargin(el, this.alignment); 37 | return el; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/views/index.ts: -------------------------------------------------------------------------------- 1 | export type { default as DepictionBaseView } from "./base"; 2 | 3 | import AutoStackView from "./auto_stack"; 4 | import ButtonView from "./button"; 5 | import HeaderView from "./header"; 6 | import ImageView from "./image"; 7 | import LabelView from "./label"; 8 | import LayerView from "./layer"; 9 | import MarkdownView from "./markdown"; 10 | import MinVersionForceView from "./min_version"; 11 | import RatingView from "./rating"; 12 | import ReviewView from "./review"; 13 | import ScreenshotsView from "./screenshots"; 14 | import SeparatorView from "./separator"; 15 | import SpacerView from "./spacer"; 16 | import StackView from "./stack"; 17 | import SubheaderView from "./subheader"; 18 | import TabView from "./tab"; 19 | import TableButtonView from "./table_button"; 20 | import TableTextView from "./table_text"; 21 | import VideoView from "./video"; 22 | import WebView from "./web"; 23 | 24 | import DepictionBaseView from "./base"; 25 | 26 | export type DepictionViewConstructor = { 27 | new (dictionary: any): T; 28 | viewName: string; 29 | hydrate?(el: HTMLElement): void; 30 | }; 31 | 32 | export const views = new Map>([ 33 | ["DepictionAutoStackView", AutoStackView], 34 | ["DepictionButtonView", ButtonView], 35 | ["DepictionHeaderView", HeaderView], 36 | ["DepictionImageView", ImageView], 37 | ["DepictionLabelView", LabelView], 38 | ["DepictionLayerView", LayerView], 39 | ["DepictionMarkdownView", MarkdownView], 40 | ["DepictionMinVersionForceView", MinVersionForceView], 41 | ["DepictionRatingView", RatingView], 42 | ["DepictionReviewView", ReviewView], 43 | ["DepictionScreenshotsView", ScreenshotsView], 44 | ["DepictionSeparatorView", SeparatorView], 45 | ["DepictionSpacerView", SpacerView], 46 | ["DepictionStackView", StackView], 47 | ["DepictionSubheaderView", SubheaderView], 48 | ["DepictionTabView", TabView], 49 | ["DepictionTableButtonView", TableButtonView], 50 | ["DepictionTableTextView", TableTextView], 51 | ["DepictionVideoView", VideoView], 52 | ["DepictionWebView", WebView], 53 | ]); 54 | 55 | export const mountable = new Map>( 56 | Array.from(views.values()).map((v) => [v.viewName, v]) 57 | ); 58 | -------------------------------------------------------------------------------- /lib/views/label.ts: -------------------------------------------------------------------------------- 1 | import { createElement, RenderableElement, setStyles } from "../renderable"; 2 | import { defaultIfNotType, fontWeightParse, parseSize, textAlignment, undefIfNotType } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionLabelView extends DepictionBaseView { 6 | text?: string; 7 | margins = { left: 0, right: 0, top: 0, bottom: 0 }; 8 | textColor?: string; 9 | weight: string; 10 | alignment: string; 11 | isActionable: any; 12 | isHighlighted: any; 13 | fontSize: number; 14 | static viewName = "DepictionLabelView"; 15 | 16 | constructor(dictionary: any) { 17 | super(dictionary); 18 | if (typeof dictionary["text"] === "string") { 19 | this.text = dictionary.text; 20 | } 21 | 22 | let rawMargins = dictionary["margins"]; 23 | if (typeof rawMargins === "string") { 24 | let [top, left, bottom, right] = parseSize(rawMargins); 25 | this.margins = { left, right, top, bottom }; 26 | } 27 | 28 | if (this.margins.left === 0) this.margins.left = 16; 29 | if (this.margins.right === 0) this.margins.right = 16; 30 | 31 | let useMargins = defaultIfNotType(dictionary["useMargins"], "boolean", true); 32 | let usePadding = defaultIfNotType(dictionary["usePadding"], "boolean", true); 33 | 34 | if (!useMargins) { 35 | this.margins = { left: 0, right: 0, top: 0, bottom: 0 }; 36 | } else if (!usePadding) { 37 | this.margins.top = 0; 38 | this.margins.bottom = 0; 39 | } 40 | 41 | let fontWeight = defaultIfNotType(dictionary["fontWeight"], "string", "normal"); 42 | 43 | this.fontSize = defaultIfNotType(dictionary["fontSize"], "number", 14); 44 | 45 | this.textColor = undefIfNotType(dictionary["textColor"], "color"); 46 | 47 | this.weight = fontWeightParse(fontWeight); 48 | this.alignment = textAlignment(dictionary["alignment"]); 49 | } 50 | 51 | async make(): Promise { 52 | const el = createElement("p", { class: "nd-label" }, [this.text]); 53 | let styles: Record = { 54 | "text-align": this.alignment, 55 | "font-weight": this.weight, 56 | "font-size": `${this.fontSize}px`, 57 | "margin-top": this.margins.top + "px", 58 | "margin-right": this.margins.right + "px", 59 | "margin-left": this.margins.left + "px", 60 | "margin-bottom": this.margins.bottom + "px", 61 | }; 62 | if (this.textColor) styles.color = this.textColor; 63 | if (this.tintColor) styles["--kennel-tint-color"] = this.tintColor; 64 | if (!this.textColor) { 65 | if (this.isActionable) { 66 | if (this.isHighlighted) { 67 | styles.filter = "saturation(75%)"; 68 | } else { 69 | styles.color = "var(--kennel-tint-color)"; 70 | } 71 | } 72 | } 73 | setStyles(el, styles); 74 | return el; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/views/layer.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "../renderable"; 2 | import { constructViews, guardIfNotType, makeViews } from "../util"; 3 | import DepictionBaseView from "./base"; 4 | 5 | export default class DepictionLayerView extends DepictionBaseView { 6 | views: DepictionBaseView[]; 7 | static viewName = "DepictionLayerView"; 8 | 9 | constructor(dictionary: any) { 10 | super(dictionary); 11 | let rawViews = guardIfNotType(dictionary["views"], "array"); 12 | this.views = constructViews(rawViews); 13 | } 14 | 15 | async make() { 16 | const el = createElement("div", { class: "nd-layer" }); 17 | el.children = await makeViews(this.views); 18 | return el; 19 | } 20 | 21 | static hydrate(el: HTMLElement) { 22 | let arr = Array.from(el.children); 23 | let maxHeight = arr.reduce((max, el) => { 24 | let height = el.getBoundingClientRect().height; 25 | return height > max ? height : max; 26 | }, 0); 27 | el.style.height = maxHeight + "px"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/views/markdown.ts: -------------------------------------------------------------------------------- 1 | import { marked } from "marked"; 2 | import markdownStyles from "../markdown.css?raw"; 3 | import { createElement, createRawNode, createShadowedElement, renderElementString, setStyles } from "../renderable"; 4 | import { defaultIfNotType, escapeHTML, guardIfNotType, makeView } from "../util"; 5 | import DepictionBaseView from "./base"; 6 | import DepictionSeparatorView from "./separator"; 7 | 8 | function callMarked(str: string, opt?: any): Promise { 9 | return new Promise((resolve, reject) => 10 | marked(str, opt, (err: any, html: string) => { 11 | if (err) reject(err); 12 | else resolve(html); 13 | }) 14 | ); 15 | } 16 | 17 | export default class DepictionMarkdownView extends DepictionBaseView { 18 | markdown: Promise; 19 | useSpacing: boolean; 20 | useMargins: boolean; 21 | useRawFormat: boolean; 22 | static viewName = "DepictionMarkdownView"; 23 | 24 | constructor(dictionary: any) { 25 | super(dictionary); 26 | let md = guardIfNotType(dictionary["markdown"], "string"); 27 | this.useMargins = defaultIfNotType(dictionary["useMargins"], "boolean", true); 28 | this.useSpacing = defaultIfNotType(dictionary["useSpacing"], "boolean", true); 29 | this.useRawFormat = defaultIfNotType(dictionary["useRawFormat"], "boolean", false); 30 | if (this.useRawFormat) { 31 | this.markdown = callMarked(md, { gfm: false }).then(async (rendered) => { 32 | let didWarnXSS = false; 33 | let xssWarn = `

[Warning: This depiction may be trying to maliciously run code in your browser.]


`; 34 | rendered = rendered.replace( 35 | /
/gi, 36 | await renderElementString(await makeView(new DepictionSeparatorView(undefined))) 37 | ); 38 | if ( 39 | rendered.toLowerCase().indexOf("") !== -1 41 | ) { 42 | rendered = rendered 43 | .replace(/