Skip to main content

meta_language/document_formatting/
profile.rs

1//! Per-format capability profiles for cross-format document reconstruction.
2//!
3//! Each supported document format (`txt`, `Markdown`, `HTML`, `PDF`, `DOCX`)
4//! exposes a [`LanguageProfile`] over the shared, language-free formatting
5//! concept ontology (issue #83). The profile records which formatting concepts
6//! the format can represent natively and, for every concept it cannot, the
7//! documented lossy fallback applied when the concept is encountered. This is
8//! the per-target fidelity report required by issue #86: a document parsed from
9//! one format reconstructs into any other when the source uses only concepts
10//! both formats support, and unsupported concepts degrade through a declared
11//! fallback rather than silent data loss.
12
13use crate::language_profile::LanguageProfile;
14use crate::link_network::LinkType;
15
16/// The ordered set of document formats the cross-format reconstruction layer
17/// supports, used as both source and target of `reconstruct_text_as`.
18pub const DOCUMENT_FORMATS: &[&str] = &["txt", "Markdown", "HTML", "PDF", "DOCX"];
19
20/// The shared cross-format formatting concepts, by readable concept identity.
21///
22/// Cross-format fidelity reports consider these concepts. Every format profile
23/// classifies each of them as either natively supported or carrying a
24/// documented lossy fallback.
25pub const CROSS_FORMAT_CONCEPT_IDS: &[&str] = &[
26    "heading",
27    "paragraph",
28    "bullet-list",
29    "ordered-list",
30    "list-item",
31    "strong-emphasis",
32    "emphasis",
33    "hyperlink",
34];
35
36/// [`CROSS_FORMAT_CONCEPT_IDS`] under the identities earlier releases published.
37///
38/// The order is the same, with `strong` for `strong-emphasis`, so consumers
39/// that list or match the published identities keep working. Profile lookups accept
40/// either identity: [`LanguageProfile::supports_concept`] and
41/// [`LanguageProfile::concept_fallback`] resolve a former identity through
42/// [`crate::current_concept_id`].
43pub const CROSS_FORMAT_CONCEPTS: &[&str] = &[
44    "heading",
45    "paragraph",
46    "bullet-list",
47    "ordered-list",
48    "list-item",
49    "strong",
50    "emphasis",
51    "hyperlink",
52];
53
54/// Returns the capability profile for a document `format`, or `None` when the
55/// format is not one of the cross-format reconstruction targets.
56///
57/// The returned [`LanguageProfile`] lists the formatting concepts the format
58/// represents natively (via [`LanguageProfile::supports_concept`]) and the
59/// documented fallback for every concept it cannot
60/// (via [`LanguageProfile::concept_fallback`] / [`LanguageProfile::fallbacks`]).
61#[must_use]
62pub fn document_format_profile(format: &str) -> Option<LanguageProfile> {
63    let canonical = canonical_document_format(format)?;
64    let profile = base_profile(canonical);
65    Some(match canonical {
66        "txt" => txt_profile(profile),
67        "Markdown" => markdown_profile(profile),
68        "HTML" => html_profile(profile),
69        "PDF" => pdf_profile(profile),
70        "DOCX" => docx_profile(profile),
71        _ => unreachable!("canonical_document_format only yields known formats"),
72    })
73}
74
75/// Canonicalizes a format/language label to one of [`DOCUMENT_FORMATS`].
76///
77/// Matching is case-insensitive and accepts the common aliases used when a
78/// network is parsed (for example `md` for Markdown or `plain-text` for `txt`).
79#[must_use]
80pub fn canonical_document_format(format: &str) -> Option<&'static str> {
81    match format.to_ascii_lowercase().as_str() {
82        "txt" | "text" | "plain-text" | "plaintext" => Some("txt"),
83        "markdown" | "md" => Some("Markdown"),
84        "html" | "htm" => Some("HTML"),
85        "pdf" => Some("PDF"),
86        "docx" => Some("DOCX"),
87        _ => None,
88    }
89}
90
91fn base_profile(canonical: &str) -> LanguageProfile {
92    LanguageProfile::new(canonical, canonical)
93        .with_link_type(LinkType::Document)
94        .with_link_type(LinkType::Concept)
95        .with_link_type(LinkType::Token)
96}
97
98fn with_supported<'a>(
99    mut profile: LanguageProfile,
100    concepts: impl IntoIterator<Item = &'a str>,
101) -> LanguageProfile {
102    for concept in concepts {
103        profile = profile.with_concept(concept);
104    }
105    profile
106}
107
108fn txt_profile(profile: LanguageProfile) -> LanguageProfile {
109    with_supported(profile, ["paragraph"])
110        .with_concept_fallback(
111            "heading",
112            "flattened to a plain paragraph (heading level dropped)",
113        )
114        .with_concept_fallback(
115            "bullet-list",
116            "flattened to plain lines with a `- ` marker per item",
117        )
118        .with_concept_fallback(
119            "ordered-list",
120            "flattened to plain lines with a `N. ` marker per item",
121        )
122        .with_concept_fallback("list-item", "rendered as a single plain line")
123        .with_concept_fallback("strong-emphasis", "rendered as unstyled plain text")
124        .with_concept_fallback("emphasis", "rendered as unstyled plain text")
125        .with_concept_fallback("hyperlink", "rendered as its visible text (URL dropped)")
126}
127
128fn markdown_profile(profile: LanguageProfile) -> LanguageProfile {
129    with_supported(
130        profile,
131        [
132            "heading",
133            "paragraph",
134            "bullet-list",
135            "list-item",
136            "strong-emphasis",
137            "emphasis",
138            "hyperlink",
139        ],
140    )
141    .with_concept_fallback(
142        "ordered-list",
143        "rendered with bullet `- ` markers (ordering not preserved by the Markdown profile)",
144    )
145}
146
147fn html_profile(profile: LanguageProfile) -> LanguageProfile {
148    // HTML represents every cross-format concept natively.
149    with_supported(profile, CROSS_FORMAT_CONCEPT_IDS.iter().copied())
150}
151
152fn pdf_profile(profile: LanguageProfile) -> LanguageProfile {
153    with_supported(
154        profile,
155        [
156            "heading",
157            "paragraph",
158            "bullet-list",
159            "ordered-list",
160            "list-item",
161            "strong-emphasis",
162            "emphasis",
163        ],
164    )
165    .with_concept_fallback(
166        "hyperlink",
167        "rendered as its visible text, unstyled (URL dropped)",
168    )
169}
170
171fn docx_profile(profile: LanguageProfile) -> LanguageProfile {
172    with_supported(
173        profile,
174        [
175            "heading",
176            "paragraph",
177            "bullet-list",
178            "ordered-list",
179            "list-item",
180            "strong-emphasis",
181            "emphasis",
182        ],
183    )
184    .with_concept_fallback(
185        "hyperlink",
186        "rendered as its visible text, unstyled (URL dropped)",
187    )
188}