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 9, 2025
1 parent d95a8ab commit 54f3e88
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 603 deletions.
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
}
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 54f3e88

Please sign in to comment.