diff --git a/LOG.md b/LOG.md index bd8b9f9..e334869 100644 --- a/LOG.md +++ b/LOG.md @@ -2,6 +2,16 @@ Questo file documenta gli avanzamenti significativi e le decisioni chiave del progetto `normattiva_2_md`. +## 2026-06-05 + +### Feature: --no-notes flag (issue #24) + +- Aggiunto `--no-notes` alla CLI: esclude i blocchi `AGGIORNAMENTO (N)` dall'output Markdown +- Aggiunto `no_notes=False` a `convert_url()`, `convert_xml()` e funzioni interne dell'API +- Il parametro percorre tutta la catena: `generate_markdown_text` → `process_content_with_paragraphs` +- I marcatori `(( ))` da `/` rimangono invariati (non sono note storiche) +- Aggiunti 6 test in `tests/test_no_notes.py` + ## 2026-01-30 ### Release v2.1.10: Progressive OpenData Fallback Retry diff --git a/openspec/changes/add-no-notes-flag/proposal.md b/openspec/changes/add-no-notes-flag/proposal.md new file mode 100644 index 0000000..8f37ae5 --- /dev/null +++ b/openspec/changes/add-no-notes-flag/proposal.md @@ -0,0 +1,52 @@ +# Proposal: add-no-notes-flag + +## Summary + +Aggiunge il flag `--no-notes` alla CLI e il parametro `no_notes` all'API pubblica per escludere dall'output Markdown i blocchi di note `AGGIORNAMENTO` presenti nei documenti Akoma Ntoso. + +## Motivazione + +Issue #24: chi usa normattiva2md con sistemi RAG (Retrieval-Augmented Generation) segnala che i blocchi `AGGIORNAMENTO (N)` — testo descrittivo delle modifiche storiche, non testo normativo vigente — creano rumore nel retrieval. Serve un modo per ottenere solo il testo della norma corrente. + +## Scope + +Due capability coinvolte: + +| Capability | Tipo di modifica | +|---|---| +| `cli-interface` | MODIFIED — aggiunge `--no-notes` | +| `markdown-conversion` | MODIFIED — aggiunge `no_notes` nelle funzioni di conversione | + +## Comportamento atteso + +Con `--no-notes`: +- I blocchi `AGGIORNAMENTO (N)` e il testo che li segue all'interno dello stesso elemento `` vengono omessi dall'output. +- I marcatori `(( testo modificato ))` da `/` **rimangono**: fanno parte del testo vigente, non delle note storiche. + +Senza `--no-notes` (default): comportamento invariato — nessuna regressione. + +## Architettura della modifica + +Il parametro `no_notes` percorre la catena di chiamate esistente: + +``` +CLI (--no-notes) + → api.py: convert_url(no_notes=...) / convert_xml(no_notes=...) + → _convert_xml_internal(no_notes=...) + → generate_markdown_text(no_notes=...) + → generate_markdown_fragments(no_notes=...) + → extract_body_fragments(no_notes=...) + → process_content_with_paragraphs(no_notes=...) +``` + +Punto di filtraggio: in `process_content_with_paragraphs()` (markdown_converter.py:222), quando `no_notes=True`, i paragrafi che iniziano con `"AGGIORNAMENTO"` vengono saltati. + +## Fuori scope + +- Rimozione dei marcatori `(( ))` (potrebbero essere una feature separata futura). +- `convert_akomantoso_to_markdown_improved()` nella CLI legacy — da aggiornare solo se ancora usata direttamente. + +## Impatto stimato + +- Basso: ~30 righe di codice, nessuna dipendenza esterna. +- Test: aggiungere uno scenario nel test suite esistente con un file XML che contiene blocchi AGGIORNAMENTO. diff --git a/openspec/changes/add-no-notes-flag/specs/cli-interface/spec.md b/openspec/changes/add-no-notes-flag/specs/cli-interface/spec.md new file mode 100644 index 0000000..ce9a821 --- /dev/null +++ b/openspec/changes/add-no-notes-flag/specs/cli-interface/spec.md @@ -0,0 +1,33 @@ +# cli-interface — Delta Spec + +## MODIFIED Requirements + +### Requirement: Notes Filter Parameter +The system SHALL provide a `--no-notes` flag to exclude AGGIORNAMENTO annotation blocks from the Markdown output. + +#### Scenario: Basic usage with local XML file +- **WHEN** user runs `normattiva2md --no-notes input.xml output.md` +- **THEN** output SHALL not contain any paragraph starting with `AGGIORNAMENTO` +- **AND** all other article text SHALL be preserved unchanged +- **AND** inline modifications `(( ... ))` SHALL remain in the output + +#### Scenario: Usage with normattiva.it URL +- **WHEN** user runs `normattiva2md --no-notes "https://www.normattiva.it/..." output.md` +- **THEN** conversion SHALL download the XML normally +- **AND** output SHALL exclude AGGIORNAMENTO blocks +- **AND** exit code SHALL be 0 + +#### Scenario: Default behavior unchanged +- **WHEN** user runs `normattiva2md input.xml output.md` without `--no-notes` +- **THEN** AGGIORNAMENTO blocks SHALL appear in the output as before +- **AND** no behavioral change from current version + +#### Scenario: Combination with --art +- **WHEN** user runs `normattiva2md --no-notes --art 4 input.xml` +- **THEN** output SHALL contain only article 4 +- **AND** AGGIORNAMENTO blocks within article 4 SHALL be excluded + +#### Scenario: Help documentation +- **WHEN** user runs `normattiva2md --help` +- **THEN** `--no-notes` SHALL be listed in the options table +- **AND** description SHALL explain that it excludes AGGIORNAMENTO annotation blocks diff --git a/openspec/changes/add-no-notes-flag/specs/markdown-conversion/spec.md b/openspec/changes/add-no-notes-flag/specs/markdown-conversion/spec.md new file mode 100644 index 0000000..60727fa --- /dev/null +++ b/openspec/changes/add-no-notes-flag/specs/markdown-conversion/spec.md @@ -0,0 +1,35 @@ +# markdown-conversion — Delta Spec + +## MODIFIED Requirements + +### Requirement: Notes Filter in Conversion Functions +The conversion API SHALL accept a `no_notes` parameter that, when `True`, omits AGGIORNAMENTO annotation blocks from the generated Markdown. + +#### Scenario: convert_url with no_notes=True +- **WHEN** `convert_url(url, no_notes=True)` is called +- **THEN** returned `ConversionResult.markdown` SHALL not contain paragraphs starting with `AGGIORNAMENTO` +- **AND** inline `(( ... ))` modifications SHALL be preserved +- **AND** all metadata in `ConversionResult` SHALL be unchanged + +#### Scenario: convert_xml with no_notes=True +- **WHEN** `convert_xml(xml_path, no_notes=True)` is called +- **THEN** returned `ConversionResult.markdown` SHALL not contain AGGIORNAMENTO blocks +- **AND** conversion SHALL complete without errors + +#### Scenario: Default value preserves backward compatibility +- **WHEN** `convert_url(url)` or `convert_xml(xml_path)` is called without `no_notes` +- **THEN** behavior SHALL be identical to current version +- **AND** no AGGIORNAMENTO content SHALL be removed + +#### Scenario: Parameter propagation through internal functions +- **WHEN** `no_notes=True` is passed to `convert_url` or `convert_xml` +- **THEN** `no_notes` SHALL be forwarded to `_convert_xml_internal` +- **AND** from there to `generate_markdown_text` +- **AND** from there to `generate_markdown_fragments` and `extract_body_fragments` +- **AND** ultimately to `process_content_with_paragraphs` where filtering occurs + +#### Scenario: Filtering scope limited to AGGIORNAMENTO paragraphs +- **WHEN** `no_notes=True` and an article contains both normal paragraphs and AGGIORNAMENTO blocks +- **THEN** only paragraphs whose text starts with `AGGIORNAMENTO` SHALL be omitted +- **AND** all other paragraph text SHALL appear in the output unchanged +- **AND** `/` wrapped text (rendered as `(( ... ))`) SHALL not be affected diff --git a/openspec/changes/add-no-notes-flag/tasks.md b/openspec/changes/add-no-notes-flag/tasks.md new file mode 100644 index 0000000..be5038a --- /dev/null +++ b/openspec/changes/add-no-notes-flag/tasks.md @@ -0,0 +1,20 @@ +# Tasks: add-no-notes-flag + +## Implementation + +- [ ] **1. markdown_converter.py** — aggiunge `no_notes=False` a `process_content_with_paragraphs()`: salta i paragrafi che iniziano con `"AGGIORNAMENTO"` quando `no_notes=True` +- [ ] **2. markdown_converter.py** — propaga `no_notes` attraverso la catena: `generate_markdown_text` → `generate_markdown_fragments` → `extract_body_fragments` → `process_body_element` → `process_article` → `process_content_with_paragraphs` +- [ ] **3. api.py** — aggiunge `no_notes=False` a `convert_url()`, `convert_xml()`, `_convert_xml_internal()`; passa il parametro a `generate_markdown_text()` +- [ ] **4. cli.py** — aggiunge `--no-notes` ad argparse; passa `no_notes` alla funzione di conversione; aggiorna la tabella help Rich + +## Validation + +- [ ] **5. Test** — aggiunge test in `tests/` con file XML contenente blocchi AGGIORNAMENTO: verifica che con `no_notes=True` siano assenti nell'output e con `no_notes=False` siano presenti +- [ ] **6. Smoke test CLI** — `normattiva2md --no-notes test_data/20050516_005G0104_VIGENZA_20250130.xml` non deve contenere "AGGIORNAMENTO" nell'output + +## Notes + +- Task 1 e 2 sono dipendenti (fare in sequenza). +- Task 3 dipende da 1+2. +- Task 4 dipende da 3. +- Task 5 e 6 parallelizzabili dopo task 4. diff --git a/pyproject.toml b/pyproject.toml index 13729a9..94f4839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "normattiva2md" -version = "2.1.10" +version = "2.1.11" description = "Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)" readme = "README.md" requires-python = ">=3.7" diff --git a/setup.py b/setup.py index 13044ad..8b881ce 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def read_readme(): setup( name="normattiva2md", - version="2.1.10", + version="2.1.11", description="Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)", long_description=read_readme(), long_description_content_type="text/markdown", diff --git a/src/normattiva2md/api.py b/src/normattiva2md/api.py index 7e2d3ce..dacbaa7 100644 --- a/src/normattiva2md/api.py +++ b/src/normattiva2md/api.py @@ -58,6 +58,7 @@ def convert_url( with_urls: bool = False, force_opendata: bool = False, quiet: bool = False, + no_notes: bool = False, ) -> Optional[ConversionResult]: """ Converte documento da URL normattiva.it a Markdown. @@ -160,6 +161,7 @@ def convert_url( with_urls=with_urls, metadata=metadata, quiet=quiet, + no_notes=no_notes, ) if result: @@ -182,6 +184,7 @@ def convert_xml( with_urls: bool = False, metadata: Optional[Dict] = None, quiet: bool = False, + no_notes: bool = False, ) -> Optional[ConversionResult]: """ Converte file XML locale a Markdown. @@ -225,6 +228,7 @@ def convert_xml( with_urls=with_urls, metadata=metadata, quiet=quiet, + no_notes=no_notes, ) @@ -234,6 +238,7 @@ def _convert_xml_internal( with_urls: bool = False, metadata: Optional[Dict] = None, quiet: bool = False, + no_notes: bool = False, ) -> Optional[ConversionResult]: """ Internal conversion function used by both convert_url and convert_xml. @@ -284,6 +289,7 @@ def _convert_xml_internal( ns=AKN_NAMESPACE, metadata=metadata, cross_references=cross_references, + no_notes=no_notes, ) if not quiet: diff --git a/src/normattiva2md/cli.py b/src/normattiva2md/cli.py index e7b5e18..7d61a74 100644 --- a/src/normattiva2md/cli.py +++ b/src/normattiva2md/cli.py @@ -142,6 +142,7 @@ def print_rich_help(): "--opendata", "Forza download Akoma Ntoso via API OpenData (ZIP AKN)" ) proc_table.add_row("--with-references", "Scarica anche leggi citate") + proc_table.add_row("--no-notes", "Esclude blocchi AGGIORNAMENTO dall'output") proc_table.add_row("--provvedimenti", "Esporta provvedimenti attuativi CSV") proc_table.add_row("--validate", "Validazione strutturale del Markdown") console.print(proc_table) @@ -410,6 +411,11 @@ def main(): action="store_true", help="Genera link Markdown agli URL originali di normattiva.it per gli articoli citati", ) + parser.add_argument( + "--no-notes", + action="store_true", + help="Esclude i blocchi AGGIORNAMENTO (note storiche delle modifiche) dall'output Markdown", + ) parser.add_argument( "--opendata", action="store_true", @@ -753,6 +759,7 @@ def main(): metadata=metadata, article_ref=article_ref, with_urls=args.with_urls, + no_notes=args.no_notes, ) if success: @@ -817,6 +824,7 @@ def main(): metadata=None, article_ref=article_filter_eid, with_urls=args.with_urls, + no_notes=args.no_notes, ) if success: diff --git a/src/normattiva2md/markdown_converter.py b/src/normattiva2md/markdown_converter.py index 33fd13c..f58d4e6 100644 --- a/src/normattiva2md/markdown_converter.py +++ b/src/normattiva2md/markdown_converter.py @@ -187,48 +187,58 @@ def clean_text_content(element, cross_references=None): return cleaned_text -def process_content_with_paragraphs(content_element, ns, cross_references=None): +def process_content_with_paragraphs(content_element, ns, cross_references=None, no_notes=False): """ Process a element that may contain multiple

elements. Treats each

as a block-level element with appropriate spacing. - + Args: content_element: The XML element ns: XML namespace cross_references: Optional cross-reference mapping - + no_notes: If True, skip AGGIORNAMENTO annotation blocks + Returns: str: Processed text with proper line breaks between paragraphs """ if content_element is None: return "" - + # Check if content has

children p_elements = content_element.findall("./akn:p", ns) - + if not p_elements: # No

children, process normally return clean_text_content(content_element, cross_references) - + # Process each

separately parts = [] + skip_aggiornamento = False for p_elem in p_elements: p_text = clean_text_content(p_elem, cross_references).strip() - + # Skip empty, dash-only, or parentheses-only lines if not p_text or re.match(r"^[-()]+$", p_text): continue - + # Check if this is an AGGIORNAMENTO header if p_text.startswith("AGGIORNAMENTO"): + if no_notes: + skip_aggiornamento = True + continue + skip_aggiornamento = False parts.append(f"\n\n{p_text}") else: + if skip_aggiornamento and no_notes: + # Content paragraphs following an AGGIORNAMENTO header are also notes + continue + skip_aggiornamento = False # Regular text - add with space if not first if parts: parts.append(f" {p_text}") else: parts.append(p_text) - + return "".join(parts) def convert_akomantoso_to_markdown_improved( @@ -238,6 +248,7 @@ def convert_akomantoso_to_markdown_improved( article_ref=None, cross_references=None, with_urls=False, + no_notes=False, ): try: # If with_urls is enabled, build cross-reference mapping from tags @@ -293,7 +304,7 @@ def convert_akomantoso_to_markdown_improved( metadata = extract_metadata_from_xml(root) markdown_fragments = generate_markdown_fragments( - root, AKN_NAMESPACE, metadata, cross_references + root, AKN_NAMESPACE, metadata, cross_references, no_notes=no_notes ) except ET.ParseError as e: print(f"Errore durante il parsing del file XML: {e}", file=sys.stderr) @@ -337,7 +348,7 @@ def convert_akomantoso_to_markdown_improved( print(f"Errore durante la scrittura del file Markdown: {e}", file=sys.stderr) return False -def generate_markdown_fragments(root, ns, metadata=None, cross_references=None): +def generate_markdown_fragments(root, ns, metadata=None, cross_references=None, no_notes=False): """Build the markdown fragments for a parsed Akoma Ntoso document.""" fragments = [] @@ -347,8 +358,8 @@ def generate_markdown_fragments(root, ns, metadata=None, cross_references=None): # Generate body content preamble_fragments = extract_preamble_fragments(root, ns, cross_references) - body_elements_fragments = extract_body_fragments(root, ns, cross_references) - attachment_fragments = extract_attachments_fragments(root, ns, cross_references) + body_elements_fragments = extract_body_fragments(root, ns, cross_references, no_notes=no_notes) + attachment_fragments = extract_attachments_fragments(root, ns, cross_references, no_notes=no_notes) body_fragments = [] body_fragments.extend(preamble_fragments) @@ -373,11 +384,11 @@ def generate_markdown_fragments(root, ns, metadata=None, cross_references=None): return fragments def generate_markdown_text( - root, ns=AKN_NAMESPACE, metadata=None, cross_references=None + root, ns=AKN_NAMESPACE, metadata=None, cross_references=None, no_notes=False ): """Return the Markdown rendering for the provided Akoma Ntoso root.""" - return "".join(generate_markdown_fragments(root, ns, metadata, cross_references)) + return "".join(generate_markdown_fragments(root, ns, metadata, cross_references, no_notes=no_notes)) def extract_document_title(root, ns): """Convert the `` element to a Markdown H1 if present.""" @@ -407,7 +418,7 @@ def extract_preamble_fragments(root, ns, cross_references=None): fragments.append(f"{text}\n\n") return fragments -def extract_body_fragments(root, ns, cross_references=None): +def extract_body_fragments(root, ns, cross_references=None, no_notes=False): """Traverse body nodes and delegate conversion to specialised handlers.""" fragments = [] @@ -416,11 +427,11 @@ def extract_body_fragments(root, ns, cross_references=None): return fragments for element in body: - fragments.extend(process_body_element(element, ns, cross_references)) + fragments.extend(process_body_element(element, ns, cross_references, no_notes=no_notes)) return fragments -def extract_attachments_fragments(root, ns, cross_references=None): +def extract_attachments_fragments(root, ns, cross_references=None, no_notes=False): """Collect attachment fragments that live outside the main body.""" fragments = [] @@ -434,30 +445,30 @@ def extract_attachments_fragments(root, ns, cross_references=None): fragments.append("## Allegati\n\n") for attachment in attachment_elements: - fragments.extend(process_attachment(attachment, ns, cross_references)) + fragments.extend(process_attachment(attachment, ns, cross_references, no_notes=no_notes)) return fragments -def process_body_element(element, ns, cross_references=None): +def process_body_element(element, ns, cross_references=None, no_notes=False): """Process a direct child of `` producing Markdown fragments.""" if element.tag.endswith("title"): - return process_title(element, ns, cross_references) + return process_title(element, ns, cross_references, no_notes=no_notes) if element.tag.endswith("part"): - return process_part(element, ns, cross_references) + return process_part(element, ns, cross_references, no_notes=no_notes) if element.tag.endswith("chapter"): - return process_chapter(element, ns, cross_references) + return process_chapter(element, ns, cross_references, no_notes=no_notes) if element.tag.endswith("article"): article_fragments = [] process_article( - element, article_fragments, ns, level=2, cross_references=cross_references + element, article_fragments, ns, level=2, cross_references=cross_references, no_notes=no_notes ) return article_fragments if element.tag.endswith("attachment"): - return process_attachment(element, ns, cross_references) + return process_attachment(element, ns, cross_references, no_notes=no_notes) return [] -def process_chapter(chapter_element, ns, cross_references=None): +def process_chapter(chapter_element, ns, cross_references=None, no_notes=False): """ Convert a chapter element to Markdown fragments with proper hierarchy. @@ -502,7 +513,7 @@ def process_chapter(chapter_element, ns, cross_references=None): # Process child elements for child in chapter_element: if child.tag.endswith("section"): - chapter_fragments.extend(process_section(child, ns, cross_references)) + chapter_fragments.extend(process_section(child, ns, cross_references, no_notes=no_notes)) elif child.tag.endswith("article"): process_article( child, @@ -510,11 +521,12 @@ def process_chapter(chapter_element, ns, cross_references=None): ns, level=article_level, cross_references=cross_references, + no_notes=no_notes, ) return chapter_fragments -def process_section(section_element, ns, cross_references=None): +def process_section(section_element, ns, cross_references=None, no_notes=False): """Convert a section element and its articles to Markdown fragments.""" section_fragments = [] @@ -530,10 +542,11 @@ def process_section(section_element, ns, cross_references=None): ns, level=4, cross_references=cross_references, + no_notes=no_notes, ) return section_fragments -def process_title(title_element, ns, cross_references=None): +def process_title(title_element, ns, cross_references=None, no_notes=False): """ Convert a title element to Markdown H2 heading. Titles are top-level structural elements. @@ -547,7 +560,7 @@ def process_title(title_element, ns, cross_references=None): # Process any nested content (chapters, articles, etc.) for child in title_element: if child.tag.endswith("chapter"): - title_fragments.extend(process_chapter(child, ns, cross_references)) + title_fragments.extend(process_chapter(child, ns, cross_references, no_notes=no_notes)) elif child.tag.endswith("article"): process_article( child, @@ -555,11 +568,12 @@ def process_title(title_element, ns, cross_references=None): ns, level=3, cross_references=cross_references, + no_notes=no_notes, ) return title_fragments -def process_part(part_element, ns, cross_references=None): +def process_part(part_element, ns, cross_references=None, no_notes=False): """ Convert a part element to Markdown fragments. Parts are major structural divisions, rendered as H3. @@ -573,7 +587,7 @@ def process_part(part_element, ns, cross_references=None): # Process nested content (chapters, articles, etc.) for child in part_element: if child.tag.endswith("chapter"): - part_fragments.extend(process_chapter(child, ns, cross_references)) + part_fragments.extend(process_chapter(child, ns, cross_references, no_notes=no_notes)) elif child.tag.endswith("article"): process_article( child, @@ -581,11 +595,12 @@ def process_part(part_element, ns, cross_references=None): ns, level=3, cross_references=cross_references, + no_notes=no_notes, ) return part_fragments -def process_attachment(attachment_element, ns, cross_references=None): +def process_attachment(attachment_element, ns, cross_references=None, no_notes=False): """ Convert an attachment element to Markdown fragments. Attachments are rendered as a separate section. @@ -633,7 +648,7 @@ def process_attachment(attachment_element, ns, cross_references=None): for paragraph in paragraphs: # Process paragraph content with support for multiple

elements para_content = paragraph.find("./akn:content", ns) - text = process_content_with_paragraphs(para_content, ns, cross_references) if para_content is not None else "" + text = process_content_with_paragraphs(para_content, ns, cross_references, no_notes=no_notes) if para_content is not None else "" if text and clean_heading: normalized = re.sub(r"\s+", " ", text).strip() @@ -751,7 +766,7 @@ def process_table(table_element, ns, cross_references=None): return markdown_table def process_article( - article_element, markdown_content_list, ns, level=2, cross_references=None + article_element, markdown_content_list, ns, level=2, cross_references=None, no_notes=False ): article_num_element = article_element.find("./akn:num", ns) article_heading_element = article_element.find("./akn:heading", ns) @@ -811,7 +826,7 @@ def process_article( else: # Handle regular paragraph content paragraph_text = ( - process_content_with_paragraphs(para_content_element, ns, cross_references) + process_content_with_paragraphs(para_content_element, ns, cross_references, no_notes=no_notes) if para_content_element is not None else "" ) diff --git a/tests/test_cli_validation.py b/tests/test_cli_validation.py index 327592e..49571dc 100644 --- a/tests/test_cli_validation.py +++ b/tests/test_cli_validation.py @@ -29,6 +29,7 @@ def test_validate_flag_triggers_validation(self, mock_cli_open, mock_exists, moc completo=False, with_references=False, with_urls=False, + no_notes=False, provvedimenti=False, debug_search=False, auto_select=True, diff --git a/tests/test_no_notes.py b/tests/test_no_notes.py new file mode 100644 index 0000000..33999e9 --- /dev/null +++ b/tests/test_no_notes.py @@ -0,0 +1,72 @@ +import unittest +import xml.etree.ElementTree as ET +from pathlib import Path + +from normattiva2md.markdown_converter import generate_markdown_text, process_content_with_paragraphs + +AKN_NS = "http://docs.oasis-open.org/legaldocml/ns/akn/3.0" +NS = {"akn": AKN_NS} + +FIXTURE_PATH = ( + Path(__file__).resolve().parents[1] + / "test_data" + / "20050516_005G0104_VIGENZA_20250130.xml" +) + + +class NoNotesTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + tree = ET.parse(FIXTURE_PATH) + cls.root = tree.getroot() + + def test_aggiornamento_present_by_default(self): + md = generate_markdown_text(self.root, no_notes=False) + self.assertIn("AGGIORNAMENTO", md) + + def test_aggiornamento_absent_with_no_notes(self): + md = generate_markdown_text(self.root, no_notes=True) + self.assertNotIn("AGGIORNAMENTO", md) + + def test_double_parens_preserved_with_no_notes(self): + """Inline modifications (( )) must survive --no-notes.""" + md = generate_markdown_text(self.root, no_notes=True) + self.assertIn("((", md) + + def test_process_content_skips_aggiornamento(self): + xml_str = ( + f'' + "

------------

" + "

AGGIORNAMENTO (1)

" + "

Testo della nota storica.

" + "
" + ) + elem = ET.fromstring(xml_str) + result = process_content_with_paragraphs(elem, NS, no_notes=True) + self.assertEqual(result, "") + + def test_process_content_keeps_regular_text(self): + xml_str = ( + f'' + "

Testo normativo vigente.

" + "
" + ) + elem = ET.fromstring(xml_str) + result = process_content_with_paragraphs(elem, NS, no_notes=True) + self.assertIn("Testo normativo vigente.", result) + + def test_process_content_default_includes_aggiornamento(self): + xml_str = ( + f'' + "

AGGIORNAMENTO (5)

" + "

Nota.

" + "
" + ) + elem = ET.fromstring(xml_str) + result = process_content_with_paragraphs(elem, NS, no_notes=False) + self.assertIn("AGGIORNAMENTO", result) + + +if __name__ == "__main__": + unittest.main()