Skip to content

Commit

Permalink
✨ (grapher) allow line breaks after hyphens (#3689)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann authored Jun 10, 2024
1 parent 23f14e3 commit 0ba25b7
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 23 deletions.
51 changes: 28 additions & 23 deletions packages/@ourworldindata/components/src/TextWrap/TextWrap.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import {
isEmpty,
max,
stripHTML,
Bounds,
FontFamily,
} from "@ourworldindata/utils"
import { max, stripHTML, Bounds, FontFamily } from "@ourworldindata/utils"
import { computed } from "mobx"
import React from "react"
import { Fragment, joinFragments, splitIntoFragments } from "./TextWrapUtils"

declare type FontSize = number

Expand All @@ -16,6 +11,7 @@ interface TextWrapProps {
lineHeight?: number
fontSize: FontSize
fontWeight?: number
separators?: string[]
rawHtml?: boolean
}

Expand Down Expand Up @@ -81,6 +77,9 @@ export class TextWrap {
@computed get text(): string {
return this.props.text
}
@computed get separators(): string[] {
return this.props.separators ?? [" "]
}

// We need to take care that HTML tags are not split across lines.
// Instead, we want every line to have opening and closing tags for all tags that appear.
Expand Down Expand Up @@ -127,45 +126,51 @@ export class TextWrap {
}

@computed get lines(): WrapLine[] {
const { text, maxWidth, fontSize, fontWeight } = this
const { text, separators, maxWidth, fontSize, fontWeight } = this

const words = isEmpty(text)
? []
: // Prepend spaces so that the string is also split before newline characters
// See startsWithNewline
text.replace(/\n/g, " \n").split(" ")
// Prepend spaces so that the string is also split before newline characters
// See startsWithNewline
const fragments = splitIntoFragments(
text.replace(/\n/g, " \n"),
separators
)

const lines: WrapLine[] = []

let line: string[] = []
let line: Fragment[] = []
let lineBounds = Bounds.empty()

words.forEach((word) => {
const nextLine = line.concat([word])
fragments.forEach((fragment) => {
const nextLine = line.concat([fragment])

// Strip HTML if a raw string is passed
const text = this.props.rawHtml
? stripHTML(nextLine.join(" "))
: nextLine.join(" ")
? stripHTML(joinFragments(nextLine))
: joinFragments(nextLine)

const nextBounds = Bounds.forText(text, {
fontSize,
fontWeight,
})

if (
startsWithNewline(word) ||
startsWithNewline(fragment.text) ||
(nextBounds.width + 10 > maxWidth && line.length >= 1)
) {
// Introduce a newline _before_ this word
lines.push({
text: line.join(" "),
text: joinFragments(line),
width: lineBounds.width,
height: lineBounds.height,
})
// ... and start a new line with this word (with a potential leading newline stripped)
const wordWithoutNewline = word.replace(/^\n/, "")
line = [wordWithoutNewline]
const wordWithoutNewline = fragment.text.replace(/^\n/, "")
line = [
{
text: wordWithoutNewline,
separator: fragment.separator,
},
]
lineBounds = Bounds.forText(wordWithoutNewline, {
fontSize,
fontWeight,
Expand All @@ -179,7 +184,7 @@ export class TextWrap {
// Push the last line
if (line.length > 0)
lines.push({
text: line.join(" "),
text: joinFragments(line),
width: lineBounds.width,
height: lineBounds.height,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#! /usr/bin/env jest

import { joinFragments, splitIntoFragments } from "./TextWrapUtils"

it("splits text correctly into fragments", () => {
expect(splitIntoFragments("")).toEqual([])
expect(splitIntoFragments("word")).toEqual([
{ text: "word", separator: "" },
])
expect(splitIntoFragments("an example line")).toEqual([
{ text: "an", separator: " " },
{ text: "example", separator: " " },
{ text: "line", separator: "" },
])
expect(splitIntoFragments("high-income countries")).toEqual([
{ text: "high-income", separator: " " },
{ text: "countries", separator: "" },
])
expect(splitIntoFragments("high-income countries", [" ", "-"])).toEqual([
{ text: "high", separator: "-" },
{ text: "income", separator: " " },
{ text: "countries", separator: "" },
])
})

it("splits and joins text correctly into fragments", () => {
const examples = [
"",
"word",
"an example line",
"an example spaced out text",
"an example-with-hyphens",
"an example - with - a - differennt - kind-of - hyphen",
"hyphen at the end -",
"a mixed-bag - ok",
]
for (const text of examples) {
expect(joinFragments(splitIntoFragments(text))).toEqual(text)
expect(joinFragments(splitIntoFragments(text, [" ", "-"]))).toEqual(
text
)
}
})
32 changes: 32 additions & 0 deletions packages/@ourworldindata/components/src/TextWrap/TextWrapUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { isEmpty } from "@ourworldindata/utils"

export type Fragment = {
text: string
separator: string
}

export function splitIntoFragments(
text: string,
separators = [" "]
): Fragment[] {
if (isEmpty(text)) return []
const fragments: Fragment[] = []
let currText = ""
for (const char of text) {
if (separators.includes(char)) {
fragments.push({ text: currText, separator: char })
currText = ""
} else {
currText += char
}
}
fragments.push({ text: currText, separator: "" })
return fragments
}

export function joinFragments(fragments: Fragment[]): string {
return fragments
.map(({ text, separator }) => text + separator)
.join("")
.trim()
}
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ class LabelledSlopes
...valueLabelProps,
maxWidth,
fontWeight: 700,
separators: [" ", "-"],
}
const leftEntityLabel = new TextWrap({
text,
Expand Down

0 comments on commit 0ba25b7

Please sign in to comment.