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
46 changes: 46 additions & 0 deletions .github/workflows/docs-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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)

permissions:
contents: read

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==26.2.1
pip install --only-binary :all: -r doc/requirements.txt

Check warning on line 35 in .github/workflows/docs-build.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Using dependencies without locking resolved versions is security-sensitive.

See more on https://sonarcloud.io/project/issues?id=Cadons_Docraft&issues=AaALPQNJ63KyUSPFyVjm&open=AaALPQNJ63KyUSPFyVjm&pullRequest=81

- 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/
42 changes: 42 additions & 0 deletions .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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

permissions:
contents: read
packages: read
jobs:
build-docs:
uses: ./.github/workflows/docs-build.yml
with:
upload-artifact: true

deploy-docs:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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
63 changes: 4 additions & 59 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,73 +1,18 @@
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
permissions:
contents: read

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
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
12 changes: 6 additions & 6 deletions doc/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
sphinx
breathe
sphinx_rtd_theme
sphinx-design
sphinx-sitemap
sphinxext-opengraph
sphinx==9.0.4
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 62 additions & 40 deletions docraft/src/docraft/loom/pipeline/docraft_loom_layout_processor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -607,22 +607,16 @@ namespace docraft::loom::pipeline {
std::vector<float> 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;
Expand All @@ -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<std::size_t>(c)] > 0.0F; };
auto column_weight = [&](int c) {
if (c >= 0 && c < static_cast<int>(weights.size()) && weights[static_cast<std::size_t>(c)] > 0.0F)
return weights[static_cast<std::size_t>(c)];
return 1.0F; // missing/non-positive weight defaults to 1.0, same as distribute_weighted_amounts()
};

std::vector resolved(static_cast<std::size_t>(cols), 0.0F);

std::vector<float> resolved(static_cast<std::size_t>(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<std::size_t>(c)];
if (explicit_w > 0.0F)
{
resolved[static_cast<std::size_t>(c)] = explicit_w;
any_explicit = true;
}
else
if (is_fixed(c))
{
resolved[static_cast<std::size_t>(c)] = by_weight[static_cast<std::size_t>(c)];
resolved[static_cast<std::size_t>(c)] = geometry.explicit_widths[static_cast<std::size_t>(c)];
remaining -= resolved[static_cast<std::size_t>(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<std::size_t>(c)] =
std::max(geometry.natural_widths[static_cast<std::size_t>(c)], share);
flexible_total += resolved[static_cast<std::size_t>(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<std::size_t>(c)] *= scale;
}
return resolved;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +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;
draw_container_background(node->style(), node->layout_box().frame.position, node->layout_box().frame.size);
for (int i: node->paint_order_indices())
if (auto child = node->edit_child(i))
child->accept(*this);
const auto& frame = node->layout_box().frame;
draw_container_background(node->style(), frame.position, frame.size);
// 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)
Expand All @@ -261,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions docraft/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading