From 77283a531aff28e3025a3c457fc45a379a9a069a Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Thu, 3 Sep 2026 08:18:11 +0100 Subject: [PATCH] refactor(templates): one place turns a printed contact into a followable one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule that a printed telephone number and the number a device dials are different strings was written three times — ContactUri in the CV presets, InvoiceUri in the invoice presets, ProposalUri in the proposal presets — and where the three overlapped they were byte-identical, down to the regex and its comment. They are now one helper, core.identity.ContactUri, beside the contact-block and link records it serves: tel, telLink, mailLink, webLink, channelLink. Twenty-three preset files call it and the three copies are gone. It is public because the three families are three packages and a helper shared between them cannot be package-private. It is not marked experimental, because the shape is not a guess: three independent implementations had already agreed on it. Every method answers null rather than throwing, which the proposal copy had already learned and the other two had not: a notice, a name or a line of prose can reach a channel field, DocumentLinkOptions refuses a string that is not a URI, and a contact line is not the place to find that out. Nothing renders differently. A link annotation is not ink, so every pixel baseline in the suite passes untouched — the reactor gate ran the visual tests and moved no baseline. Tests: the CV suite's ContactUriTest moves to the shared package and gains what the other two copies contributed — an address, a site with and without a scheme, a channel read from its own shape, a site carrying digits that is not mistaken for a number, and prose in a channel field losing its link rather than the page. Ten cases. Reactor gate green, 1238 tests. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 17 +++ knowledge/api/templates.json | 85 ++++++++++- knowledge/api/templates.md | 9 +- .../core/identity/ContactUriTest.java | 97 ++++++++++++ .../templates/cv/presets/ContactUriTest.java | 53 ------- .../templates/core/identity/ContactUri.java | 144 ++++++++++++++++++ .../templates/core/identity/package-info.java | 3 + .../templates/cv/presets/ContactUri.java | 56 ------- .../cv/presets/EngineeringResume.java | 2 + .../templates/cv/presets/Executive.java | 2 + .../templates/cv/presets/MintEditorial.java | 2 + .../templates/cv/presets/MonogramSidebar.java | 2 + .../document/templates/cv/presets/Panel.java | 2 + .../templates/cv/presets/SidebarPortrait.java | 2 + .../templates/cv/presets/TimelineMinimal.java | 2 + .../templates/invoice/presets/InvoiceUri.java | 87 ----------- .../invoice/presets/MerchantClosing.java | 9 +- .../invoice/presets/MerchantMasthead.java | 7 +- .../invoice/presets/MerchantSettlement.java | 3 +- .../invoice/presets/MeteredClosing.java | 9 +- .../invoice/presets/MeteredMasthead.java | 7 +- .../invoice/presets/MeteredSettlement.java | 3 +- .../invoice/presets/ObsidianCards.java | 3 +- .../invoice/presets/ObsidianClosing.java | 3 +- .../invoice/presets/ObsidianParties.java | 5 +- .../invoice/presets/PlatformClosing.java | 9 +- .../invoice/presets/PlatformMasthead.java | 7 +- .../invoice/presets/PlatformSettlement.java | 3 +- .../invoice/presets/SubscriptionClosing.java | 7 +- .../invoice/presets/SubscriptionMasthead.java | 5 +- .../proposal/presets/IndigoClosing.java | 3 +- .../proposal/presets/IndigoMasthead.java | 5 +- .../proposal/presets/ProposalUri.java | 118 -------------- 33 files changed, 418 insertions(+), 353 deletions(-) create mode 100644 qa/src/test/java/com/demcha/compose/document/templates/core/identity/ContactUriTest.java delete mode 100644 qa/src/test/java/com/demcha/compose/document/templates/cv/presets/ContactUriTest.java create mode 100644 templates/src/main/java/com/demcha/compose/document/templates/core/identity/ContactUri.java delete mode 100644 templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ContactUri.java delete mode 100644 templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/InvoiceUri.java delete mode 100644 templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/ProposalUri.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a9771799b..298b16e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -176,6 +176,23 @@ follow semantic versioning; release dates are ISO 8601. the model whose shape it renders. Every component normalizes `null` to its empty form and freezes its collections, matching the family's existing records. +- **One place turns a printed contact into a followable one: + `core.identity.ContactUri`.** The rule that a printed telephone number and the number + a device dials are different strings was written three times — once in the CV presets, + once in the invoice presets, once in the proposal presets — and the three copies were + byte-identical where they overlapped. They are now one helper in the family-neutral + identity layer, beside the contact-block and link records it serves, with `tel` / + `telLink` / `mailLink` / `webLink` / `channelLink`. +

+ It is public because the three families are three packages and a helper shared between + them cannot be package-private; it is not marked experimental because the shape is not + a guess — three independent implementations had already agreed on it. Every method + answers `null` rather than throwing, including where the string cannot be a URI at + all: a notice, a name or a line of prose can reach a channel field, and losing the + affordance is not worth failing to compose the page over. Twenty-three preset files + now call it and nothing renders differently — a link annotation is not ink, and every + pixel baseline in the suite passes untouched. + ### Templates - **The invoice presets set their page number in their own face.** A header or footer diff --git a/knowledge/api/templates.json b/knowledge/api/templates.json index daacbe0e2..dd6b77f0d 100644 --- a/knowledge/api/templates.json +++ b/knowledge/api/templates.json @@ -22,8 +22,8 @@ "graph-compose-testing:sources" ], "counts": { - "types": 227, - "methods": 1276, + "types": 228, + "methods": 1281, "constants": 177, "generated": 689 }, @@ -427,6 +427,87 @@ } ] }, + { + "name": "ContactUri", + "binaryName": "com.demcha.compose.document.templates.core.identity.ContactUri", + "kind": "class", + "modifiers": [ + "final" + ], + "artifact": "graph-compose-templates", + "members": [ + { + "kind": "method", + "name": "tel", + "static": true, + "origin": "source", + "typeParameters": null, + "returns": "String", + "params": [ + { + "type": "String", + "name": "phone" + } + ] + }, + { + "kind": "method", + "name": "telLink", + "static": true, + "origin": "source", + "typeParameters": null, + "returns": "DocumentLinkOptions", + "params": [ + { + "type": "String", + "name": "phone" + } + ] + }, + { + "kind": "method", + "name": "mailLink", + "static": true, + "origin": "source", + "typeParameters": null, + "returns": "DocumentLinkOptions", + "params": [ + { + "type": "String", + "name": "email" + } + ] + }, + { + "kind": "method", + "name": "webLink", + "static": true, + "origin": "source", + "typeParameters": null, + "returns": "DocumentLinkOptions", + "params": [ + { + "type": "String", + "name": "website" + } + ] + }, + { + "kind": "method", + "name": "channelLink", + "static": true, + "origin": "source", + "typeParameters": null, + "returns": "DocumentLinkOptions", + "params": [ + { + "type": "String", + "name": "contact" + } + ] + } + ] + }, { "name": "Headline", "binaryName": "com.demcha.compose.document.templates.core.identity.Headline", diff --git a/knowledge/api/templates.md b/knowledge/api/templates.md index 047c1253f..d1dd8b7a9 100644 --- a/knowledge/api/templates.md +++ b/knowledge/api/templates.md @@ -28,7 +28,7 @@ note: "Generated from the pinned artifact's class files. Authoritative closed se **GraphCompose version:** 2.4.0-SNAPSHOT -Types: 227 · methods: 1276 · constants: 177 · compiler-generated members: 689 +Types: 228 · methods: 1281 · constants: 177 · compiler-generated members: 689 ## com.demcha.compose.document.templates.api @@ -59,6 +59,13 @@ Types: 227 · methods: 1276 · constants: 177 · compiler-generated members: 689 ### ContactLine.Order (enum) - constants: `PHONE_FIRST`, `ADDRESS_FIRST` +### ContactUri (class) +- `String tel(String phone)` +- `DocumentLinkOptions telLink(String phone)` +- `DocumentLinkOptions mailLink(String email)` +- `DocumentLinkOptions webLink(String website)` +- `DocumentLinkOptions channelLink(String contact)` + ### Headline (class) - `void spacedCentered(SectionBuilder host, String name, BrandTheme theme)` - `void uppercaseCentered(SectionBuilder host, String name, BrandTheme theme)` diff --git a/qa/src/test/java/com/demcha/compose/document/templates/core/identity/ContactUriTest.java b/qa/src/test/java/com/demcha/compose/document/templates/core/identity/ContactUriTest.java new file mode 100644 index 000000000..abc2fa454 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/core/identity/ContactUriTest.java @@ -0,0 +1,97 @@ +package com.demcha.compose.document.templates.core.identity; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Pins the rules {@link ContactUri} exists for, starting with the one it was + * written for: a printed number and the number a device dials are not the same + * string. + * + *

Every preset in every family that draws a contact routes through it, so + * the rules are asserted here once rather than through each of their renders.

+ */ +class ContactUriTest { + + @Test + void aTrunkPrefixIsDroppedBecauseAnInternationalCallerOmitsIt() { + assertThat(ContactUri.tel("+44 (0)20 7946 0832")).isEqualTo("tel:+442079460832"); + assertThat(ContactUri.tel("+44 (0) 20 3966 1900")).isEqualTo("tel:+442039661900"); + } + + @Test + void aParenthesisedAreaCodeIsNotATrunkPrefixAndStays() { + // Only an all-zero group goes. A US area code in brackets is part of + // the number, and dropping it would dial somewhere else entirely. + assertThat(ContactUri.tel("+1 (415) 555 7842")).isEqualTo("tel:+14155557842"); + } + + @Test + void aNumberWithoutAPlusKeepsItsLocalForm() { + assertThat(ContactUri.tel("020 7946 0832")).isEqualTo("tel:02079460832"); + } + + @Test + void separatorsAndSpacingDoNotReachTheTarget() { + assertThat(ContactUri.tel("+61 402-938-209")).isEqualTo("tel:+61402938209"); + } + + @Test + void textWithNoDigitsIsNotDialable() { + assertThat(ContactUri.tel("ask reception")).isNull(); + assertThat(ContactUri.tel("")).isNull(); + assertThat(ContactUri.tel(" ")).isNull(); + assertThat(ContactUri.tel(null)).isNull(); + } + + @Test + void theLinkFormMirrorsTheTargetForm() { + assertThat(ContactUri.telLink("+44 (0)20 7946 0832")).isNotNull(); + assertThat(ContactUri.telLink("ask reception")).isNull(); + assertThat(ContactUri.telLink(null)).isNull(); + } + + @Test + void anAddressIsMailedAndASiteIsOpened() { + assertThat(ContactUri.mailLink(" billing@example.com ").uri()) + .isEqualTo("mailto:billing@example.com"); + assertThat(ContactUri.webLink("example.com/business").uri()) + .isEqualTo("https://example.com/business"); + assertThat(ContactUri.webLink("http://example.com").uri()) + .isEqualTo("http://example.com"); + assertThat(ContactUri.mailLink(" ")).isNull(); + assertThat(ContactUri.webLink(null)).isNull(); + } + + @Test + void aChannelIsReadFromTheShapeOfWhatItPrints() { + // A foot lists its channels without saying which is which, because a + // reader can see it. This reads the same thing back. + assertThat(ContactUri.channelLink("business@example.com").uri()) + .isEqualTo("mailto:business@example.com"); + assertThat(ContactUri.channelLink("+44 20 3322 8352").uri()) + .isEqualTo("tel:+442033228352"); + assertThat(ContactUri.channelLink("example.com/business").uri()) + .isEqualTo("https://example.com/business"); + assertThat(ContactUri.channelLink(" ")).isNull(); + } + + @Test + void aSiteCarryingDigitsIsStillASite() { + // The dialable test asks for nine digits AND a mostly-numeric string, + // so a domain with a year in it does not become a phone number. + assertThat(ContactUri.channelLink("example2026.com").uri()) + .isEqualTo("https://example2026.com"); + } + + @Test + void proseWhereAChannelWasExpectedLosesItsLinkRatherThanThePage() { + // A notice, a name, a line of prose can all reach a channel field. + // DocumentLinkOptions refuses a string that is not a URI, and a contact + // line is not the place to find that out. + assertThat(ContactUri.channelLink("Confidential — for the addressee only")).isNull(); + assertThat(ContactUri.webLink("ask at the bar")).isNull(); + assertThat(ContactUri.mailLink("Jane Doe ")).isNull(); + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/ContactUriTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/ContactUriTest.java deleted file mode 100644 index 16d0b49e7..000000000 --- a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/ContactUriTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.demcha.compose.document.templates.cv.presets; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Pins the one rule {@link ContactUri} exists for: a printed number and the - * number a device dials are not the same string. - * - *

Every preset that draws a telephone number now routes through it, so the - * rule is asserted here once rather than through each of their renders.

- */ -class ContactUriTest { - - @Test - void aTrunkPrefixIsDroppedBecauseAnInternationalCallerOmitsIt() { - assertThat(ContactUri.tel("+44 (0)20 7946 0832")).isEqualTo("tel:+442079460832"); - assertThat(ContactUri.tel("+44 (0) 20 3966 1900")).isEqualTo("tel:+442039661900"); - } - - @Test - void aParenthesisedAreaCodeIsNotATrunkPrefixAndStays() { - // Only an all-zero group goes. A US area code in brackets is part of - // the number, and dropping it would dial somewhere else entirely. - assertThat(ContactUri.tel("+1 (415) 555 7842")).isEqualTo("tel:+14155557842"); - } - - @Test - void aNumberWithoutAPlusKeepsItsLocalForm() { - assertThat(ContactUri.tel("020 7946 0832")).isEqualTo("tel:02079460832"); - } - - @Test - void separatorsAndSpacingDoNotReachTheTarget() { - assertThat(ContactUri.tel("+61 402-938-209")).isEqualTo("tel:+61402938209"); - } - - @Test - void textWithNoDigitsIsNotDialable() { - assertThat(ContactUri.tel("ask reception")).isNull(); - assertThat(ContactUri.tel("")).isNull(); - assertThat(ContactUri.tel(" ")).isNull(); - assertThat(ContactUri.tel(null)).isNull(); - } - - @Test - void theLinkFormMirrorsTheTargetForm() { - assertThat(ContactUri.telLink("+44 (0)20 7946 0832")).isNotNull(); - assertThat(ContactUri.telLink("ask reception")).isNull(); - assertThat(ContactUri.telLink(null)).isNull(); - } -} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/core/identity/ContactUri.java b/templates/src/main/java/com/demcha/compose/document/templates/core/identity/ContactUri.java new file mode 100644 index 000000000..82bf82ca1 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/core/identity/ContactUri.java @@ -0,0 +1,144 @@ +package com.demcha.compose.document.templates.core.identity; + +import com.demcha.compose.document.node.DocumentLinkOptions; + +import java.util.regex.Pattern; + +/** + * Turns a printed contact detail into something a reader can act on. + * + *

A document prints a number, an address and a site the way a person reads + * them; a reader's device needs the form it can dial, mail or open. Every + * template family needs that conversion and they all need the same one, so it + * lives beside the rest of the neutral identity layer rather than three times + * over in three families' presets.

+ * + *

A missing link is never a failed render

+ * + *

Every method here answers {@code null} rather than throwing: not + * everything a document prints where a contact would go is one, and losing the + * affordance is not worth failing to compose the page over. A caller passes the + * result straight to a component that takes a link or nothing.

+ * + * @author Artem Demchyshyn + * @since 2.4.0 + */ +public final class ContactUri { + + /** + * A trunk prefix in a printed number is for a domestic dialler and is not + * part of the international one — {@code +44 (0)20 7946 0832} dials + * {@code +442079460832}. Left in, the digits run together into a number that + * reaches nobody. A parenthesised area code is not a trunk prefix, + * which is why only an all-zero group goes. + */ + private static final Pattern TRUNK_PREFIX = Pattern.compile("\\(0+\\)"); + + private ContactUri() { + } + + /** + * A printed telephone number as a {@code tel:} target. + * + * @param phone the number as the sheet prints it + * @return the target, or {@code null} when the text carries no digits to + * dial + */ + public static String tel(String phone) { + if (phone == null || phone.isBlank()) { + return null; + } + String dialled = TRUNK_PREFIX.matcher(phone).replaceAll(""); + String digits = dialled.replaceAll("[^0-9]", ""); + return digits.isEmpty() + ? null + : "tel:" + (phone.trim().startsWith("+") ? "+" : "") + digits; + } + + /** + * The same, ready to hand to a component that takes a link or nothing. + * + * @param phone the number as the sheet prints it + * @return the link options, or {@code null} when there is nothing to dial + */ + public static DocumentLinkOptions telLink(String phone) { + return options(tel(phone)); + } + + /** + * An address as something a reader can write to. + * + * @param email the address as the sheet prints it + * @return the link options, or {@code null} when there is no address, or + * none that could be made from what was printed + */ + public static DocumentLinkOptions mailLink(String email) { + return email == null || email.isBlank() + ? null + : options("mailto:" + email.trim()); + } + + /** + * A written site as something a reader can follow. A sheet prints + * {@code example.com}; a viewer needs a scheme in front of it. + * + * @param website the site as the sheet prints it + * @return the link options, or {@code null} when there is no site, or none + * that could be made from what was printed + */ + public static DocumentLinkOptions webLink(String website) { + if (website == null || website.isBlank()) { + return null; + } + String trimmed = website.trim(); + return options(trimmed.startsWith("http://") || trimmed.startsWith("https://") + ? trimmed + : "https://" + trimmed); + } + + /** + * The link a printed contact string wants, guessed from its own shape. + * + *

A document's foot often lists its channels as plain strings — a site, + * an address, a number — without saying which is which, because a reader can + * see it. This reads the same thing back: an address has an at-sign, a + * number starts with a plus or is mostly digits, and anything else is a + * site.

+ * + *

Reach for this only where the document really does not say. Where a + * field is known to hold a number or an address, name it: a heuristic that + * is right almost always is worse than a fact.

+ * + * @param contact the channel as the sheet prints it + * @return the link options, or {@code null} when nothing can be made of it + */ + public static DocumentLinkOptions channelLink(String contact) { + if (contact == null || contact.isBlank()) { + return null; + } + String trimmed = contact.trim(); + if (trimmed.contains("@")) { + return mailLink(trimmed); + } + String digits = trimmed.replaceAll("[^0-9]", ""); + boolean dialable = trimmed.startsWith("+") + || digits.length() >= 9 && digits.length() >= trimmed.length() / 2; + return dialable ? telLink(trimmed) : webLink(trimmed); + } + + /** + * The link a target makes, or nothing when it makes none. + * + *

{@link DocumentLinkOptions} refuses a string that is not a URI, and a + * contact line is not the place to find that out: a notice, a name, a line + * of prose can all reach a channel field, and none of them should stop the + * page composing.

+ */ + private static DocumentLinkOptions options(String target) { + try { + return new DocumentLinkOptions(target); + } catch (IllegalArgumentException notAUri) { + return null; + } + } +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/core/identity/package-info.java b/templates/src/main/java/com/demcha/compose/document/templates/core/identity/package-info.java index b46b1fc05..646ec3a86 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/core/identity/package-info.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/core/identity/package-info.java @@ -20,6 +20,9 @@ * — the header widgets that render a {@code PartyIdentity}. *
  • {@link com.demcha.compose.document.templates.core.identity.SvgGlyph} * — a small inline-SVG glyph helper used by the header widgets.
  • + *
  • {@link com.demcha.compose.document.templates.core.identity.ContactUri} + * — turns a printed number, address or site into the target a reader's + * device can act on.
  • * */ package com.demcha.compose.document.templates.core.identity; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ContactUri.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ContactUri.java deleted file mode 100644 index 3c08c581a..000000000 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ContactUri.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.demcha.compose.document.templates.cv.presets; - -import com.demcha.compose.document.node.DocumentLinkOptions; - -import java.util.regex.Pattern; - -/** - * Turns a printed contact detail into something a reader can act on. - * - *

    A sheet prints a number the way a person reads it and a reader's device - * dials the digits, and the two are not the same string — so the conversion - * belongs in one place rather than beside every contact block that needs it.

    - */ -final class ContactUri { - - /** - * A trunk prefix in a printed number is for a domestic dialler and is not - * part of the international one — {@code +44 (0)20 7946 0832} dials - * {@code +442079460832}. Left in, the digits run together into a number - * that reaches nobody. A parenthesised area code is not a trunk - * prefix, which is why only an all-zero group goes. - */ - private static final Pattern TRUNK_PREFIX = Pattern.compile("\\(0+\\)"); - - private ContactUri() { - } - - /** - * A printed telephone number as something a reader can dial. - * - * @param phone the number as the sheet prints it - * @return a {@code tel:} target, or {@code null} when the text carries no - * digits to dial - */ - static String tel(String phone) { - if (phone == null || phone.isBlank()) { - return null; - } - String dialled = TRUNK_PREFIX.matcher(phone).replaceAll(""); - String digits = dialled.replaceAll("[^0-9]", ""); - return digits.isEmpty() - ? null - : "tel:" + (phone.trim().startsWith("+") ? "+" : "") + digits; - } - - /** - * The same, ready to hand to a component that takes a link or nothing. - * - * @param phone the number as the sheet prints it - * @return the link options, or {@code null} when there is nothing to dial - */ - static DocumentLinkOptions telLink(String phone) { - String uri = tel(phone); - return uri == null ? null : new DocumentLinkOptions(uri); - } -} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java index 195edbda9..07987b4cc 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java @@ -16,6 +16,8 @@ import com.demcha.compose.document.templates.cv.data.*; import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.util.ArrayList; import java.util.List; import java.util.Locale; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java index 626dc63a9..57b98d90a 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java @@ -20,6 +20,8 @@ import com.demcha.compose.document.templates.cv.widgets.SectionHeader; import com.demcha.compose.font.FontName; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.util.List; import java.util.Locale; import java.util.Objects; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java index c4307cfd3..0660232d7 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java @@ -29,6 +29,8 @@ import com.demcha.compose.document.templates.core.identity.SvgGlyph; import com.demcha.compose.document.svg.SvgIcon; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java index 777245f32..3cf12270b 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java @@ -21,6 +21,8 @@ import com.demcha.compose.document.templates.core.identity.SvgGlyph; import com.demcha.compose.font.FontName; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.util.*; /** diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java index 9677693a3..e6bbb129a 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java @@ -17,6 +17,8 @@ import com.demcha.compose.document.templates.core.widgets.CardWidget; import com.demcha.compose.font.FontName; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.util.List; import java.util.Locale; import java.util.Objects; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java index 7ebb8cae1..d9472ea84 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java @@ -22,6 +22,8 @@ import com.demcha.compose.document.templates.core.theme.BrandTheme; import com.demcha.compose.document.templates.core.identity.SvgGlyph; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java index 15b33e7fb..ee5e62639 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java @@ -24,6 +24,8 @@ import com.demcha.compose.document.templates.core.identity.SvgGlyph; import com.demcha.compose.document.templates.core.widgets.TimelineAxisWidget; +import com.demcha.compose.document.templates.core.identity.ContactUri; + import java.util.*; /** diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/InvoiceUri.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/InvoiceUri.java deleted file mode 100644 index c801a9046..000000000 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/InvoiceUri.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.demcha.compose.document.templates.invoice.presets; - -import com.demcha.compose.document.node.DocumentLinkOptions; - -import java.util.regex.Pattern; - -/** - * Turns a printed contact detail into something a reader can act on. - * - *

    A sheet prints a number, an address and a site the way a person reads - * them, and a reader's device needs the form it can dial, mail or open. The two - * are not the same string, so the conversion lives in one place rather than - * beside every block on every invoice preset that needs it.

    - */ -final class InvoiceUri { - - /** - * A trunk prefix in a printed number is for a domestic dialler and is not - * part of the international one — {@code +44 (0)20 7946 0832} dials - * {@code +442079460832}. Left in, the digits run together into a number that - * reaches nobody. A parenthesised area code is not a trunk prefix, - * which is why only an all-zero group goes. - */ - private static final Pattern TRUNK_PREFIX = Pattern.compile("\\(0+\\)"); - - private InvoiceUri() { - } - - /** - * A printed telephone number as something a reader can dial. - * - * @param phone the number as the sheet prints it - * @return a {@code tel:} target, or {@code null} when the text carries no - * digits to dial - */ - private static String tel(String phone) { - if (phone == null || phone.isBlank()) { - return null; - } - String dialled = TRUNK_PREFIX.matcher(phone).replaceAll(""); - String digits = dialled.replaceAll("[^0-9]", ""); - return digits.isEmpty() - ? null - : "tel:" + (phone.trim().startsWith("+") ? "+" : "") + digits; - } - - /** - * The same, ready to hand to a component that takes a link or nothing. - * - * @param phone the number as the sheet prints it - * @return the link options, or {@code null} when there is nothing to dial - */ - static DocumentLinkOptions telLink(String phone) { - String uri = tel(phone); - return uri == null ? null : new DocumentLinkOptions(uri); - } - - /** - * An address as something a reader can write to. - * - * @param email the address as the sheet prints it - * @return the link options, or {@code null} when there is no address - */ - static DocumentLinkOptions mailLink(String email) { - return email == null || email.isBlank() - ? null - : new DocumentLinkOptions("mailto:" + email.trim()); - } - - /** - * A written site as something a reader can follow. A sheet prints - * {@code example.com}; a viewer needs a scheme in front of it. - * - * @param website the site as the sheet prints it - * @return the link options, or {@code null} when there is no site - */ - static DocumentLinkOptions webLink(String website) { - if (website == null || website.isBlank()) { - return null; - } - String trimmed = website.trim(); - return new DocumentLinkOptions( - trimmed.startsWith("http://") || trimmed.startsWith("https://") - ? trimmed - : "https://" + trimmed); - } -} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantClosing.java index 663c0f6fe..4ff97c111 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantClosing.java @@ -9,6 +9,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock; @@ -166,7 +167,7 @@ static void renderIdentity(PageFlowBuilder page, InvoiceBrand brand, p.name("FooterSite"); p.text(supplier.website()); p.textStyle(style(FOOTER_SITE_SIZE, ACCENT)); - p.link(InvoiceUri.webLink(supplier.website())); + p.link(ContactUri.webLink(supplier.website())); p.margin(new DocumentInsets( capGap(FOOTER_SITE_CAP_Y - FOOTER_ADDR_CAP_Y, FOOTER_ADDR_SIZE, false, FOOTER_SITE_SIZE, false), @@ -218,9 +219,9 @@ private static String monogram(InvoiceBrand brand) { private static void writeReachable(ParagraphBuilder paragraph, String prose, InvoiceNotesBlock notes, InvoiceContactBlock supplier) { List found = new ArrayList<>(); - addIfPresent(found, prose, notes.contactEmail(), InvoiceUri.mailLink(notes.contactEmail())); - addIfPresent(found, prose, supplier.email(), InvoiceUri.mailLink(supplier.email())); - addIfPresent(found, prose, supplier.website(), InvoiceUri.webLink(supplier.website())); + addIfPresent(found, prose, notes.contactEmail(), ContactUri.mailLink(notes.contactEmail())); + addIfPresent(found, prose, supplier.email(), ContactUri.mailLink(supplier.email())); + addIfPresent(found, prose, supplier.website(), ContactUri.webLink(supplier.website())); found.sort(Comparator.comparingInt(Reachable::at)); int cursor = 0; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantMasthead.java index b3f1d40ec..5e8f2c0a1 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantMasthead.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantMasthead.java @@ -11,6 +11,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead; @@ -180,11 +181,11 @@ private static void renderSupplier(SectionBuilder column, InvoiceContactBlock su .margin(new DocumentInsets( inkGapToImage(280 - 236, BODY_SIZE, false), 0, 0, 0)); channel(block, "Website", MerchantIcons.WEBSITE, supplier.website(), - InvoiceUri.webLink(supplier.website())); + ContactUri.webLink(supplier.website())); channel(block, "Email", MerchantIcons.EMAIL, supplier.email(), - InvoiceUri.mailLink(supplier.email())); + ContactUri.mailLink(supplier.email())); channel(block, "Phone", MerchantIcons.PHONE, supplier.phone(), - InvoiceUri.telLink(supplier.phone())); + ContactUri.telLink(supplier.phone())); }); if (!supplier.taxRegistrationNumber().isBlank()) { diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantSettlement.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantSettlement.java index 7df328d32..850808f03 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantSettlement.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MerchantSettlement.java @@ -7,6 +7,7 @@ import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceTotalsBlock; @@ -156,7 +157,7 @@ private static void renderPanel(SectionBuilder column, InvoicePaymentBlock payme // A bank detail is a reference, not a destination — except // an address, the one field a reader would act on. if (field.value().contains("@")) { - p.link(InvoiceUri.mailLink(field.value())); + p.link(ContactUri.mailLink(field.value())); } }); }); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredClosing.java index 4a9c61012..9a01eddb1 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredClosing.java @@ -5,6 +5,7 @@ import com.demcha.compose.document.node.DocumentLinkOptions; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock; @@ -93,10 +94,10 @@ static void render(SectionBuilder body, InvoiceNotesBlock notes, private static void writeReachable(ParagraphBuilder paragraph, String prose, InvoiceNotesBlock notes, InvoiceContactBlock supplier) { List found = new ArrayList<>(); - addIfPresent(found, prose, notes.contactEmail(), InvoiceUri.mailLink(notes.contactEmail())); - addIfPresent(found, prose, supplier.email(), InvoiceUri.mailLink(supplier.email())); - addIfPresent(found, prose, notes.contactPhone(), InvoiceUri.telLink(notes.contactPhone())); - addIfPresent(found, prose, supplier.website(), InvoiceUri.webLink(supplier.website())); + addIfPresent(found, prose, notes.contactEmail(), ContactUri.mailLink(notes.contactEmail())); + addIfPresent(found, prose, supplier.email(), ContactUri.mailLink(supplier.email())); + addIfPresent(found, prose, notes.contactPhone(), ContactUri.telLink(notes.contactPhone())); + addIfPresent(found, prose, supplier.website(), ContactUri.webLink(supplier.website())); found.sort(Comparator.comparingInt(Reachable::at)); int cursor = 0; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredMasthead.java index 263bcbcc3..745311583 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredMasthead.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredMasthead.java @@ -10,6 +10,7 @@ import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead; @@ -165,17 +166,17 @@ private static void renderSupplierBlock(SectionBuilder column, InvoiceContactBlo if (!supplier.phone().isBlank()) { labelledRow(contacts, "SupplierContact_" + index++, "Phone:", supplier.phone(), - SUPPLIER_LABEL_W, BODY, BODY, InvoiceUri.telLink(supplier.phone())); + SUPPLIER_LABEL_W, BODY, BODY, ContactUri.telLink(supplier.phone())); } if (!supplier.email().isBlank()) { labelledRow(contacts, "SupplierContact_" + index++, "Email:", supplier.email(), SUPPLIER_LABEL_W, BODY, BODY, - InvoiceUri.mailLink(supplier.email())); + ContactUri.mailLink(supplier.email())); } if (!supplier.website().isBlank()) { labelledRow(contacts, "SupplierContact_" + index, "Website:", supplier.website(), SUPPLIER_LABEL_W, BODY, BODY, - InvoiceUri.webLink(supplier.website())); + ContactUri.webLink(supplier.website())); } }); }); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredSettlement.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredSettlement.java index 1ea27a35c..1c917343b 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredSettlement.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/MeteredSettlement.java @@ -8,6 +8,7 @@ import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.style.DocumentStroke; import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceTotalsBlock; @@ -145,7 +146,7 @@ private static void renderPaymentDetails(SectionBuilder column, InvoicePaymentBl * address, which is the one field on the panel a reader would act on. */ private static DocumentLinkOptions linkFor(String value) { - return value.contains("@") ? InvoiceUri.mailLink(value) : null; + return value.contains("@") ? ContactUri.mailLink(value) : null; } private static void renderDueBy(SectionBuilder column, InvoicePaymentBlock payment) { diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianCards.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianCards.java index 60ad278be..904dd049e 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianCards.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianCards.java @@ -6,6 +6,7 @@ import com.demcha.compose.document.node.RowVerticalAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; @@ -124,7 +125,7 @@ private static void renderPayment(SectionBuilder cell, InvoicePaymentBlock payme // A bank detail is a reference, not a destination — // except an address, the one field a reader would act on. field.value().contains("@") - ? InvoiceUri.mailLink(field.value()) + ? ContactUri.mailLink(field.value()) : null)); }); } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianClosing.java index 6bc65adc2..4aa7a5d96 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianClosing.java @@ -7,6 +7,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; @@ -82,7 +83,7 @@ static void render(PageFlowBuilder page, InvoiceBrand brand, text.addParagraph(p -> { p.name("ClosingContact").align(TextAlign.RIGHT); p.inlineText(supplier.email(), plain(CLOSING_RIGHT_SIZE, MUTED), - InvoiceUri.mailLink(supplier.email())); + ContactUri.mailLink(supplier.email())); }); } }); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianParties.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianParties.java index e7cbfff14..5861cb58e 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianParties.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ObsidianParties.java @@ -9,6 +9,7 @@ import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceRecipient; @@ -67,11 +68,11 @@ static void render(PageFlowBuilder page, InvoiceBrand brand, row.addSection("SupplierCard", cell -> renderCard(cell, "Supplier", "From", supplierDisc(brand, supplier), supplier.legalName(), supplier.addressLines(), supplier.email(), - InvoiceUri.mailLink(supplier.email()))); + ContactUri.mailLink(supplier.email()))); row.addSection("BillToCard", cell -> renderCard(cell, "BillTo", billTo.heading(), initialsDisc("BillToDisc", DISC_CLIENT, initials(billTo.name())), billTo.name(), billTo.addressLines(), billTo.email(), - InvoiceUri.mailLink(billTo.email()))); + ContactUri.mailLink(billTo.email()))); }); } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformClosing.java index ca2bfb270..3cf1d3674 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformClosing.java @@ -7,6 +7,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceNotesBlock; @@ -157,10 +158,10 @@ static void renderIdentity(PageFlowBuilder page, InvoiceBrand brand, private static void writeReachable(ParagraphBuilder paragraph, String prose, InvoiceNotesBlock notes, InvoiceContactBlock supplier) { List found = new ArrayList<>(); - addIfPresent(found, prose, notes.contactEmail(), InvoiceUri.mailLink(notes.contactEmail())); - addIfPresent(found, prose, supplier.email(), InvoiceUri.mailLink(supplier.email())); - addIfPresent(found, prose, supplier.website(), InvoiceUri.webLink(supplier.website())); - addIfPresent(found, prose, notes.contactPhone(), InvoiceUri.telLink(notes.contactPhone())); + addIfPresent(found, prose, notes.contactEmail(), ContactUri.mailLink(notes.contactEmail())); + addIfPresent(found, prose, supplier.email(), ContactUri.mailLink(supplier.email())); + addIfPresent(found, prose, supplier.website(), ContactUri.webLink(supplier.website())); + addIfPresent(found, prose, notes.contactPhone(), ContactUri.telLink(notes.contactPhone())); found.sort(Comparator.comparingInt(Reachable::at)); int cursor = 0; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformMasthead.java index 33ce330c2..84f4b894d 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformMasthead.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformMasthead.java @@ -8,6 +8,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead; @@ -155,11 +156,11 @@ private static void renderSupplier(SectionBuilder column, InvoiceContactBlock su block.spacing(capPitch(28, BODY_SIZE)) .margin(new DocumentInsets(capPitch(46, BODY_SIZE), 0, 0, 0)); channel(block, "Website", PlatformIcons.WEBSITE, supplier.website(), - InvoiceUri.webLink(supplier.website())); + ContactUri.webLink(supplier.website())); channel(block, "Email", PlatformIcons.EMAIL, supplier.email(), - InvoiceUri.mailLink(supplier.email())); + ContactUri.mailLink(supplier.email())); channel(block, "Phone", PlatformIcons.PHONE, supplier.phone(), - InvoiceUri.telLink(supplier.phone())); + ContactUri.telLink(supplier.phone())); }); if (!supplier.taxRegistrationNumber().isBlank()) { diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformSettlement.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformSettlement.java index d9ab639c3..d5ed49de9 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformSettlement.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/PlatformSettlement.java @@ -7,6 +7,7 @@ import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceTotalsBlock; @@ -142,7 +143,7 @@ private static void renderPaymentPanel(SectionBuilder column, InvoicePaymentBloc // except the remittance address, the one field on the // panel a reader would act on. if (field.value().contains("@")) { - p.link(InvoiceUri.mailLink(field.value())); + p.link(ContactUri.mailLink(field.value())); } }); }); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionClosing.java index fa43a8c09..7d0405bc4 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionClosing.java @@ -11,6 +11,7 @@ import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.svg.SvgIcon; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoicePaymentBlock; @@ -265,9 +266,9 @@ static void renderBand(PageFlowBuilder page, InvoicePaymentBlock payment, private static void writeReachable(ParagraphBuilder paragraph, String prose, InvoiceContactBlock supplier) { List found = new ArrayList<>(); - addIfPresent(found, prose, supplier.email(), InvoiceUri.mailLink(supplier.email())); - addIfPresent(found, prose, supplier.website(), InvoiceUri.webLink(supplier.website())); - addIfPresent(found, prose, supplier.phone(), InvoiceUri.telLink(supplier.phone())); + addIfPresent(found, prose, supplier.email(), ContactUri.mailLink(supplier.email())); + addIfPresent(found, prose, supplier.website(), ContactUri.webLink(supplier.website())); + addIfPresent(found, prose, supplier.phone(), ContactUri.telLink(supplier.phone())); found.sort(Comparator.comparingInt(Reachable::at)); int cursor = 0; diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionMasthead.java index fdc0f16d6..7c9d7fc90 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionMasthead.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/SubscriptionMasthead.java @@ -9,6 +9,7 @@ import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.invoice.InvoiceBrand; import com.demcha.compose.document.templates.data.invoice.InvoiceContactBlock; import com.demcha.compose.document.templates.data.invoice.InvoiceMasthead; @@ -143,9 +144,9 @@ private static void renderSupplier(SectionBuilder cell, InvoiceContactBlock supp block.margin(new DocumentInsets(baselineGap(48.9, BODY_SIZE, BODY_SIZE), 0, 0, 0)); block.spacing(px(30) - 1.2 * BODY_SIZE); contact(block, "Email", "E", supplier.email(), - InvoiceUri.mailLink(supplier.email())); + ContactUri.mailLink(supplier.email())); contact(block, "Website", "W", supplier.website(), - InvoiceUri.webLink(supplier.website())); + ContactUri.webLink(supplier.website())); contact(block, "Registration", supplier.taxRegistrationLabel(), supplier.taxRegistrationNumber(), null); }); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoClosing.java index 54d57abec..3481b773c 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoClosing.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoClosing.java @@ -7,6 +7,7 @@ import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.proposal.ProposalBrand; import com.demcha.compose.document.templates.data.proposal.ProposalFooter; @@ -175,7 +176,7 @@ private static void renderChannels(SectionBuilder identity, ProposalFooter foote /** A channel, made followable when its printed shape says what it is. */ private static void appendChannel(ParagraphBuilder p, String contact, DocumentTextStyle body) { - DocumentLinkOptions link = ProposalUri.channelLink(contact); + DocumentLinkOptions link = ContactUri.channelLink(contact); if (link == null) { p.inlineText(contact, body); } else { diff --git a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoMasthead.java index 7e7ee9425..5b5f01b47 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoMasthead.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/IndigoMasthead.java @@ -6,6 +6,7 @@ import com.demcha.compose.document.node.TextAlign; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentRowColumn; +import com.demcha.compose.document.templates.core.identity.ContactUri; import com.demcha.compose.document.templates.data.proposal.ProposalAttention; import com.demcha.compose.document.templates.data.proposal.ProposalBrand; import com.demcha.compose.document.templates.data.proposal.ProposalMetaLine; @@ -223,9 +224,9 @@ private static void renderAttention(SectionBuilder left, ProposalAttention atten .margin(new DocumentInsets( cell.capAt(ATTN_ROLE_CAP, ATTN_ROLE_SIZE, false), 0, 0, 0))); reachable(left, "AttentionEmail", attention.email(), - ProposalUri.mailLink(attention.email()), cell, ATTN_EMAIL_CAP); + ContactUri.mailLink(attention.email()), cell, ATTN_EMAIL_CAP); reachable(left, "AttentionPhone", attention.phone(), - ProposalUri.telLink(attention.phone()), cell, ATTN_PHONE_CAP); + ContactUri.telLink(attention.phone()), cell, ATTN_PHONE_CAP); } /** diff --git a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/ProposalUri.java b/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/ProposalUri.java deleted file mode 100644 index 0b2989fb1..000000000 --- a/templates/src/main/java/com/demcha/compose/document/templates/proposal/presets/ProposalUri.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.demcha.compose.document.templates.proposal.presets; - -import com.demcha.compose.document.node.DocumentLinkOptions; - -import java.util.regex.Pattern; - -/** - * Turns a printed contact detail into something a reader can act on. - * - *

    A proposal prints a number, an address and a site the way a person reads - * them, and a reader's device needs the form it can dial, mail or open. The - * conversion lives in one place rather than beside every block that needs it.

    - */ -final class ProposalUri { - - /** - * A trunk prefix in a printed number is for a domestic dialler and is not - * part of the international one — {@code +44 (0)20 7946 0832} dials - * {@code +442079460832}. Left in, the digits run together into a number that - * reaches nobody. A parenthesised area code is not a trunk prefix, - * which is why only an all-zero group goes. - */ - private static final Pattern TRUNK_PREFIX = Pattern.compile("\\(0+\\)"); - - private ProposalUri() { - } - - /** - * A printed telephone number as something a reader can dial. - * - * @param phone the number as the sheet prints it - * @return the link options, or {@code null} when the text carries no digits - */ - static DocumentLinkOptions telLink(String phone) { - if (phone == null || phone.isBlank()) { - return null; - } - String dialled = TRUNK_PREFIX.matcher(phone).replaceAll(""); - String digits = dialled.replaceAll("[^0-9]", ""); - return digits.isEmpty() - ? null - : new DocumentLinkOptions( - "tel:" + (phone.trim().startsWith("+") ? "+" : "") + digits); - } - - /** - * An address as something a reader can write to. - * - * @param email the address as the sheet prints it - * @return the link options, or {@code null} when there is no address, or - * none that could be made from what was printed - */ - static DocumentLinkOptions mailLink(String email) { - if (email == null || email.isBlank()) { - return null; - } - try { - return new DocumentLinkOptions("mailto:" + email.trim()); - } catch (IllegalArgumentException notAUri) { - return null; - } - } - - /** - * A written site as something a reader can follow. A sheet prints - * {@code example.com}; a viewer needs a scheme in front of it. - * - *

    Not everything a document prints where a site would go is one, and a - * string that cannot be a URI is printed as it stands rather than refused: - * a link is an affordance, and losing it is not worth failing to render the - * page over.

    - * - * @param website the site as the sheet prints it - * @return the link options, or {@code null} when there is no site, or none - * that could be made from what was printed - */ - static DocumentLinkOptions webLink(String website) { - if (website == null || website.isBlank()) { - return null; - } - String trimmed = website.trim(); - String target = trimmed.startsWith("http://") || trimmed.startsWith("https://") - ? trimmed - : "https://" + trimmed; - try { - return new DocumentLinkOptions(target); - } catch (IllegalArgumentException notAUri) { - return null; - } - } - - /** - * The link a printed contact string wants, guessed from its own shape. - * - *

    A proposal's foot lists its channels as plain strings — a site, an - * address, a number — without saying which is which, because a reader can - * see it. This reads the same thing back: an address has an at-sign, a - * number starts with a plus or is mostly digits, and anything else is a - * site.

    - * - * @param contact the channel as the sheet prints it - * @return the link options, or {@code null} when nothing can be made of it - */ - static DocumentLinkOptions channelLink(String contact) { - if (contact == null || contact.isBlank()) { - return null; - } - String trimmed = contact.trim(); - if (trimmed.contains("@")) { - return mailLink(trimmed); - } - String digits = trimmed.replaceAll("[^0-9]", ""); - if (trimmed.startsWith("+") || digits.length() >= 9 && digits.length() >= trimmed.length() / 2) { - return telLink(trimmed); - } - return webLink(trimmed); - } -}