From ce6f7e1f4dee0072280d3f6799d77d709e921724 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 16:47:58 +0200 Subject: [PATCH 1/8] fix(text-wrapper): prevent boundary word duplication in line wrapping --- .../pipeline/docraft_loom_text_wrapper.cc | 3 +- docraft/test/CMakeLists.txt | 1 + .../docraft_loom_text_wrapper_test.cc | 73 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 docraft/test/docraft/loom/pipeline/docraft_loom_text_wrapper_test.cc diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_text_wrapper.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_text_wrapper.cc index 927b0cf..4dab2bf 100644 --- a/docraft/src/docraft/loom/pipeline/docraft_loom_text_wrapper.cc +++ b/docraft/src/docraft/loom/pipeline/docraft_loom_text_wrapper.cc @@ -79,7 +79,8 @@ namespace docraft::loom::pipeline { } continue; } - const std::string candidate = current_line.append(" ").append(word); + std::string candidate = current_line; + candidate=candidate.append(" ").append(word); if (text_backend_->measure_text_width(candidate, font_name, font_size) <= max_width) { current_line = candidate; } else { diff --git a/docraft/test/CMakeLists.txt b/docraft/test/CMakeLists.txt index 6618c63..1390a25 100644 --- a/docraft/test/CMakeLists.txt +++ b/docraft/test/CMakeLists.txt @@ -15,6 +15,7 @@ set(TEST_SOURCES docraft/utils/docraft_file_utils_test.cc docraft/utils/docraft_test_temp_file.h docraft/loom/pipeline/docraft_loom_measure_processor_test.cc + docraft/loom/pipeline/docraft_loom_text_wrapper_test.cc docraft/loom/pipeline/docraft_loom_layout_processor_test.cc docraft/loom/pipeline/docraft_loom_pagination_processor_test.cc docraft/loom/nodes/docraft_loom_stack_nodes_test.cc diff --git a/docraft/test/docraft/loom/pipeline/docraft_loom_text_wrapper_test.cc b/docraft/test/docraft/loom/pipeline/docraft_loom_text_wrapper_test.cc new file mode 100644 index 0000000..ab86026 --- /dev/null +++ b/docraft/test/docraft/loom/pipeline/docraft_loom_text_wrapper_test.cc @@ -0,0 +1,73 @@ +#include "docraft/loom/pipeline/docraft_loom_text_wrapper.h" + +#include +#include + +#include +#include +#include + +#include "../../backend/docraft_mock_backend.h" + +namespace docraft::test { + class DocraftLoomTextWrapperTest : public ::testing::Test + { + protected: + void SetUp() override + { + text_backend_mock_ = std::make_shared<::testing::NiceMock>(); + // Deterministic stand-in for real glyph measurement: one width unit per byte. + ON_CALL(*text_backend_mock_, measure_text_width(::testing::_, ::testing::_, ::testing::_)) + .WillByDefault( + [](const std::string &text, const std::string &, float) { return static_cast(text.size()); }); + wrapper_ = std::make_unique(text_backend_mock_); + } + + std::unique_ptr wrapper_; + std::shared_ptr text_backend_mock_; + }; + + // Regression test: wrapping a multi-word line whose full text is wider than + // max_width must not reprint the boundary word -- each source word must appear + // in the wrapped output exactly once, and every produced line must respect + // max_width. Previously current_line.append(...).append(...) mutated + // current_line as a side effect while building `candidate`, so the rejected + // line got pushed already containing the overflowing word, which was then + // placed again on the next line. + TEST_F(DocraftLoomTextWrapperTest, MultiWordWrapDoesNotDuplicateBoundaryWord) + { + const auto lines = wrapper_->wrap("brown fox jumps", 9.0F, "Helvetica", 9.0F); + + EXPECT_THAT(lines, ::testing::ElementsAre("brown fox", "jumps")); + for (const auto &line: lines) { + EXPECT_LE(line.size(), 9U) << "line \"" << line << "\" exceeds max_width"; + } + } + + TEST_F(DocraftLoomTextWrapperTest, MultiWordWrapPreservesEachWordExactlyOnce) + { + const auto lines = wrapper_->wrap("The quick brown fox jumps", 11.0F, "Helvetica", 9.0F); + + std::vector words; + for (const auto &line: lines) { + std::istringstream iss(line); + std::string word; + while (iss >> word) { + words.push_back(word); + } + } + + EXPECT_THAT(words, ::testing::ElementsAre("The", "quick", "brown", "fox", "jumps")); + } + + TEST_F(DocraftLoomTextWrapperTest, SingleWordAloneStillCharacterSplitsCorrectly) + { + const auto lines = wrapper_->wrap("Balasubramanian", 6.0F, "Helvetica", 9.0F); + + std::string rejoined; + for (const auto &line: lines) { + rejoined += line; + } + EXPECT_EQ(rejoined, "Balasubramanian"); + } +} // namespace docraft::test From 5f0fcc1c8d49f9ff0d954a8e796f6de790a32f68 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:21:24 +0200 Subject: [PATCH 2/8] fix(loom-table): stop table columns overflowing available width resolve_table_column_widths gave every flexible column a share of the FULL available_width even when another column had an explicit width, instead of deducting that explicit width first. The resolved columns' total could then exceed available_width, silently pushing the table past the page/body margin. Flexible columns now divide only what's left after explicit-width columns are deducted, and only the flexible columns are rescaled to close any gap left by their natural-width floors. Co-Authored-By: Claude Sonnet 5 --- .../pipeline/docraft_loom_layout_processor.cc | 102 +++++++++++------- .../loom/nodes/docraft_loom_table_test.cc | 9 +- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc index 94d776d..b08be78 100644 --- a/docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc +++ b/docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc @@ -607,22 +607,16 @@ namespace docraft::loom::pipeline { std::vector DocraftLoomLayoutProcessor::resolve_table_column_widths( const nodes::DocraftLoomTable& table, const TableNaturalGeometry& geometry, float incoming_width) const { - // Resolves each column's final width: - // - available_width is incoming_width (an ancestor's constraint pushed down via - // inherited_width_, or page_size_.width at the root -- see visit(Table)) minus - // outer padding, or -- if incoming_width is 0 (e.g. a table built without a - // page width in a unit test) -- the sum of the natural widths, so the table - // just hugs its own content. Mirrors the matching fix in - // DocraftLoomMeasureProcessor::visit(Table). - // - column weights: missing or non-positive entries default to 1.0 (handled by - // distribute_weighted_amounts()), so an all-zero weight vector divides evenly. - // - a column with an explicit width uses it verbatim (a hard constraint); otherwise - // it gets its proportional share of available_width by weight, floored at its own - // natural width (a column is never squeezed narrower than its content). - // - if no column used an explicit width and the natural-width floor left the columns - // not summing exactly to available_width, rescale all of them proportionally so - // the table fills available_width exactly. Skipped if any column has an explicit - // width, since that width must not be stretched or shrunk to make the total add up. + // Goal: resolved column widths must sum to available_width, not more. + // + // Example: available_width = 200, column A has explicit width = 150. + // Column B (flexible, weight 1) must get 200 - 150 = 50. + // The old bug gave B a share of the FULL 200 (e.g. 200/2 = 100 for 2 columns), + // as if A wasn't taking any space, so A + B = 250 > 200 and the table overflowed. + // + // available_width comes from incoming_width (see visit(Table)) minus padding, + // or -- if there's no incoming_width, e.g. in a unit test -- the sum of the + // columns' natural widths, so the table just hugs its own content. const int cols = table.column_count(); float sum_natural = 0.0F; @@ -633,37 +627,65 @@ namespace docraft::loom::pipeline { (2.0F * table.padding()) : sum_natural; - const auto by_weight = - distribute_weighted_amounts(available_width, table.column_weights(), cols, geometry.natural_widths); + // A column is "fixed" if the author gave it an explicit width, "flexible" + // otherwise. Both helpers just read geometry/table -- no bookkeeping needed. + const auto& weights = table.column_weights(); + auto is_fixed = [&](int c) { return geometry.explicit_widths[static_cast(c)] > 0.0F; }; + auto column_weight = [&](int c) { + if (c >= 0 && c < static_cast(weights.size()) && weights[static_cast(c)] > 0.0F) + return weights[static_cast(c)]; + return 1.0F; // missing/non-positive weight defaults to 1.0, same as distribute_weighted_amounts() + }; + + std::vector resolved(static_cast(cols), 0.0F); - std::vector resolved(static_cast(cols), 0.0F); - bool any_explicit = false; + // 1) Fixed columns keep their own width verbatim. `remaining` is what's + // actually left over for the flexible ones -- not the full available_width. + float remaining = available_width; for (int c = 0; c < cols; ++c) { - const float explicit_w = geometry.explicit_widths[static_cast(c)]; - if (explicit_w > 0.0F) - { - resolved[static_cast(c)] = explicit_w; - any_explicit = true; - } - else + if (is_fixed(c)) { - resolved[static_cast(c)] = by_weight[static_cast(c)]; + resolved[static_cast(c)] = geometry.explicit_widths[static_cast(c)]; + remaining -= resolved[static_cast(c)]; } } - if (!any_explicit) + remaining = std::max(0.0F, remaining); + + // 2) Split `remaining` among the flexible columns by weight. total_weight + // only sums flexible columns' weights, so a fixed column's weight can't + // dilute anyone else's share -- this is the actual fix (see example above). + // Each share is floored at the column's own natural width: never squeeze a + // column narrower than its content. + float total_weight = 0.0F; + for (int c = 0; c < cols; ++c) + if (!is_fixed(c)) + total_weight += column_weight(c); + + float flexible_total = 0.0F; + for (int c = 0; c < cols; ++c) + { + if (is_fixed(c)) + continue; + const float share = total_weight > 0.0F ? remaining * column_weight(c) / total_weight : 0.0F; + resolved[static_cast(c)] = + std::max(geometry.natural_widths[static_cast(c)], share); + flexible_total += resolved[static_cast(c)]; + } + + // 3) The floor in step 2 can push a column above its weighted share, so the + // flexible columns might no longer add up to `remaining`. Scale just those + // columns (fixed ones stay untouched) so the total matches available_width + // whenever the floors allow it. If the floors alone already exceed + // `remaining`, this scales below 1 and shrinks columns under their natural + // width -- content genuinely doesn't fit, but the table still stays close to + // available_width instead of overflowing it freely. + if (flexible_total > 0.0F && remaining > 0.0F) { - float sum_resolved = 0.0F; - for (float w : resolved) - sum_resolved += w; - if (sum_resolved > 0.0F && available_width > 0.0F) - { - const float scale = available_width / sum_resolved; - for (float& w : resolved) - { - w *= scale; - } - } + const float scale = remaining / flexible_total; + for (int c = 0; c < cols; ++c) + if (!is_fixed(c)) + resolved[static_cast(c)] *= scale; } return resolved; } diff --git a/docraft/test/docraft/loom/nodes/docraft_loom_table_test.cc b/docraft/test/docraft/loom/nodes/docraft_loom_table_test.cc index 3756111..ea0c7c9 100644 --- a/docraft/test/docraft/loom/nodes/docraft_loom_table_test.cc +++ b/docraft/test/docraft/loom/nodes/docraft_loom_table_test.cc @@ -72,9 +72,12 @@ namespace docraft::test { table->accept(layout); EXPECT_FLOAT_EQ(table->cell(0, 0)->layout_box().frame.size.width, 40.0F); - // no rescale when any explicit width is present: column 1 gets its weight-based - // share of available_width (195 / 2 columns = 97.5), floored at its natural width - EXPECT_FLOAT_EQ(table->cell(0, 1)->layout_box().frame.size.width, 97.5F); + // column 1 (the only flexible column) gets everything available_width has left + // over after column 0's explicit width is deducted (195 - 40 = 155), not a share + // of the full available_width diluted by column 0 -- otherwise the table's total + // resolved width would exceed available_width (40 + 97.5 = 137.5 happened to fit + // here, but the same dilution overflows the margin with more columns/less slack). + EXPECT_FLOAT_EQ(table->cell(0, 1)->layout_box().frame.size.width, 155.0F); } TEST_F(DocraftLoomTableTest, NaturalWidthFloorIsRespectedWhenNoRescaleIsNeeded) From e4f18c7e12ecf89879c988d312e1feb86237be3a Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:21:30 +0200 Subject: [PATCH 3/8] fix(loom-rectangle): clip children to rectangle bounds when rendering visit(DocraftLoomRectangle*) painted children with no clip at all, unlike the equivalent visit(DocraftLoomCanvas*). A child whose computed size exceeded the rectangle's own frame (e.g. a Text node whose own explicit wrap_width overrides the width relayed by its parent rectangle) painted past the rectangle's edges instead of being contained by it. Co-Authored-By: Claude Sonnet 5 --- .../docraft_loom_rendering_processor.cc | 10 +++++- .../loom/nodes/docraft_loom_rectangle_test.cc | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc index c9c7af5..f2d1c40 100644 --- a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc +++ b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc @@ -249,10 +249,18 @@ namespace docraft::loom::pipeline { { if (!node || !should_render(*node)) return; - draw_container_background(node->style(), node->layout_box().frame.position, node->layout_box().frame.size); + const auto& frame = node->layout_box().frame; + draw_container_background(node->style(), frame.position, frame.size); + // Clips children to the rectangle's own bounds, mirroring visit(DocraftLoomCanvas*) + // below -- without this, a child whose computed size exceeds the rectangle's frame + // (e.g. a Text node whose own explicit wrap_width overrides the width relayed by + // this rectangle) paints past the rectangle's edges instead of being contained by it. + shape_backend_->save_state(); + shape_backend_->clip_rectangle(frame.position.x, frame.position.y, frame.size.width, frame.size.height); for (int i: node->paint_order_indices()) if (auto child = node->edit_child(i)) child->accept(*this); + shape_backend_->restore_state(); } void DocraftLoomRenderingProcessor::visit(docraft::loom::nodes::DocraftLoomCanvas* node) diff --git a/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc b/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc index 78e7556..d6539bb 100644 --- a/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc +++ b/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc @@ -6,6 +6,8 @@ #include "docraft/loom/nodes/docraft_loom_text.h" #include "docraft/loom/pipeline/docraft_loom_layout_processor.h" #include "docraft/loom/pipeline/docraft_loom_measure_processor.h" +#include "docraft/loom/pipeline/docraft_loom_rendering_processor.h" +#include "docraft/utils/docraft_mock_rendering_backend.h" #include "../../backend/docraft_mock_backend.h" namespace docraft::test { @@ -113,4 +115,37 @@ namespace docraft::test { EXPECT_FLOAT_EQ(rect.layout_box().frame.position.x, 80.0F); EXPECT_FLOAT_EQ(rect.layout_box().frame.position.y, 90.0F); } + + TEST_F(DocraftLoomRectangleTest, RenderingClipsChildrenToRectangleBoundsBracketedBySaveRestore) + { + // Regression test: visit(DocraftLoomRectangle*) used to paint children with no + // clip at all (unlike the equivalent visit(DocraftLoomCanvas*)), so a child whose + // computed size exceeded the rectangle's own frame (e.g. a Text node whose own + // explicit wrap_width overrides the width relayed by this rectangle) painted past + // the rectangle's edges instead of being contained by it. + utils::MockRenderingBackend backend; + loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend); + + auto rect = std::make_shared(); + rect->set_position_mode(loom::nodes::DocraftPositionType::kAbsolute); + rect->set_explicit_position({.x = 10.0F, .y = 10.0F}); + rect->set_width(80.0F); + rect->set_height(60.0F); + auto child = std::make_shared("overflowing text"); + rect->add_child(child); + + rect->accept(*measure_); + rect->accept(*layout_); + rect->accept(rendering); + + // Assert against the rectangle's own resolved frame (rather than the + // width_/height_ set above) so this test only exercises clipping and stays + // agnostic to how that frame is sized. + const auto& frame = rect->layout_box().frame; + ASSERT_EQ(backend.clip_calls().size(), 1U); + EXPECT_FLOAT_EQ(backend.clip_calls()[0].x, frame.position.x); + EXPECT_FLOAT_EQ(backend.clip_calls()[0].y, frame.position.y); + EXPECT_FLOAT_EQ(backend.clip_calls()[0].width, frame.size.width); + EXPECT_FLOAT_EQ(backend.clip_calls()[0].height, frame.size.height); + } } // namespace docraft::test \ No newline at end of file From 22cfed44d540583f2fb57215727686d161375f13 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:29:40 +0200 Subject: [PATCH 4/8] refactor(loom-rendering): extract paint_children_clipped_to_frame visit(Rectangle*) and visit(Canvas*) each hand-rolled the same save_state()/clip_rectangle()/paint children/restore_state() sequence (flagged as duplication by SonarQube). Extracted into a shared helper, mirroring how draw_container_background() already factors out the background paint sequence shared by container node visitors. Co-Authored-By: Claude Sonnet 5 --- .../docraft_loom_rendering_processor.h | 9 +++++ .../docraft_loom_rendering_processor.cc | 39 ++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/docraft/include/docraft/loom/pipeline/docraft_loom_rendering_processor.h b/docraft/include/docraft/loom/pipeline/docraft_loom_rendering_processor.h index 20ac783..133a407 100644 --- a/docraft/include/docraft/loom/pipeline/docraft_loom_rendering_processor.h +++ b/docraft/include/docraft/loom/pipeline/docraft_loom_rendering_processor.h @@ -150,6 +150,15 @@ namespace docraft::loom::pipeline { void draw_container_background(const nodes::DocraftLoomShapeStyle& style, const nodes::Position& position, const nodes::Size& size); + /** + * @brief Paints node's children (in paint_order_indices() order) clipped to + * frame, so nothing a child paints can escape those bounds. Shared by every + * container that must contain its children (Rectangle, Canvas) so the + * save_state()/clip_rectangle()/.../restore_state() sequence isn't duplicated + * at each call site. + */ + void paint_children_clipped_to_frame(nodes::DocraftLoomNode& node, const nodes::Rect& frame); + /** * @brief Whether a node should be drawn during the current page's render pass: * true for unpaginated content (layout_box().page_index < 0, e.g. a header/footer diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc index f2d1c40..a9fdc11 100644 --- a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc +++ b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc @@ -245,22 +245,28 @@ namespace docraft::loom::pipeline { shape_backend_->restore_state(); } + void DocraftLoomRenderingProcessor::paint_children_clipped_to_frame(nodes::DocraftLoomNode& node, + const nodes::Rect& frame) + { + shape_backend_->save_state(); + shape_backend_->clip_rectangle(frame.position.x, frame.position.y, frame.size.width, frame.size.height); + for (int i: node.paint_order_indices()) + if (auto child = node.edit_child(i)) + child->accept(*this); + shape_backend_->restore_state(); + } + void DocraftLoomRenderingProcessor::visit(docraft::loom::nodes::DocraftLoomRectangle* node) { if (!node || !should_render(*node)) return; const auto& frame = node->layout_box().frame; draw_container_background(node->style(), frame.position, frame.size); - // Clips children to the rectangle's own bounds, mirroring visit(DocraftLoomCanvas*) - // below -- without this, a child whose computed size exceeds the rectangle's frame - // (e.g. a Text node whose own explicit wrap_width overrides the width relayed by - // this rectangle) paints past the rectangle's edges instead of being contained by it. - shape_backend_->save_state(); - shape_backend_->clip_rectangle(frame.position.x, frame.position.y, frame.size.width, frame.size.height); - for (int i: node->paint_order_indices()) - if (auto child = node->edit_child(i)) - child->accept(*this); - shape_backend_->restore_state(); + // Without this clip, a child whose computed size exceeds the rectangle's own + // frame (e.g. a Text node whose own explicit wrap_width overrides the width + // relayed by this rectangle) would paint past the rectangle's edges instead of + // being contained by it. + paint_children_clipped_to_frame(*node, frame); } void DocraftLoomRenderingProcessor::visit(docraft::loom::nodes::DocraftLoomCanvas* node) @@ -269,15 +275,10 @@ namespace docraft::loom::pipeline { return; const auto& frame = node->layout_box().frame; draw_container_background(node->style(), frame.position, frame.size); - // Clips children to the canvas bounds, trimming anything that overflows -- see - // visit(DocraftLoomCanvas*) in the layout processor for how children are - // positioned relative to this origin in the first place. - shape_backend_->save_state(); - shape_backend_->clip_rectangle(frame.position.x, frame.position.y, frame.size.width, frame.size.height); - for (int i: node->paint_order_indices()) - if (auto child = node->edit_child(i)) - child->accept(*this); - shape_backend_->restore_state(); + // Trims anything that overflows the canvas bounds -- see visit(DocraftLoomCanvas*) + // in the layout processor for how children are positioned relative to this + // origin in the first place. + paint_children_clipped_to_frame(*node, frame); } void DocraftLoomRenderingProcessor::visit(docraft::loom::nodes::DocraftLoomParagraph* paragraph) From b730c6ab59db3ce28d1b3b3502632b4f7eb47be2 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:39:23 +0200 Subject: [PATCH 5/8] feat(docs): modularize documentation workflows with separate YAML files --- .github/workflows/docs-build.yml | 43 +++++++++++++++++++++ .github/workflows/docs-deploy.yml | 39 +++++++++++++++++++ .github/workflows/docs.yml | 62 +------------------------------ 3 files changed, 84 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/docs-build.yml create mode 100644 .github/workflows/docs-deploy.yml diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml new file mode 100644 index 0000000..1fb34f2 --- /dev/null +++ b/.github/workflows/docs-build.yml @@ -0,0 +1,43 @@ +name: Build Documentation + +on: + workflow_call: + inputs: + upload-artifact: + description: 'Upload the built docs as a GitHub Pages artifact' + type: boolean + default: false + +jobs: + build-docs: + runs-on: ubuntu-latest + name: Build documentation (Sphinx + Doxygen) + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y doxygen graphviz + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r doc/requirements.txt + + - name: Build documentation + run: | + cd doc + sphinx-build -b html source build/html -W --keep-going + + - name: Upload GitHub Pages artifact + if: inputs.upload-artifact + uses: actions/upload-pages-artifact@v3 + with: + path: doc/build/html/ diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000..5b0cfdc --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,39 @@ +name: Documentation Deploy + +on: + push: + branches: [main] + paths: + - 'doc/**' + - 'docraft/include/**' + - '.github/workflows/docs-deploy.yml' + - '.github/workflows/docs-build.yml' + workflow_dispatch: + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-docs: + uses: ./.github/workflows/docs-build.yml + with: + upload-artifact: true + + deploy-docs: + needs: build-docs + runs-on: ubuntu-latest + name: Deploy documentation to GitHub Pages + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 54c2cc6..f6b06a3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,73 +1,15 @@ name: Documentation on: - push: - branches: [main] - paths: - - 'doc/**' - - 'docraft/include/**' - - '.github/workflows/docs.yml' pull_request: branches: [main, dev] paths: - 'doc/**' - 'docraft/include/**' - '.github/workflows/docs.yml' + - '.github/workflows/docs-build.yml' workflow_dispatch: -concurrency: - group: pages - cancel-in-progress: false - jobs: build-docs: - runs-on: ubuntu-latest - name: Build documentation (Sphinx + Doxygen) - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y doxygen graphviz - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install -r doc/requirements.txt - - - name: Build documentation - run: | - cd doc - sphinx-build -b html source build/html -W --keep-going - - - name: Upload GitHub Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: doc/build/html/ - - deploy-docs: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: build-docs - runs-on: ubuntu-latest - name: Deploy documentation to GitHub Pages - - permissions: - pages: write - id-token: write - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - + uses: ./.github/workflows/docs-build.yml From bb144ab437ea1b3542782d9a4f5d6bec949728b9 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:43:02 +0200 Subject: [PATCH 6/8] ci(docs): update permissions for documentation workflows --- .github/workflows/docs-build.yml | 3 +++ .github/workflows/docs-deploy.yml | 3 +++ .github/workflows/docs.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 1fb34f2..3509c70 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -13,6 +13,9 @@ jobs: runs-on: ubuntu-latest name: Build documentation (Sphinx + Doxygen) + permissions: + contents: read + pages: write steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 5b0cfdc..e79f779 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -14,6 +14,9 @@ concurrency: group: pages cancel-in-progress: false +permissions: + contents: read + packages: read jobs: build-docs: uses: ./.github/workflows/docs-build.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f6b06a3..682c978 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,6 +10,9 @@ on: - '.github/workflows/docs-build.yml' workflow_dispatch: +permissions: + contents: read + jobs: build-docs: uses: ./.github/workflows/docs-build.yml From 9e451c293dd5966f9673590cfe4c3cc565ee72c5 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:45:29 +0200 Subject: [PATCH 7/8] ci(docs): update Python dependencies and permissions in documentation workflow --- .github/workflows/docs-build.yml | 6 +++--- doc/requirements.txt | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 3509c70..1858b7b 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -15,7 +15,7 @@ jobs: permissions: contents: read - pages: write + steps: - uses: actions/checkout@v4 @@ -31,8 +31,8 @@ jobs: - name: Install Python dependencies run: | - python -m pip install --upgrade pip - pip install -r doc/requirements.txt + python -m pip install --upgrade pip==26.2.1 + pip install --only-binary :all: -r doc/requirements.txt - name: Build documentation run: | diff --git a/doc/requirements.txt b/doc/requirements.txt index 499dc5a..aeb452b 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ -sphinx -breathe -sphinx_rtd_theme -sphinx-design -sphinx-sitemap -sphinxext-opengraph +sphinx==9.1.0 +breathe==4.36.0 +sphinx_rtd_theme==3.1.0 +sphinx-design==0.7.0 +sphinx-sitemap==2.9.0 +sphinxext-opengraph==0.13.0 From da77a0a297f91f7336a97797ae99790703048e11 Mon Sep 17 00:00:00 2001 From: cadons Date: Sun, 16 Aug 2026 17:49:04 +0200 Subject: [PATCH 8/8] fix(requirements): downgrade sphinx to version 9.0.4 --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index aeb452b..54f444a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx==9.1.0 +sphinx==9.0.4 breathe==4.36.0 sphinx_rtd_theme==3.1.0 sphinx-design==0.7.0