diff --git a/Examples/Document Configuration/report.yml b/Examples/Document Configuration/report.yml index 016bf59..4619307 100644 --- a/Examples/Document Configuration/report.yml +++ b/Examples/Document Configuration/report.yml @@ -9,15 +9,17 @@ _style: Customizing your document configuration and styling: - > There are `three` categories of document configuration - - 1: The document template - 2: Text styles - 3: Table styles - + - _ol: + - The document template + - Text styles + - Table styles + - > These three categories are set at three different levels of priority - - 1: The internal defaults (lowest priority) - 2: Project config - 3: Document internal config (highest priority) + - _ol: + - The internal defaults (lowest priority) + - Project config + - Document internal config (highest priority) - > Each level of priority in the configuration heirarchy effectively inherits the configuration of the level of priority above it. @@ -27,10 +29,11 @@ Customizing your document configuration and styling: - The document template (_doc): - > The template controls settings such as - - - Page size - - Margins - - PDF background file path - - And whether or not you have a separate set of the above settings applying to the first page of your document or not. + - _ul: + - Page size + - Margins + - PDF background file path + - And whether or not you have a separate set of the above settings applying to the first page of your document or not. - > Currently, the document template is internally configured to only use a single content frame. This limitation would only affect you if you wanted to do something like a two-column layout (where text flows into the second column on the same page once the first one is full). @@ -39,39 +42,36 @@ Customizing your document configuration and styling: - > With text styles, you can control the body text - Text styling attributes: - - Font family - - Text color - - Size - - Line spacing ratio - - Bullet style: - - Bullet color - - Bullet symbol heirarchy - - Bullet indentation - + _ul: + - Font family + - Text color + - Size + - Line spacing ratio + - "Bullet style:" + - - Bullet color + - Bullet symbol heirarchy + - Bullet indentation + - > You can also control the text heading styles separately from the body text by adjusting their - - - - Font family - - Text color - - Typographic ratio (in musical intervals, e.g. "minor second" or "major third") + + - _ul: + - Font family + - Text color + - Typographic ratio (in musical intervals, e.g. "minor second" or "major third") - Table styles (_tablestyle): - > You can control how your tables are displayed by modifying the table style. With the _tablestyle key, you can control - - Table header - - background (color) - - gridlines (above, below, between) - - text - - font - - size - - color - - Table rows - - even (rows, background color) - - odd (rows, background color) - - gridlines (above, below, between) - - text - - font - - size - - color + - _ul: + - "Table header:" + - - background (color) + - gridlines (above, below, between) + - "text: font, size, color" + - "Table rows:" + - - even (rows, background color) + - odd (rows, background color) + - gridlines (above, below, between) + - "text: font, size, color" diff --git a/Examples/Document variables/report.yml b/Examples/Document variables/report.yml index 7e02423..90a2d1a 100644 --- a/Examples/Document variables/report.yml +++ b/Examples/Document variables/report.yml @@ -15,14 +15,13 @@ Document Variables (_vars): representation is shown (i.e. the result of str(var)). This document has five variables defined, a, b, c, d, e. - - _spacer: 0 # The use of a "0-spacer" is a hack to prevent the paragraph above being interpreted as a bullet point. - - - - a = {{a}} - - b = {{b}} - - c = {{c}} - - d = {{d}} - - e = {{e.key2}} - - f = {{f}} + - _ul: + - a = {{a}} + - b = {{b}} + - c = {{c}} + - d = {{d}} + - e = {{e.key2}} + - f = {{f}} - Passing Python Objects: > It is also possible to pass variable data as Python objects to blocks (see "YMPrint blocks") using the $var syntax (similar to bash). diff --git a/Examples/PDF Backgrounds/report.yml b/Examples/PDF Backgrounds/report.yml index e1293e6..dd96297 100644 --- a/Examples/PDF Backgrounds/report.yml +++ b/Examples/PDF Backgrounds/report.yml @@ -26,6 +26,7 @@ Custom styling with PDF backgrounds: Form fields are _automatically_ populated when a document variable name exactly matches the PDF form field name. If you want to avoid this behaviour: - - Make sure that none of your variable names match your field names (i.e. using a special naming convention for your form field names to prevent accidental clashes) - - Do not use form fields + _ul: + - Make sure that none of your variable names match your field names (i.e. using a special naming convention for your form field names to prevent accidental clashes) + - Do not use form fields Example: The form fields below have been populated from the document vars \ No newline at end of file diff --git a/Examples/Simple example/report.yml b/Examples/Simple example/report.yml index 06ba833..d395398 100644 --- a/Examples/Simple example/report.yml +++ b/Examples/Simple example/report.yml @@ -17,19 +17,20 @@ Report title: - Standard content types: - Bullets: - - Bullet 1 - - Bullet 2 - - Bullet 3 - - - Bullet 4 - - Bullet 5 - - Bullet 6 + _ul: + - Bullet 1 + - Bullet 2 + - Bullet 3 + - - Bullet 4 + - Bullet 5 + - Bullet 6 - Ordered list: - 1: First Item - 2: Second item - 3: - 12: Fourth item, - 6: Fifth item, - 10: Sixth Item + _ol: + - First item + - Second item + - - Nested first + - Nested second + - Nested third - Tables: - Item Number: 12.01 Description: There is a problem here. This report documents it. diff --git a/Examples/YMPrint blocks/report.yml b/Examples/YMPrint blocks/report.yml index c4d4fcf..e1f3382 100644 --- a/Examples/YMPrint blocks/report.yml +++ b/Examples/YMPrint blocks/report.yml @@ -16,12 +16,12 @@ Using Blocks: scale_ratio: 0.3 - Admonitions: - "The following admonitions blocks are available:" - - - - _info - - _warning - - _danger - - _tip - - _note + - _ul: + - _info + - _warning + - _danger + - _tip + - _note - They are rendered below _info: Here is an "info" admonition @@ -108,10 +108,10 @@ Using Blocks: for k in [a, b, c]: acc.append(k) - "Now, the values of the variables a, b, and c can be included in the document:" - - - - a = {{py1.a}} (using py1.a) - - b = {{py1.b}} (using py1.b) - - c = {{py1.c}} (using py1.c) + - _ul: + - a = {{py1.a}} (using py1.a) + - b = {{py1.b}} (using py1.b) + - c = {{py1.c}} (using py1.c) - _code: source: | yaml_data: is being shown @@ -132,6 +132,6 @@ Using Blocks: path: extra_vars.json namespace: extra_vars - "Here are the values of the vars contained within the 'extravars' namespace:" - - - - bn = {{extra_vars.bn}} - - dx = {{extra_vars.dx}} + - _ul: + - bn = {{extra_vars.bn}} + - dx = {{extra_vars.dx}} diff --git a/design/explicit-list-syntax.md b/design/explicit-list-syntax.md new file mode 100644 index 0000000..40adf83 --- /dev/null +++ b/design/explicit-list-syntax.md @@ -0,0 +1,100 @@ +# Design: Explicit list syntax (`_ul` / `_ol`) + +Status: prototype +Branch: `features/explicit-list-syntax` (off `main`) + +## Problem + +A YAML list under a heading is structurally identical whether the author means +"several paragraphs" or "an unordered list". Today YMPrint guesses from the list's +*contents*: + +- a **pure** list of strings → `convert_ul` → bullets +- a **mixed** list (strings + block/subsection dicts) → each string becomes a + paragraph + +So the same `heading:\n - >prose` renders as a bullet or a paragraph depending on +what else is in the list. A single wrapped paragraph under a heading silently comes +out as a one-item bullet list (observed under "Alignment and named styles" in the +text-styles example). + +The ambiguity is **structural** — YAML gives identical structure to both intents — +so no heuristic can resolve it. One of the two meanings must be made explicit. + +## Decision + +Make **bullets explicit** and let a bare list mean "a sequence of content items". + +- A YAML list is always a *sequence of blocks*: strings become paragraphs, mappings + become subsections (heading + content), in order. It is never auto-bulleted. +- Unordered lists are written with the **`_ul`** block; ordered lists with **`_ol`**. +- The implicit detections (`check_for_nested_lists` → bullets, and the + dict-with-integer-keys → numbered list) are retired from dispatch. The check + functions remain (still unit-tested) but no longer drive `build_story`. + +This yields a single rule with no guessing: *a list is content in order; bullets and +numbers are named constructs.* + +### Rejected alternative + +Making **paragraphs** explicit (`_p`) instead would avoid breaking existing bullet +lists, but it keeps the surprising default (bare list = bullets) and only adds an +escape hatch beside the ambiguity rather than removing it. + +## Syntax + +```yaml +Findings: + - The inspection covered three areas. # paragraph + - _ul: # unordered list + - The handrail is loose on the north stair. + - Two ceiling tiles are water-stained. + - - a nested sub-point # nested list → sub-bullets + - another sub-point + - Recommended actions: # subsection heading + _ol: # ordered list + - Re-secure the handrail. + - Replace the stained tiles. +``` + +- `_ul` value is a list; nested lists produce sub-bullets (unchanged `convert_ul`). +- `_ol` value is a list; numbering is automatic by position; nested lists produce + nested numbering. (`convert_ol` also still accepts a mapping for back-compat.) +- Both may be written as a list item (`- _ul: [...]`) or as the value of a heading + key (`heading:\n _ul: [...]`). Suffixes are allowed for uniqueness in a mapping + (`_ul_left`, `_ol_steps`), consistent with other block codes. + +## Implementation + +`_ul` / `_ol` are intercepted **directly in `build_story`**, not registered in the +block registry. Two reasons: + +1. They are structural (they change how a list is interpreted), sitting naturally + beside the list-dispatch logic. +2. It keeps them **forward-compatible with scoped text styles**: `build_story` is + where a `current_style` parameter lives (on the `features/scoped-text-styles` + branch), so intercepting here lets `_ul`/`_ol` pass the active style into + `convert_ul`/`convert_ol`. Routing them through the generic block registry — + whose converters do not receive the active style — would make bullets ignore the + surrounding `_textstyle` scope. (On this `main`-based branch there is no + `current_style` yet; the interception point is chosen so the two features compose + cleanly when merged.) + +| File | Change | +| --- | --- | +| `story_builder.py` | Intercept `_ul`/`_ol` (list-item and heading-value forms, with suffixes) → `convert_ul`/`convert_ol`. Replace the implicit bullet/ordered dispatch: a bare list/mapping now always recurses (strings → paragraphs, mappings → subsections). | +| `content_converters.py` | `convert_ol` accepts a **list** (positional numbering; nested lists nest) as well as a mapping (back-compat). | +| test data / examples | Migrate bare-list bullets and integer-keyed ordered lists to `_ul` / `_ol`. Genuine multi-paragraph lists (e.g. report 2 "third topic") are left as lists and now render as paragraphs — the intended fix. | + +## Backward compatibility + +This is a **breaking** content change (consistent with the pre-1.0 status and the +earlier multi-page-template change): existing documents that relied on bare lists for +bullets, or integer-keyed mappings for numbered lists, must adopt `_ul` / `_ol`. + +## Open questions + +- Should `_ol` support an explicit `start:` offset or custom markers (a/i/…)? +- Should list items be allowed to contain blocks (e.g. an image inside a bullet)? +- When merged with scoped text styles, thread `current_style` into the `_ul`/`_ol` + interception so bullets honour the active family. diff --git a/docs/guide/document-structure.md b/docs/guide/document-structure.md index b9e5b3a..ccb7b7a 100644 --- a/docs/guide/document-structure.md +++ b/docs/guide/document-structure.md @@ -16,8 +16,10 @@ Report title: ``` Because a YAML key can only have **one** value, anything with more than a single piece of -content underneath a heading must be written as a **list**. Each list item is rendered in -order. +content underneath a heading must be written as a **list**. A list is a **sequence of +content items rendered in order**: a string becomes a paragraph, a mapping becomes a +sub-heading with its own content. A list is *not* a bulleted list — bullets and numbers are +opt-in via the [`_ul` and `_ol`](#bullet-lists) blocks. ### Heading → paragraph → sub-heading @@ -65,49 +67,55 @@ Inline markdown is supported inside text — see [Inline formatting](#inline-for below. ::: -## Bullet lists +(bullet-lists)= +## Bullet lists — `_ul` -A plain YAML list (items that are *not* mappings) renders as a bulleted list: +A bare list is a sequence of paragraphs/sub-sections (see above), **not** a bulleted list. +To render bullets, wrap the items in a `_ul` block: ```yaml Standard content types: - - Bullets: + _ul: - Bullet 1 - Bullet 2 - Bullet 3 ``` +:::{note} +This is a change from earlier versions, where a plain list of strings was auto-bulleted. +A list on its own now means "these items in order" (paragraphs and sub-sections); bullets +are explicit. This removes the ambiguity where a single wrapped paragraph under a heading +would come out as a one-item bullet. +::: + ### Nested bullets -Nest a list inside a list item to indent bullets. The bullet symbol changes with depth +Nest a list inside a `_ul` item to indent bullets. The bullet symbol changes with depth (the default hierarchy is `•‣⁃∘`): ```yaml -Bullets: +_ul: - Bullet 1 - Bullet 2 - - - Bullet 4 - - Bullet 5 - - Bullet 6 + - - Sub-bullet A + - Sub-bullet B + - Sub-bullet C ``` -## Ordered lists +## Ordered lists — `_ol` -Write a mapping whose keys act as the visible numbers. The keys are used as labels, so you -control the numbering: +Wrap the items in an `_ol` block. Numbering is automatic by position, and nesting a list +inside an item creates a nested numbered list: ```yaml Ordered list: - 1: First Item - 2: Second item - 3: - 12: Fourth item - 6: Fifth item - 10: Sixth Item + _ol: + - First item + - Second item + - - Nested first + - Nested second ``` -Nesting a mapping under an item creates a nested ordered list. - ## Tables A list of mappings becomes a **table**. The keys of the first mapping are the column @@ -179,11 +187,13 @@ Report title: A paragraph and a sub-heading under one top-level heading require a list. - Standard content types: - Bullets: - - Bullet 1 - - Bullet 2 + _ul: + - Bullet 1 + - Bullet 2 - Ordered list: - 1: First Item - 2: Second item + _ol: + - First item + - Second item - Tables: - Item Number: 12.01 Location: Under the stairs diff --git a/docs/reference/blocks.md b/docs/reference/blocks.md index 6c67f44..94eceaf 100644 --- a/docs/reference/blocks.md +++ b/docs/reference/blocks.md @@ -38,6 +38,8 @@ The suffix does not affect how the block is executed. It is simply an optional i | [`_code`](#block-code) | A non-executable code block. | | [`_py`](#block-py) | Execute Python and optionally show the syntax-highlighted source. | | [`_loadjson`](#block-loadjson) | Load variables into the document from a JSON file. | +| [`_ul`](#block-ul) | An unordered (bulleted) list. | +| [`_ol`](#block-ol) | An ordered (numbered) list. | | [`_pagebreak`](#block-pagebreak) | Force a page break. | | [`_hrule`](#block-hrule) | A customizable horizontal rule. | | [`_spacer`](#block-spacer) | Insert vertical whitespace. | @@ -248,6 +250,45 @@ _loadjson: --- +(block-ul)= +## `_ul` — Unordered lists + +A bulleted list. A bare YAML list is a sequence of paragraphs/sub-sections, so bullets are +written explicitly with `_ul`. Nest a list inside an item to indent (the bullet glyph +changes with depth, default hierarchy `•‣⁃∘`). + +```yaml +Findings: + _ul: + - The handrail is loose on the north stair. + - Two ceiling tiles are water-stained in the lobby. + - - a nested sub-point + - another sub-point +``` + +`_ul` works as the value of a heading key (as above) or as a list item +(`- _ul: [...]`). Bullet glyph, colour, and indentation come from `_style.body.bullets` +(see [Configuration → Text styles](#cfg-style)). + +--- + +(block-ol)= +## `_ol` — Ordered lists + +A numbered list. Numbering is automatic by position; nesting a list inside an item creates +a nested numbered list. + +```yaml +Recommended actions: + _ol: + - Re-secure the handrail. + - Replace the stained ceiling tiles. + - - Nested step one + - Nested step two +``` + +--- + (block-pagebreak)= ## `_pagebreak` — Page breaks diff --git a/src/ymprint/content_converters.py b/src/ymprint/content_converters.py index 586ddcb..fedf164 100644 --- a/src/ymprint/content_converters.py +++ b/src/ymprint/content_converters.py @@ -69,7 +69,7 @@ def convert_ul(value: list[str], context: dict, level: int = 0) -> list[ListFlow return [ListFlowable(bullet_contents, start=0, bulletType='bullet', spaceAfter=space_around)] # Test -def convert_ol(value: dict, context: dict, level: int = 0) -> list[ListFlowable]: +def convert_ol(value: list | dict, context: dict, level: int = 0) -> list[ListFlowable]: sheet = context['styles']['rl']['_style'] bullet_style = sheet['body'] ymp_style: ReportStyles = context['styles']['ymprint'] @@ -79,17 +79,22 @@ def convert_ol(value: dict, context: dict, level: int = 0) -> list[ListFlowable] int(bul_color.green), int(bul_color.blue), ) + # Accept a list (positional numbering) or a mapping (back-compat: numbered by + # insertion order, keys ignored). + items = list(value.values()) if isinstance(value, dict) else value bullet_contents = [] - for idx, elem in enumerate(value.values(), start=1): - if isinstance(elem, dict): + number = 1 + for elem in items: + if isinstance(elem, (list, dict)): sub_bullets = convert_ol(elem, context, level=level + 1) bullet_contents.append(sub_bullets) - else: - para_md = convert_inline_markdown(elem) - template = jinja_env.from_string(para_md) - rendered = template.render(context['vars']) - bullet_content = Paragraph(f'{idx}. {rendered}', bullet_style) + continue + para_md = convert_inline_markdown(elem) + template = jinja_env.from_string(para_md) + rendered = template.render(context['vars']) + bullet_content = Paragraph(f'{number}. {rendered}', bullet_style) bullet_contents.append(bullet_content) + number += 1 return [ListFlowable(bullet_contents, start=0, bulletType='bullet')] # Test diff --git a/src/ymprint/story_builder.py b/src/ymprint/story_builder.py index 676bf85..fc54cbf 100644 --- a/src/ymprint/story_builder.py +++ b/src/ymprint/story_builder.py @@ -1,9 +1,8 @@ +import re + from .content_checks import ( check_for_paragraph, - check_for_ordered_nested_lists, - check_for_subelements, check_for_tables, - check_for_nested_lists, check_for_variable, ) from .content_converters import ( @@ -15,6 +14,32 @@ from .exceptions import YMPrintSyntaxException from .blocks import get_block_callable, list_blocks, convert_blocks +# Explicit list constructs. These are intercepted structurally in build_story +# (not registered blocks): a bare YAML list is a sequence of content items, while +# bullets/numbers are opt-in via these codes. A trailing `_suffix` is allowed for +# uniqueness within a mapping, consistent with other block codes. +LIST_BLOCK_PATTERN = re.compile(r"^_(ul|ol)(?:_|$)") + + +def _extract_list_block(k, v): + """ + Returns (kind, items) when this element is a `_ul`/`_ol` construct, else None. + Handles both the heading-value form (`_ul: [...]`) and the single-key list-item + form (`- _ul: [...]`). + """ + key = value = None + if isinstance(k, str) and LIST_BLOCK_PATTERN.match(k): + key, value = k, v + elif k is None and isinstance(v, dict) and len(v) == 1: + (only_key, only_value), = v.items() + if isinstance(only_key, str) and LIST_BLOCK_PATTERN.match(only_key): + key, value = only_key, only_value + if key is None: + return None + kind = LIST_BLOCK_PATTERN.match(key).group(1) + return kind, value + + def build_story(source_data: dict | list, context: dict, level: int = 0) -> list: """ Returns a list of Flowables generated from 'source_data' and 'context' @@ -34,6 +59,17 @@ def build_story(source_data: dict | list, context: dict, level: int = 0) -> list k = None v = elem + # Explicit unordered / ordered lists are structural, intercepted before any + # heading or block dispatch. + list_block = _extract_list_block(k, v) + if list_block is not None: + kind, items = list_block + if kind == "ul": + story.extend(convert_ul(items, context)) + else: + story.extend(convert_ol(items, context)) + continue + if k is not None: heading_level = level if str(k).startswith(tuple(registered_blocks)): @@ -53,19 +89,15 @@ def build_story(source_data: dict | list, context: dict, level: int = 0) -> list "To evaluate a string representation of the variable use the {{VAR}} syntax instead." ) if check_for_paragraph(v, context): - paragraph = convert_paragraph(v,context) + paragraph = convert_paragraph(v, context) story.extend(paragraph) - elif check_for_nested_lists(v, context): - ul = convert_ul(v,context) - story.extend(ul) - elif check_for_ordered_nested_lists(v, context): - ol = convert_ol(v,context) - story.extend(ol) elif check_for_tables(v, context): - table = convert_table(v,context) + table = convert_table(v, context) story.extend(table) - elif check_for_subelements(v, context): - story.extend(build_story(v, context, level = level + 1)) + elif isinstance(v, (list, dict)): + # A bare list/mapping is a sequence of content items: strings become + # paragraphs, mappings become subsections. Bullets/numbers require _ul/_ol. + story.extend(build_story(v, context, level=level + 1)) continue else: continue diff --git a/tests/test-data/example_output1.pdf b/tests/test-data/example_output1.pdf index 94418d2..cc7d26b 100644 Binary files a/tests/test-data/example_output1.pdf and b/tests/test-data/example_output1.pdf differ diff --git a/tests/test-data/example_output2.pdf b/tests/test-data/example_output2.pdf index 0af00b7..785f6c6 100644 Binary files a/tests/test-data/example_output2.pdf and b/tests/test-data/example_output2.pdf differ diff --git a/tests/test-data/example_output3.pdf b/tests/test-data/example_output3.pdf index 6aeaefc..63f9504 100644 Binary files a/tests/test-data/example_output3.pdf and b/tests/test-data/example_output3.pdf differ diff --git a/tests/test-data/filled_forms.pdf b/tests/test-data/filled_forms.pdf index fe5d2eb..48beaa9 100644 Binary files a/tests/test-data/filled_forms.pdf and b/tests/test-data/filled_forms.pdf differ diff --git a/tests/test-data/report_example_1.yml b/tests/test-data/report_example_1.yml index 033a16a..c046427 100644 --- a/tests/test-data/report_example_1.yml +++ b/tests/test-data/report_example_1.yml @@ -36,29 +36,31 @@ title: scale_ratio: 0.5 caption: Figure 1:Here is my figure second topic: - - first bullet point. Result = {{c}} - - Here is a new bullet a = {{a}} - - third bullet point From the vars = {{d}} - - - - subbullet 1 - - subbullet 2 + _ul: + - first bullet point. Result = {{c}} + - Here is a new bullet a = {{a}} + - third bullet point From the vars = {{d}} - - - subbullet 3 - - - - subbuullet 4 - - fourth bullet point + - subbullet 1 + - subbullet 2 + - + - subbullet 3 + - + - subbuullet 4 + - fourth bullet point third topic: - First sub paragraph leading into subheading - Third paragraph here. - subheading: - 1: first numbered item - 2: second numbered item - 4: third numbered item - 5: - 1: Sub item - 2: sub item + _ol: + - first numbered item + - second numbered item + - third numbered item + - + - Sub item + - sub item An actual topic: first sub topic: > Here is another paragraph. diff --git a/tests/test-data/report_example_2.yml b/tests/test-data/report_example_2.yml index 57af91a..b69938b 100644 --- a/tests/test-data/report_example_2.yml +++ b/tests/test-data/report_example_2.yml @@ -24,18 +24,20 @@ title: There is a line break here too. - second topic: - - first bullet point - - second bullet point - - third bullet point + _ul: + - first bullet point + - second bullet point + - third bullet point - _pagebreak: null - third topic: - First sub paragraph leading into subheading - Second sub paragraph leading into subheading - subheading: - 1: first numbered item - 2: second numbered item - 4: third numbered item - 3: fourth numbered item + _ol: + - first numbered item + - second numbered item + - third numbered item + - fourth numbered item fourth topic: first sub topic: > Here is another paragraph. diff --git a/tests/test_content_converters.py b/tests/test_content_converters.py index 85d5339..8b86078 100644 --- a/tests/test_content_converters.py +++ b/tests/test_content_converters.py @@ -35,13 +35,17 @@ def test_convert_paragraph(report_ex1, default_context): assert con.convert_paragraph(data['title']['first topic'], default_context) assert con.convert_paragraph(data['title']['An actual topic']['first sub topic'], default_context) -def test_convert_ul(report_ex1, default_context): - data = report_ex1 - assert con.convert_ul(data['title']['second topic'], default_context) +def test_convert_ul(default_context): + items = ["first bullet", "second bullet", ["nested a", "nested b"]] + assert con.convert_ul(items, default_context) -def test_convert_ol(report_ex1, default_context): - data = report_ex1 - assert con.convert_ol(data['title']['third topic'][2]['subheading'], default_context) +def test_convert_ol_list(default_context): + items = ["first", "second", ["nested one", "nested two"]] + assert con.convert_ol(items, default_context) + +def test_convert_ol_mapping_backcompat(default_context): + items = {1: "first", 2: "second", 3: "third"} + assert con.convert_ol(items, default_context) def test_convert_table(report_ex1, default_context): data = report_ex1 diff --git a/tests/test_explicit_lists.py b/tests/test_explicit_lists.py new file mode 100644 index 0000000..3615e2f --- /dev/null +++ b/tests/test_explicit_lists.py @@ -0,0 +1,93 @@ +import pathlib + +import pytest +from reportlab.platypus import Paragraph, ListFlowable + +from ymprint.story_builder import build_story, _extract_list_block +from ymprint.context_builder import build_context +from ymprint.config.config_loaders import load_report_config + +TEST_DATA = pathlib.Path(__file__).parent / "test-data" + + +@pytest.fixture +def ctx(): + styles, tablestyles, doctemplate = load_report_config() + return build_context( + {}, styles, doctemplate, tablestyles, {}, + pathlib.Path.cwd(), pathlib.Path.cwd(), pathlib.Path.cwd(), + ) + + +def kinds(story): + return [type(f).__name__ for f in story] + + +# --- _extract_list_block ----------------------------------------------------------- + +def test_extract_list_block_heading_value_form(): + assert _extract_list_block("_ul", ["a", "b"]) == ("ul", ["a", "b"]) + assert _extract_list_block("_ol", ["a"]) == ("ol", ["a"]) + + +def test_extract_list_block_list_item_form(): + assert _extract_list_block(None, {"_ul": ["a"]}) == ("ul", ["a"]) + assert _extract_list_block(None, {"_ol": ["a"]}) == ("ol", ["a"]) + + +def test_extract_list_block_allows_suffix(): + assert _extract_list_block("_ul_left", ["a"]) == ("ul", ["a"]) + assert _extract_list_block(None, {"_ol_steps": ["a"]}) == ("ol", ["a"]) + + +def test_extract_list_block_ignores_non_list_blocks(): + assert _extract_list_block("heading", ["a"]) is None + assert _extract_list_block("_ultra", ["a"]) is None # not _ul / _ul_* + assert _extract_list_block(None, {"_img": {}}) is None + assert _extract_list_block(None, {"a": 1, "b": 2}) is None + + +# --- bare list is now paragraphs, not bullets -------------------------------------- + +def test_bare_string_list_renders_as_paragraphs(): + story = build_story({"Heading": ["first line", "second line"]}, ctx_value()) + assert "ListFlowable" not in kinds(story) + assert kinds(story).count("Paragraph") >= 3 # heading + 2 paragraphs + + +def test_single_item_list_is_a_paragraph_not_a_bullet(): + # the original bug: one wrapped paragraph under a heading came out as a bullet + story = build_story({"Heading": ["just one paragraph"]}, ctx_value()) + assert "ListFlowable" not in kinds(story) + + +# --- explicit lists produce ListFlowables ------------------------------------------ + +def test_ul_block_produces_list_flowable(): + story = build_story({"Heading": {"_ul": ["a", "b", "c"]}}, ctx_value()) + assert "ListFlowable" in kinds(story) + + +def test_ol_block_produces_numbered_list_flowable(): + story = build_story({"_ol": ["a", "b"]}, ctx_value()) + lists = [f for f in story if isinstance(f, ListFlowable)] + assert len(lists) == 1 + + +def test_ul_as_list_item_form(): + story = build_story({"Section": ["intro paragraph", {"_ul": ["x", "y"]}]}, ctx_value()) + assert "ListFlowable" in kinds(story) + assert "Paragraph" in kinds(story) + + +# helper (module-level context without a fixture, for direct calls above) +_CTX = None +def ctx_value(): + global _CTX + if _CTX is None: + styles, tablestyles, doctemplate = load_report_config() + _CTX = build_context( + {}, styles, doctemplate, tablestyles, {}, + pathlib.Path.cwd(), pathlib.Path.cwd(), pathlib.Path.cwd(), + ) + return _CTX