Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
<br><br>
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
Expand Down
85 changes: 83 additions & 2 deletions knowledge/api/templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"graph-compose-testing:sources"
],
"counts": {
"types": 227,
"methods": 1276,
"types": 228,
"methods": 1281,
"constants": 177,
"generated": 689
},
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion knowledge/api/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)`
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*/
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 <jane@example.com>")).isNull();
}
}

This file was deleted.

Loading
Loading