diff --git a/.changeset/stale-steaks-drop.md b/.changeset/stale-steaks-drop.md
new file mode 100644
index 000000000000..324ba9bc978d
--- /dev/null
+++ b/.changeset/stale-steaks-drop.md
@@ -0,0 +1,5 @@
+---
+"create-typescript-playground-plugin": patch
+---
+
+Remove innerHTML uses
diff --git a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts
index 8344892361ca..4a5932f2a5d0 100644
--- a/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts
+++ b/packages/create-typescript-playground-plugin/template/src/vendor/utils.ts
@@ -5,10 +5,3 @@ export const requireURL = (path: string) => {
const prefix = isDev ? 'local/' : 'unpkg/typescript-playground-presentation-mode/dist/'
return prefix + path
}
-
-/** Use this to make a few dumb element generation funcs */
-export const el = (str: string, el: string, container: Element) => {
- const para = document.createElement(el)
- para.innerHTML = str
- container.appendChild(para)
-}
diff --git a/packages/playground/src/createConfigDropdown.ts b/packages/playground/src/createConfigDropdown.ts
index f67f8e38aca1..2d1c67f7a0c1 100644
--- a/packages/playground/src/createConfigDropdown.ts
+++ b/packages/playground/src/createConfigDropdown.ts
@@ -50,6 +50,40 @@ const notRelevantToPlayground = [
"forceConsistentCasingInFileNames",
]
+const createInfoIcon = () => {
+ const svgNamespace = "http://www.w3.org/2000/svg"
+ const svg = document.createElementNS(svgNamespace, "svg")
+ svg.setAttribute("width", "20px")
+ svg.setAttribute("height", "20px")
+ svg.setAttribute("viewBox", "0 0 20 20")
+ svg.setAttribute("version", "1.1")
+
+ const g = document.createElementNS(svgNamespace, "g")
+ g.setAttribute("stroke", "none")
+ g.setAttribute("stroke-width", "1")
+ g.setAttribute("fill", "none")
+ g.setAttribute("fill-rule", "evenodd")
+ svg.appendChild(g)
+
+ const circle = document.createElementNS(svgNamespace, "circle")
+ circle.setAttribute("stroke", "#0B6F57")
+ circle.setAttribute("cx", "10")
+ circle.setAttribute("cy", "10")
+ circle.setAttribute("r", "9")
+ g.appendChild(circle)
+
+ const path = document.createElementNS(svgNamespace, "path")
+ path.setAttribute(
+ "d",
+ "M9.99598394,6 C10.2048193,6 10.4243641,5.91700134 10.6546185,5.75100402 C10.8848728,5.58500669 11,5.33601071 11,5.00401606 C11,4.66666667 10.8848728,4.41499331 10.6546185,4.24899598 C10.4243641,4.08299866 10.2048193,4 9.99598394,4 C9.79250335,4 9.57563588,4.08299866 9.34538153,4.24899598 C9.11512718,4.41499331 9,4.66666667 9,5.00401606 C9,5.33601071 9.11512718,5.58500669 9.34538153,5.75100402 C9.57563588,5.91700134 9.79250335,6 9.99598394,6 Z M10.6877323,16 L10.6877323,14.8898836 L10.6877323,8 L9.30483271,8 L9.30483271,9.11011638 L9.30483271,16 L10.6877323,16 Z"
+ )
+ path.setAttribute("fill", "#0B6F57")
+ path.setAttribute("fill-rule", "nonzero")
+ g.appendChild(path)
+
+ return svg
+}
+
export const createConfigDropdown = (sandbox: Sandbox, monaco: Monaco) => {
const configContainer = document.getElementById("config-container")!
const container = document.createElement("div")
@@ -121,13 +155,21 @@ export const createConfigDropdown = (sandbox: Sandbox, monaco: Monaco) => {
label.style.position = "relative"
label.style.width = "100%"
- const svg = ``
- label.innerHTML = `${optSummary.id}${svg}
${optSummary.oneliner}`
+ const optionName = document.createElement("span")
+ optionName.textContent = optSummary.id
+ label.appendChild(optionName)
+
+ const optionReference = document.createElement("a")
+ optionReference.href = `../tsconfig#${optSummary.id}`
+ optionReference.className = "compiler_info_link"
+ optionReference.setAttribute("aria-label", `Look up ${optSummary.id} in the TSConfig Reference`)
+ optionReference.target = "_blank"
+ optionReference.rel = "noopener noreferrer"
+ optionReference.appendChild(createInfoIcon())
+ label.appendChild(optionReference)
+
+ label.appendChild(document.createElement("br"))
+ label.appendChild(document.createTextNode(optSummary.oneliner))
const input = document.createElement("input")
input.value = optSummary.id
@@ -244,7 +286,7 @@ const createSelect = (title: string, id: string, blurb: string, sandbox: Sandbox
})
const span = document.createElement("span")
- span.innerHTML = blurb
+ span.textContent = blurb
span.classList.add("compiler-flag-blurb")
label.appendChild(span)
diff --git a/packages/playground/src/createElements.ts b/packages/playground/src/createElements.ts
index 5cee7a81b28e..3569a973314e 100644
--- a/packages/playground/src/createElements.ts
+++ b/packages/playground/src/createElements.ts
@@ -119,8 +119,8 @@ export const createSidebar = () => {
return sidebar
}
-const toggleIconWhenOpen = "⇥"
-const toggleIconWhenClosed = "⇤"
+const toggleIconWhenOpen = "\u21E5"
+const toggleIconWhenClosed = "\u21E4"
export const setupSidebarToggle = () => {
const toggle = document.getElementById("sidebar-toggle")!
@@ -129,7 +129,7 @@ export const setupSidebarToggle = () => {
const sidebar = window.document.querySelector(".playground-sidebar") as HTMLDivElement
const sidebarShowing = sidebar.style.display !== "none"
- toggle.innerHTML = sidebarShowing ? toggleIconWhenOpen : toggleIconWhenClosed
+ toggle.textContent = sidebarShowing ? toggleIconWhenOpen : toggleIconWhenClosed
toggle.setAttribute("aria-label", sidebarShowing ? "Hide Sidebar" : "Show Sidebar")
}
diff --git a/packages/playground/src/ds/createDesignSystem.ts b/packages/playground/src/ds/createDesignSystem.ts
index a4089e42b54a..f8338072586f 100644
--- a/packages/playground/src/ds/createDesignSystem.ts
+++ b/packages/playground/src/ds/createDesignSystem.ts
@@ -1,5 +1,5 @@
import type { Sandbox } from "@typescript/sandbox"
-import type { DiagnosticRelatedInformation, Node } from "typescript"
+import type { DiagnosticRelatedInformation, Node as TSNode } from "typescript"
export type LocalStorageOption = {
blurb: string
@@ -17,9 +17,24 @@ export type OptionsListConfig = {
requireRestart?: true
}
+type ElementChild = string | Node
+
+const appendChildren = (el: Element, children: ElementChild[]) => {
+ children.forEach(child => {
+ el.appendChild(typeof child === "string" ? document.createTextNode(child) : child)
+ })
+}
+
const el = (str: string, elementType: string, container: Element) => {
const el = document.createElement(elementType)
- el.innerHTML = str
+ el.textContent = str
+ container.appendChild(el)
+ return el
+}
+
+const elWithChildren = (children: ElementChild[], elementType: string, container: Element) => {
+ const el = document.createElement(elementType)
+ appendChildren(el, children)
container.appendChild(el)
return el
}
@@ -102,8 +117,11 @@ export const createDesignSystem = (sandbox: Sandbox) => {
const li = document.createElement("li")
const label = document.createElement("label")
- const split = setting.oneline ? "" : "
"
- label.innerHTML = `${setting.display}${split}${setting.blurb}`
+ const display = document.createElement("span")
+ display.textContent = setting.display
+ label.appendChild(display)
+ if (!setting.oneline) label.appendChild(document.createElement("br"))
+ label.appendChild(document.createTextNode(setting.blurb))
const key = setting.flag
const input = document.createElement("input")
@@ -187,6 +205,32 @@ export const createDesignSystem = (sandbox: Sandbox) => {
return noErrorsMessage
}
+ const link = (href: string, text: string) => {
+ const a = document.createElement("a")
+ a.href = href
+ a.textContent = text
+ return a
+ }
+
+ const inlineCode = (text: string) => {
+ const code = document.createElement("code")
+ code.textContent = text
+ return code
+ }
+
+ const lineBreak = () => document.createElement("br")
+
+ const unorderedList = (...items: ElementChild[][]) => {
+ const ul = document.createElement("ul")
+ items.forEach(item => {
+ const li = document.createElement("li")
+ appendChildren(li, item)
+ ul.appendChild(li)
+ })
+ container.appendChild(ul)
+ return ul
+ }
+
const createTabBar = () => {
const tabBar = document.createElement("div")
tabBar.classList.add("playground-plugin-tabview")
@@ -305,13 +349,13 @@ export const createDesignSystem = (sandbox: Sandbox) => {
container.appendChild(ol)
}
- const createASTTree = (node: Node, settings?: { closedByDefault?: true }) => {
+ const createASTTree = (node: TSNode, settings?: { closedByDefault?: true }) => {
const autoOpen = !settings || !settings.closedByDefault
const div = document.createElement("div")
div.className = "ast"
- const infoForNode = (node: Node) => {
+ const infoForNode = (node: TSNode) => {
const name = ts.SyntaxKind[node.kind]
return {
@@ -337,20 +381,21 @@ export const createDesignSystem = (sandbox: Sandbox) => {
return li
}
- const renderSingleChild = (key: string, value: Node, depth: number) => {
+ const renderSingleChild = (key: string, value: TSNode, depth: number) => {
const li = document.createElement("li")
- li.innerHTML = `${key}: `
+ li.textContent = `${key}: `
renderItem(li, value, depth + 1)
return li
}
- const renderManyChildren = (key: string, nodes: Node[], depth: number) => {
+ const renderManyChildren = (key: string, nodes: TSNode[], depth: number) => {
const children = document.createElement("div")
children.classList.add("ast-children")
const li = document.createElement("li")
- li.innerHTML = `${key}: [
`
+ li.textContent = `${key}: [`
+ li.appendChild(document.createElement("br"))
children.appendChild(li)
nodes.forEach(node => {
@@ -358,12 +403,12 @@ export const createDesignSystem = (sandbox: Sandbox) => {
})
const liEnd = document.createElement("li")
- liEnd.innerHTML += "]"
+ liEnd.textContent = "]"
children.appendChild(liEnd)
return children
}
- const renderItem = (parentElement: Element, node: Node, depth: number) => {
+ const renderItem = (parentElement: Element, node: TSNode, depth: number) => {
const itemDiv = document.createElement("div")
parentElement.appendChild(itemDiv)
itemDiv.className = "ast-tree-start"
@@ -497,6 +542,18 @@ export const createDesignSystem = (sandbox: Sandbox) => {
subtitle: (subtitle: string) => el(subtitle, "h4", container),
/** Used to show a paragraph */
p: (subtitle: string) => el(subtitle, "p", container),
+ /** Used to show a paragraph with safe DOM children */
+ pWithChildren: (...children: ElementChild[]) => elWithChildren(children, "p", container),
+ /** Used to show a section heading with safe DOM children */
+ subtitleWithChildren: (...children: ElementChild[]) => elWithChildren(children, "h4", container),
+ /** Creates an unattached anchor with safe text */
+ link,
+ /** Creates an unattached inline code element with safe text */
+ inlineCode,
+ /** Creates an unattached line break */
+ lineBreak,
+ /** Appends an unordered list with safe DOM children */
+ unorderedList,
/** When you can't do something, or have nothing to show */
showEmptyScreen,
/**
diff --git a/packages/playground/src/navigation.ts b/packages/playground/src/navigation.ts
index 2c1aece35dae..944713a417b7 100644
--- a/packages/playground/src/navigation.ts
+++ b/packages/playground/src/navigation.ts
@@ -6,6 +6,45 @@ type StoryContent =
import type { Sandbox } from "@typescript/sandbox"
+const createStoryIcon = (type: "code" | "html") => {
+ const svgNamespace = "http://www.w3.org/2000/svg"
+ const svg = document.createElementNS(svgNamespace, "svg")
+ svg.setAttribute("fill", "none")
+
+ if (type === "code") {
+ svg.setAttribute("width", "7")
+ svg.setAttribute("height", "7")
+ svg.setAttribute("viewBox", "0 0 7 7")
+
+ const rect = document.createElementNS(svgNamespace, "rect")
+ rect.setAttribute("width", "7")
+ rect.setAttribute("height", "7")
+ rect.setAttribute("fill", "#187ABF")
+ svg.appendChild(rect)
+ } else {
+ svg.setAttribute("width", "9")
+ svg.setAttribute("height", "11")
+ svg.setAttribute("viewBox", "0 0 9 11")
+
+ const path = document.createElementNS(svgNamespace, "path")
+ path.setAttribute("d", "M8 5.5V3.25L6 1H4M8 5.5V10H1V1H4M8 5.5H4V1")
+ path.setAttribute("stroke", "#C4C4C4")
+ svg.appendChild(path)
+ }
+
+ return svg
+}
+
+const createLocalDevStoryMessage = () => {
+ const p = document.createElement("p")
+ p.appendChild(document.createTextNode("Because the gatsby dev server uses JS to build your pages, and not statically, the page will not load during dev. It does work in prod though - use "))
+ const code = document.createElement("code")
+ code.textContent = "pnpm build-site"
+ p.appendChild(code)
+ p.appendChild(document.createTextNode(" to test locally with a static build."))
+ return p
+}
+
/** Use the handbook TOC which is injected into the globals to create a sidebar */
export const showNavForHandbook = (sandbox: Sandbox, escapeFunction: () => void) => {
// @ts-ignore
@@ -73,16 +112,10 @@ const updateNavWithStoryContent = (title: string, storyContent: StoryContent[],
li.classList.add("selectable")
const a = document.createElement("a")
- let logo: string
- if (element.type === "code") {
- logo = ``
- } else if (element.type === "html") {
- logo = ``
- } else {
- logo = ""
+ if (element.type === "code" || element.type === "html") {
+ a.appendChild(createStoryIcon(element.type))
}
-
- a.innerHTML = `${logo}${element.title}`
+ a.appendChild(document.createTextNode(element.title))
a.href = `/play#${prefix}-${i}`
a.onclick = e => {
@@ -164,12 +197,14 @@ const setStoryViaHref = (href: string, sandbox: Sandbox) => {
}
if (document.location.host === "localhost:8000") {
- setStory("
Because the gatsby dev server uses JS to build your pages, and not statically, the page will not load during dev. It does work in prod though - use pnpm build-site to test locally with a static build.
Failed to load the content at ${href}. Reason: ${req.status} ${req.statusText}
`, sandbox) + const errorMessage = document.createElement("p") + errorMessage.textContent = `Failed to load the content at ${href}. Reason: ${req.status} ${req.statusText}` + setStory(errorMessage, sandbox) } }) } diff --git a/packages/playground/src/pluginUtils.ts b/packages/playground/src/pluginUtils.ts index 0a5aa06fbe54..22db5fcb4dcc 100644 --- a/packages/playground/src/pluginUtils.ts +++ b/packages/playground/src/pluginUtils.ts @@ -15,7 +15,7 @@ export const createUtils = (sb: any, react: typeof React) => { const el = (str: string, elementType: string, container: Element) => { const el = document.createElement(elementType) - el.innerHTML = str + el.textContent = str container.appendChild(el) return el } diff --git a/packages/playground/src/sidebar/plugins.ts b/packages/playground/src/sidebar/plugins.ts index 755feaeeec12..67729ce5c855 100644 --- a/packages/playground/src/sidebar/plugins.ts +++ b/packages/playground/src/sidebar/plugins.ts @@ -123,15 +123,33 @@ export const optionsPlugin: PluginFactory = (i, utils) => { const label = document.createElement("label") - // Avoid XSS by someone injecting JS via the description, which is the only free text someone can use - var p = document.createElement("p") - p.appendChild(document.createTextNode(plugin.description)) - const escapedDescription = p.innerHTML - - const top = `${plugin.name} by ${plugin.author}// ^? to highlight how inference gives different results at different locations",
+ blurb: (ds: DesignSystem) =>
+ ds.pWithChildren(
+ "Using ",
+ ds.inlineCode("// ^?"),
+ " to highlight how inference gives different results at different locations"
+ ),
code: `// @noImplicitAny: false
type Entity = {
@@ -44,21 +50,33 @@ const reference: {
content: (
sandbox: Sandbox,
container: HTMLDivElement,
- ds: ReturnType// @[option] comments inside the sample.
-// @strict: true or // @strict: false.: true to get the same behavior.// @target: ES2015// @target: 4// @types: ['jest']// @[option] comments inside the sam
const skip = ["Project_Files_0", "Watch_Options_999"]
if (skip.includes(opt.categoryID)) return
- ds.p(`// @${opt.id}// @filename: [path]."
+ ds.pWithChildren(
+ "The code file can be converted into multiple files behind the scenes. This is done by chopping the code sample whenever there is a ",
+ ds.inlineCode("// @filename: [path]"),
+ "."
)
ds.code(
@@ -164,11 +184,14 @@ document.body.appendChild(button);
{
name: "Emitter",
content: (sandbox, container, ds) => {
- ds.p(
- `
-There are ways to have your test repro be about the output of running TypeScript. There are two comment types which can be used to highlight these files.
-// @showEmit is a shortcut for showing the .js file for a single file code sample:
-`.trim()
+ ds.pWithChildren(
+ "There are ways to have your test repro be about the output of running TypeScript. There are two comment types which can be used to highlight these files.",
+ ds.lineBreak(),
+ ds.lineBreak(),
+ ds.inlineCode("// @showEmit"),
+ " is a shortcut for showing the ",
+ ds.inlineCode(".js"),
+ " file for a single file code sample:"
)
ds.code(
`
@@ -176,8 +199,10 @@ There are ways to have your test repro be about the output of running TypeScript
export const helloWorld: string = "Hi"
`.trim()
)
- ds.p(
- `The long-form is // @showEmittedFile: [filename] which allows for showing any emitted file`
+ ds.pWithChildren(
+ "The long-form is ",
+ ds.inlineCode("// @showEmittedFile: [filename]"),
+ " which allows for showing any emitted file"
)
ds.code(
`
@@ -220,8 +245,10 @@ const abc = ""
}
`)
})
- ds.p(
- "You may need to undo strict for some samples, but the others shouldn't affect most code repros."
+ ds.pWithChildren(
+ "You may need to undo ",
+ ds.inlineCode("strict"),
+ " for some samples, but the others shouldn't affect most code repros."
)
},
},
@@ -232,9 +259,12 @@ const abc = ""
"Note: this section is tricky to document... These bugs may have been fixed since the docs were created. Consider theses as ideas in how to make repros rather than useful bug reproductions."
)
examples.forEach(e => {
- // prettier-ignore
- ds.subtitle(e.name + ` ${e.issue}`)
- ds.p(e.blurb)
+ ds.subtitleWithChildren(
+ e.name,
+ " ",
+ ds.link(`https://github.com/microsoft/TypeScript/issues/${e.issue}`, String(e.issue))
+ )
+ e.blurb(ds)
const button = document.createElement("button")
button.textContent = "Show example"
button.onclick = () => sandbox.setText(e.code)
diff --git a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx
index 697abff95a3b..56123f46859d 100755
--- a/packages/typescriptlang-org/src/pages/dev/twoslash.tsx
+++ b/packages/typescriptlang-org/src/pages/dev/twoslash.tsx
@@ -127,8 +127,6 @@ const Index: React.FC${newContent}
` + textSectionNav.textContent = "" + const thanksHeader = document.createElement("h5") + thanksHeader.textContent = newContent + textSectionNav.appendChild(thanksHeader) + + popoverPopup.textContent = "" + const thanksParagraph = document.createElement("p") + thanksParagraph.textContent = newContent + popoverPopup.appendChild(thanksParagraph) } likeButton.onclick = clicked("Liked Page")