diff --git a/packages/code-block/partials/code-lines/style.module.css b/packages/code-block/partials/code-lines/code-lines.module.css
similarity index 82%
rename from packages/code-block/partials/code-lines/style.module.css
rename to packages/code-block/partials/code-lines/code-lines.module.css
index 875e10bb6..a3611cc08 100644
--- a/packages/code-block/partials/code-lines/style.module.css
+++ b/packages/code-block/partials/code-lines/code-lines.module.css
@@ -9,6 +9,12 @@ code-block/theme-(dark|light).module.css
to be present.
*/
+/*
+
+SHARED
+
+*/
+
pre.pre {
--code-padding: 16px;
--code-font-size: 0.84375rem; /* 13.5 px at default text size */
@@ -32,6 +38,83 @@ pre.pre {
overflow: hidden;
}
+.LineNumber {
+ composes: g-type-code from global;
+ display: block;
+ font-size: var(--code-font-size);
+ line-height: var(--code-line-height);
+ white-space: pre;
+ padding: 0 var(--code-padding);
+ color: var(--text-color-faded);
+
+ &.wrap {
+ border-right: 1px solid var(--divider-line-color);
+ }
+
+ &.highlight {
+ background: var(--background-highlighted-line);
+ color: var(--text-color-base);
+ }
+
+ &.dim > * {
+ color: var(--text-color-faded);
+ }
+}
+
+.LineOfCode {
+ composes: g-type-code from global;
+ display: block;
+ color: var(--text-color-base);
+ padding-right: calc(var(--code-padding) * 2);
+ padding-left: var(--code-padding);
+ font-size: var(--code-font-size);
+ line-height: var(--code-line-height);
+ min-width: max-content;
+ white-space: pre;
+
+ &.wrap {
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ min-width: 0;
+ width: 100%;
+ }
+
+ &.padRight {
+ /* Adds right padding so that the floating copy button
+ does not obscure content at the end of the line */
+ padding-right: 96px;
+ }
+
+ &.highlight {
+ background: var(--background-highlighted-line);
+ }
+
+ &.dim {
+ opacity: 0.7;
+
+ &,
+ & * {
+ /* !important is necessary here to override
+ syntax highlight color styles, which are
+ globally scoped by necessity */
+ color: var(--text-color-faded) !important;
+ }
+ }
+}
+
+/*
+
+SCROLL OVERFLOW LAYOUT
+
+*/
+
+.numbersColumn {
+ display: block;
+ border-right: 1px solid var(--divider-line-color);
+ flex-shrink: 0;
+ padding: var(--code-padding) 0;
+}
+
.linesColumn {
display: block;
flex-grow: 1;
@@ -76,7 +159,7 @@ pre.pre {
}
}
-.linesWrapper {
+.linesScrollContainer {
display: flex;
width: max-content;
min-width: 100%;
@@ -84,62 +167,28 @@ pre.pre {
flex-shrink: 0;
}
-.numbersColumn {
- display: block;
- border-right: 1px solid var(--divider-line-color);
- flex-shrink: 0;
- padding: var(--code-padding) 0;
-}
+/*
-.LineNumber {
- composes: g-type-code from global;
- display: block;
- font-size: var(--code-font-size);
- line-height: var(--code-line-height);
- white-space: pre;
- padding: 0 var(--code-padding);
- color: var(--text-color-faded);
+WRAP LAYOUT
- &.isHighlighted {
- background: var(--background-highlighted-line);
- color: var(--text-color-base);
- }
+*/
- &.isNotHighlighted > * {
- color: var(--text-color-faded);
- }
+.wrappedLinesContainer {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ flex-shrink: 0;
}
-.LineOfCode {
- composes: g-type-code from global;
- display: block;
- color: var(--text-color-base);
- padding-right: calc(var(--code-padding) * 2);
- padding-left: var(--code-padding);
- font-size: var(--code-font-size);
- line-height: var(--code-line-height);
- min-width: max-content;
- white-space: pre;
-
- &.hasFloatingCopyButton {
- /* Adds right padding so that the floating copy button
- does not obscure content at the end of the line */
- padding-right: 96px;
- }
-
- &.isHighlighted {
- background: var(--background-highlighted-line);
- }
-
- &.isNotHighlighted {
- opacity: 0.7;
+.wrappedLine {
+ display: flex;
+ flex-wrap: nowrap;
+}
- &,
- & * {
- /* !important is necessary here to override
- syntax highlight color styles, which are
- globally scoped by necessity */
- color: var(--text-color-faded) !important;
- }
- }
+/* adds space at the top and bottom of the block,
+ while supporting a continuous border for line numbers */
+.wrappedLinesSpacer {
+ display: flex;
+ flex-wrap: nowrap;
+ height: var(--code-padding);
}
diff --git a/packages/code-block/partials/code-lines/index.js b/packages/code-block/partials/code-lines/index.js
deleted file mode 100644
index c9c80aa1e..000000000
--- a/packages/code-block/partials/code-lines/index.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: MPL-2.0
- */
-
-import React, { useMemo } from 'react'
-import parseHighlightedLines from '../../utils/parse-highlighted-lines'
-import splitJsxIntoLines from './utils/split-jsx-into-lines'
-import splitHtmlIntoLines from './utils/split-html-into-lines'
-import classNames from 'classnames'
-import s from './style.module.css'
-
-function CodeLines({
- code,
- language,
- lineNumbers,
- highlight,
- hasFloatingCopyButton,
-}) {
- const linesOfCode = useMemo(() => {
- const isHtmlString = typeof code === 'string'
- return isHtmlString ? splitHtmlIntoLines(code) : splitJsxIntoLines(code)
- }, [code])
-
- const lineCount = linesOfCode.length
- const highlightedLines = parseHighlightedLines(highlight)
-
- return (
-
-
- {lineNumbers ? (
-
- {linesOfCode.map((_lineChildren, stableIdx) => {
- const number = stableIdx + 1
- const isHighlighted = highlightedLines.indexOf(number) !== -1
- const isNotHighlighted =
- highlightedLines.length > 0 && !isHighlighted
- return (
-
- )
- })}
-
- ) : null}
-
-
- {linesOfCode.map((lineChildren, stableIdx) => {
- const number = stableIdx + 1
- const isHighlighted = highlightedLines.indexOf(number) !== -1
- const isNotHighlighted = highlightedLines.length && !isHighlighted
- return (
-
- {lineChildren}
- {'\n'}
-
- )
- })}
-
-
-
-
- )
-}
-
-function LineNumber({ number, isHighlighted, isNotHighlighted, lineCount }) {
- const padLevel = Math.max(lineCount.toString().length, 1)
- const paddedNumber = number.toString().padEnd(padLevel)
- return (
-
- )
-}
-
-function LineOfCode({
- children,
- isHighlighted,
- isNotHighlighted,
- hasFloatingCopyButton,
-}) {
- return (
-
- {children}
-
- )
-}
-
-export default CodeLines
diff --git a/packages/code-block/partials/code-lines/index.tsx b/packages/code-block/partials/code-lines/index.tsx
new file mode 100644
index 000000000..a31a6df5b
--- /dev/null
+++ b/packages/code-block/partials/code-lines/index.tsx
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+/**
+ * Note: lines of code are expected to be stable. If we need to work with
+ * dynamic code blocks in the future, we could assign random unique IDs
+ * to each line during the `linesOfCode` `useMemo` function.
+ *
+ * For now, we disable react/no-array-index key for the entire file.
+ */
+/* eslint-disable react/no-array-index-key */
+
+import React, { PropsWithChildren, useMemo } from 'react'
+import classNames from 'classnames'
+import parseHighlightedLines from '../../utils/parse-highlighted-lines'
+import splitJsxIntoLines from './utils/split-jsx-into-lines'
+import splitHtmlIntoLines from './utils/split-html-into-lines'
+import s from './code-lines.module.css'
+
+interface CodeLinesProps {
+ code: string
+ language: string
+ lineNumbers?: boolean
+ highlight?: string
+ hasFloatingCopyButton?: boolean
+ wrapCode?: boolean
+}
+
+/**
+ * Render the provided code into separate line elements,
+ * accounting for all provided options.
+ */
+function CodeLines({
+ code,
+ language,
+ lineNumbers,
+ highlight,
+ hasFloatingCopyButton,
+ wrapCode,
+}: CodeLinesProps) {
+ // Parse out an array of integers representing which lines to highlight
+ const highlightedLines = parseHighlightedLines(highlight) as number[]
+
+ /**
+ * Split the incoming code into lines.
+ * We need to do this in order to render each line of code in a
+ * separate element, which is necessary for features such as highlighting
+ * specific lines and allowing code to wrap.
+ */
+ const linesOfCode = useMemo(() => {
+ const isHtmlString = typeof code === 'string'
+ const lineElements = isHtmlString
+ ? splitHtmlIntoLines(code)
+ : splitJsxIntoLines(code)
+ const lineCount = lineElements.length
+ const padLevel = Math.max(lineCount.toString().length, 1)
+ return lineElements.map((children, index) => {
+ const number = index + 1
+ const numberPadded = number.toString().padEnd(padLevel)
+ const highlight = highlightedLines.indexOf(number) !== -1
+ const dim = highlightedLines.length > 0 && !highlight
+ return { children, number: numberPadded, highlight, dim }
+ })
+ }, [code, highlightedLines])
+
+ // When rendering wrapped code with line numbers shown, we need a spacer value
+ // that matches the padding inset of all other line numbers
+ let numberSpacer: string | null = null
+ if (lineNumbers) {
+ const padLevel = Math.max(linesOfCode.length.toString().length, 1)
+ numberSpacer = ''.padEnd(padLevel)
+ }
+
+ // When the floating copy button is present, we add padding to many lines
+ const padRight = Boolean(hasFloatingCopyButton)
+
+ if (wrapCode) {
+ /**
+ * For wrapped code, we use a single-column flex layout.
+ * Lines of code are stacked in a single container, and each line row renders
+ * its own line number, which ensures that when lines wrap, the line numbers
+ * are aligned as expected
+ */
+ return (
+
+
+
+ {linesOfCode.map(({ number, children, highlight, dim }, idx) => (
+
+ {lineNumbers ? (
+
+ ) : null}
+
+ {children}
+ {'\n'}
+
+
+ ))}
+
+
+
+ )
+ } else {
+ /**
+ * For overflowing code, we use a two-column layout.
+ * The first column contains line numbers, and is effectively fixed.
+ * The second column contains the lines themselves, and is an overflow
+ * container to allow extra long lines to scroll as needed.
+ */
+ return (
+
+ {lineNumbers ? (
+
+ {linesOfCode.map(({ number, highlight, dim }, idx) => (
+
+ ))}
+
+ ) : null}
+
+
+ {linesOfCode.map(({ children, highlight, dim }, idx) => (
+
+ {children}
+ {'\n'}
+
+ ))}
+
+
+
+ )
+ }
+}
+
+/**
+ * Set up the `
` + `` container
+ * which is necessary for language-specific syntax highlighting styles
+ */
+function PreCode({
+ children,
+ language,
+}: PropsWithChildren<{ language: string }>) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+/**
+ * Provides "padding" at the top and bottom of a code block with wrapping lines
+ * while retaining the "numbers" and "lines" separation border.
+ *
+ * For context, with wrapped code, we don't have separate "numbers" and "lines"
+ * columns as we would with overflowing code. So, we can't add padding
+ * to those columns as we do with overflowing code.
+ *
+ * To create padding-equivalent space, while also rendering a continuous border
+ * between the "numbers" and "lines" columns, we use this component,
+ * which is essentially and empty line of code that's been shortened a bit.
+ */
+function WrappedLinesSpacer({
+ number,
+ padRight,
+}: {
+ number: string | null
+ padRight: boolean
+}) {
+ return (
+
+ {number ? : null}
+ {'\n'}
+
+ )
+}
+
+/**
+ * Renders a line number.
+ *
+ * Note the `number` is rendered in monospace in a whitespace-sensitive way,
+ * so that if a padded string is passed, we can allow for table-like alignment
+ * of line numbers, and consistent horizontal width of numbers across all lines.
+ */
+function LineNumber({
+ number,
+ highlight,
+ dim,
+ wrap,
+}: {
+ number: number | string
+ highlight?: boolean
+ dim?: boolean
+ wrap?: boolean
+}) {
+ return (
+
+ {number}
+
+ )
+}
+
+/**
+ * Renders a line of code
+ */
+function LineOfCode({
+ children,
+ highlight,
+ dim,
+ padRight,
+ wrap,
+}: PropsWithChildren<{
+ highlight?: boolean
+ dim?: boolean
+ padRight?: boolean
+ wrap?: boolean
+}>) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default CodeLines