Skip to content

Commit

Permalink
🔨 use markdown for rendering grouped text wraps
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Jan 21, 2025
1 parent ce47bab commit 17ad549
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 625 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ const RenderCurrentAndLegacy = ({
}}
/>
<g style={{ fill: RED, opacity: 0.75 }}>
{legacy.render(0, 0)}
{legacy.renderSVG(0, 0)}
</g>
<g style={{ fill: GREEN, opacity: 0.75 }}>
{current.renderSVG(0, 0)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,87 @@ describe("MarkdownTextWrap", () => {
})
})
})

describe("fromFragments", () => {
const fontSize = 14

it("should place fragments in-line by default", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
textWrapProps: {
maxWidth: 500,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(1)
expect(textWrap.htmlLines.length).toEqual(1)
})

it("should place the secondary text in a new line if requested", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "always",
textWrapProps: {
maxWidth: 1000,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})

it("should place the secondary text in a new line if line breaks should be avoided", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "avoid-wrap",
textWrapProps: {
maxWidth: 250,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})

it("should place the secondary text in the same line if possible", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Lower middle-income countries" },
secondary: { text: "30 million" },
newLine: "avoid-wrap",
textWrapProps: {
maxWidth: 1000,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(1)
expect(textWrap.htmlLines.length).toEqual(1)
})

it("should use all available space when one fragment exceeds the given max width", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "Long-word-that-can't-be-broken-up more words" },
secondary: { text: "30 million" },
textWrapProps: {
maxWidth: 150,
fontSize,
},
})
expect(textWrap.width).toBeGreaterThan(150)
})

it("should place very long words in a separate line", () => {
const textWrap = MarkdownTextWrap.fromFragments({
main: { text: "30 million" },
secondary: { text: "Long-word-that-can't-be-broken-up" },
textWrapProps: {
maxWidth: 150,
fontSize,
},
})
expect(textWrap.svgLines.length).toEqual(2)
expect(textWrap.htmlLines.length).toEqual(2)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -507,18 +507,83 @@ export const sumTextWrapHeights = (
sum(elements.map((element) => element.height)) +
(elements.length - 1) * spacer

type MarkdownTextWrapProps = {
text: string
fontSize: number
type MarkdownTextWrapOptions = {
maxWidth?: number
fontFamily?: FontFamily
fontSize: number
fontWeight?: number
lineHeight?: number
maxWidth?: number
style?: CSSProperties
detailsOrderedByReference?: string[]
}

type MarkdownTextWrapProps = { text: string } & MarkdownTextWrapOptions

type TextFragment = { text: string; bold?: boolean }

export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
static fromFragments({
main,
secondary,
newLine = "continue-line",
textWrapProps,
}: {
main: TextFragment
secondary: TextFragment
newLine?: "continue-line" | "always" | "avoid-wrap"
textWrapProps: Omit<MarkdownTextWrapOptions, "fontWeight">
}) {
const mainMarkdownText = maybeBoldMarkdownText(main)
const secondaryMarkdownText = maybeBoldMarkdownText(secondary)

const combinedTextContinued = [
mainMarkdownText,
secondaryMarkdownText,
].join(" ")
const combinedTextNewLine = [
mainMarkdownText,
secondaryMarkdownText,
].join("\n")

if (newLine === "always") {
return new MarkdownTextWrap({
text: combinedTextNewLine,
...textWrapProps,
})
}

if (newLine === "continue-line") {
return new MarkdownTextWrap({
text: combinedTextContinued,
...textWrapProps,
})
}

// if newLine is set to 'avoid-wrap', we first try to fit the secondary text
// on the same line as the main text. If it doesn't fit, we place it on a new line.

const mainTextWrap = new MarkdownTextWrap({ ...main, ...textWrapProps })
const secondaryTextWrap = new MarkdownTextWrap({
text: secondaryMarkdownText,
...textWrapProps,
maxWidth: mainTextWrap.maxWidth - mainTextWrap.lastLineWidth,
})

const secondaryTextFitsOnSameLine =
secondaryTextWrap.svgLines.length === 1
if (secondaryTextFitsOnSameLine) {
return new MarkdownTextWrap({
text: combinedTextContinued,
...textWrapProps,
})
} else {
return new MarkdownTextWrap({
text: combinedTextNewLine,
...textWrapProps,
})
}
}

@computed get maxWidth(): number {
return this.props.maxWidth ?? Infinity
}
Expand Down Expand Up @@ -602,10 +667,18 @@ export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
return max(lineLengths) ?? 0
}

@computed get singleLineHeight(): number {
return this.fontSize * this.lineHeight
}

@computed get lastLineWidth(): number {
return sumBy(last(this.htmlLines), (token) => token.width) ?? 0
}

@computed get height(): number {
const { htmlLines, lineHeight, fontSize } = this
const { htmlLines } = this
if (htmlLines.length === 0) return 0
return htmlLines.length * lineHeight * fontSize
return htmlLines.length * this.singleLineHeight
}

@computed get style(): any {
Expand Down Expand Up @@ -648,13 +721,13 @@ export class MarkdownTextWrap extends React.Component<MarkdownTextWrapProps> {
detailsMarker?: DetailsMarker
id?: string
} = {}
): React.ReactElement | null {
): React.ReactElement {
const { fontSize, lineHeight } = this
const lines =
detailsMarker === "superscript"
? this.svgLinesWithDodReferenceNumbers
: this.svgLines
if (lines.length === 0) return null
if (lines.length === 0) return <></>

// Magic number set through experimentation.
// The HTML and SVG renderers need to position lines identically.
Expand Down Expand Up @@ -1092,3 +1165,13 @@ function appendReferenceNumbers(

return appendedTokens
}

function maybeBoldMarkdownText({
text,
bold,
}: {
text: string
bold?: boolean
}): string {
return bold ? `**${text}**` : text
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const HTMLAndSVG = ({
></div>
<svg width={width} height={height} style={{ position: "absolute" }}>
<g style={{ fill: RED, opacity: 0.75 }}>
{textwrap.render(0, 0)}
{textwrap.renderSVG(0, 0)}
</g>
</svg>
<div
Expand Down
57 changes: 0 additions & 57 deletions packages/@ourworldindata/components/src/TextWrap/TextWrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,60 +145,3 @@ describe("lines()", () => {
])
})
})

describe("firstLineOffset", () => {
it("should offset the first line if requested", () => {
const text = "an example line"
const props = { text, maxWidth: 100, fontSize: FONT_SIZE }

const wrapWithoutOffset = new TextWrap(props)
const wrapWithOffset = new TextWrap({
...props,
firstLineOffset: 50,
})

expect(wrapWithoutOffset.lines.map((l) => l.text)).toEqual([
"an example",
"line",
])
expect(wrapWithOffset.lines.map((l) => l.text)).toEqual([
"an",
"example line",
])
})

it("should break into a new line even if the first line would end up being empty", () => {
const text = "a-very-long-word"
const props = { text, maxWidth: 100, fontSize: FONT_SIZE }

const wrapWithoutOffset = new TextWrap(props)
const wrapWithOffset = new TextWrap({
...props,
firstLineOffset: 50,
})

expect(wrapWithoutOffset.lines.map((l) => l.text)).toEqual([
"a-very-long-word",
])
expect(wrapWithOffset.lines.map((l) => l.text)).toEqual([
"",
"a-very-long-word",
])
})

it("should break into a new line if firstLineOffset > maxWidth", () => {
const text = "an example line"
const wrap = new TextWrap({
text,
maxWidth: 100,
fontSize: FONT_SIZE,
firstLineOffset: 150,
})

expect(wrap.lines.map((l) => l.text)).toEqual([
"",
"an example",
"line",
])
})
})
Loading

0 comments on commit 17ad549

Please sign in to comment.