package export_test import ( "math" "sort" "testing" "github.com/carlosframework/docs/internal/export" ) // The whole point of PageOne is that it is the SAME layout the PDF // runs, not a second one that agrees with it by inspection. These // tests hold it to that: the geometry it reports has to be geometry // the exporter would really have drawn. func TestPageOneReportsALineForEachLineOfText(t *testing.T) { js := `{"type":"doc","content":[ {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"A heading"}]}, {"type":"paragraph","content":[{"type":"text","text":"One line of body text."}]} ]}` // Three lines: the title (set as an h1 by PDF and PageOne alike), // the heading, and the paragraph. lines := export.PageOne(mustParse(t, "Notes", js)) if len(lines) != 3 { t.Fatalf("got %d lines, want 3: %+v", len(lines), lines) } for i, l := range lines { if l.Height <= 0 || l.Width <= 0 { t.Errorf("line %d has no extent: %+v", i, l) } if l.Top < export.MarginPt { t.Errorf("line %d starts above the top margin: %+v", i, l) } if l.Left < export.MarginPt { t.Errorf("line %d starts left of the margin: %+v", i, l) } } } // A document with no content at all still has its title, and nothing // else — an empty document must not report a page full of phantom // lines. func TestPageOneOfAnEmptyDocumentIsJustItsTitle(t *testing.T) { lines := export.PageOne(mustParse(t, "Untitled document", `{"type":"doc","content":[]}`)) if len(lines) != 1 { t.Fatalf("got %d lines, want 1 (the title): %+v", len(lines), lines) } } // An untitled, contentless document reports nothing, which is what // lets the list render its empty box rather than a one-bar preview. func TestPageOneOfATrulyEmptyDocumentIsEmpty(t *testing.T) { lines := export.PageOne(mustParse(t, "", `{"type":"doc","content":[]}`)) if len(lines) != 0 { t.Fatalf("got %d lines, want none: %+v", len(lines), lines) } } // The bound that makes this affordable on a list page: a document of // any length costs one page of layout, not all of it. func TestPageOneStopsAtTheEndOfTheFirstPage(t *testing.T) { js := `{"type":"doc","content":[` for i := 0; i < 400; i++ { if i > 0 { js += "," } js += `{"type":"paragraph","content":[{"type":"text","text":"A paragraph."}]}` } js += `]}` lines := export.PageOne(mustParse(t, "Long", js)) if len(lines) == 0 { t.Fatal("no lines at all") } // 648pt of content box at 16.8pt a line is 38-ish; the exact // number is the layout's business, but 400 paragraphs must not // all be here and none may sit below the bottom margin. if len(lines) > 60 { t.Errorf("got %d lines — that is more than one page's worth", len(lines)) } for i, l := range lines { if l.Top+l.Height > export.PageHeightPt-export.MarginPt+0.01 { t.Errorf("line %d runs past the bottom margin: %+v", i, l) } } } // A heading's line reports a bigger size than a body line's. This is // what lets a thumbnail draw a heading heavier without the drawing // code having to know what a heading is. func TestPageOneReportsTheSizeOfEachLine(t *testing.T) { js := `{"type":"doc","content":[ {"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"Big"}]}, {"type":"paragraph","content":[{"type":"text","text":"Small"}]} ]}` lines := export.PageOne(mustParse(t, "", js)) if len(lines) != 2 { t.Fatalf("got %d lines, want 2: %+v", len(lines), lines) } if lines[0].Size <= lines[1].Size { t.Errorf("heading size %v is not bigger than body size %v", lines[0].Size, lines[1].Size) } if lines[1].Size != export.BodySizePt { t.Errorf("body line size %v, want %v", lines[1].Size, float64(export.BodySizePt)) } } // A centred line is reported where it was actually placed, not at the // margin — the thumbnail shows alignment because the layout already // knows it. func TestPageOneReportsAlignment(t *testing.T) { js := `{"type":"doc","content":[ {"type":"paragraph","content":[{"type":"text","text":"Left."}]}, {"type":"paragraph","attrs":{"align":"center"},"content":[{"type":"text","text":"Middle."}]} ]}` lines := export.PageOne(mustParse(t, "", js)) if len(lines) != 2 { t.Fatalf("got %d lines, want 2: %+v", len(lines), lines) } if lines[0].Left != export.MarginPt { t.Errorf("left-aligned line at %v, want the margin %v", lines[0].Left, float64(export.MarginPt)) } if lines[1].Left <= lines[0].Left { t.Errorf("centred line at %v is not indented past the left-aligned one at %v", lines[1].Left, lines[0].Left) } } // A test named TestPageOneDoesNotDisturbPDFExport stood here. It // exported a document, ran PageOne, exported it again and compared the // bytes — and it could not fail. PDF and PageOne build separate layout // values sharing no state, so only a package-level global could ever // have reddened it; a deliberate `l.cursor += 100` inside drawLine's // sink block left it green while reddening the agreement test below. // // Deleted rather than repaired, because a check that cannot // distinguish "no" from "I can't see it" is worse than no check: two // comments and a commit message cited it as the thing holding the sink // additive, which it never was. The exporter's own suite covers its // output, and TestPageOneMatchesTheFirstPageOfThePDF covers the // direction that can actually drift. // The test PageOne's own comment promises, and the one that makes "the // same layout, not a second opinion" a mechanism rather than a claim: // every line PageOne reports has to be a line the PDF exporter really // drew, at the same place. // // It reads the PDF back rather than trusting the layout twice. A run's // text matrix gives the baseline it was drawn at and the x it started // from; PageOne reports the top of the line box and its left edge. The // baseline must fall inside that box, and the left edges must be equal. // // Deliberately no list in the fixture: a list marker is drawn at an x // to the LEFT of its item's text (in the list's own indent), so the // leftmost run on that line is the marker rather than the line itself, // and this test's "leftmost run starts where the line starts" would be // comparing the wrong two numbers. That is a real gap, named rather // than papered over — a list's own geometry is not held by anything // here, and the pretext that another test covered it was false. func TestPageOneMatchesTheFirstPageOfThePDF(t *testing.T) { js := `{"type":"doc","content":[ {"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"A heading that is long enough to be interesting"}]}, {"type":"paragraph","content":[{"type":"text","text":"A paragraph long enough that it has to wrap onto a second line, which is the case where an estimator and a real layout would disagree about where the break falls."}]}, {"type":"paragraph","attrs":{"align":"center"},"content":[{"type":"text","text":"Centred."}]}, {"type":"paragraph","attrs":{"align":"right"},"content":[{"type":"text","text":"Right."}]} ]}` doc := mustParse(t, "Notes", js) lines := export.PageOne(doc) data, _ := mustPDF(t, doc) f := readPDF(t, data) pages := f.pages(t) if len(pages) != 1 { t.Fatalf("fixture spilled onto %d pages; it is meant to fit on one", len(pages)) } // Group the drawn runs into lines by baseline, keeping the leftmost // x on each. type drawn struct{ baseline, left float64 } var byBaseline []drawn for _, r := range f.runs(t, pages[0]) { baseline := float64(export.PageHeightPt) - r.y found := false for i := range byBaseline { if math.Abs(byBaseline[i].baseline-baseline) < 0.01 { if r.x < byBaseline[i].left { byBaseline[i].left = r.x } found = true break } } if !found { byBaseline = append(byBaseline, drawn{baseline: baseline, left: r.x}) } } sort.Slice(byBaseline, func(i, j int) bool { return byBaseline[i].baseline < byBaseline[j].baseline }) if len(byBaseline) != len(lines) { t.Fatalf("PageOne reports %d lines, the PDF drew %d", len(lines), len(byBaseline)) } for i, d := range byBaseline { l := lines[i] if d.baseline < l.Top || d.baseline > l.Top+l.Height { t.Errorf("line %d: baseline %.2f is outside the reported box [%.2f, %.2f]", i, d.baseline, l.Top, l.Top+l.Height) } if math.Abs(d.left-l.Left) > 0.01 { t.Errorf("line %d: the PDF drew it at x=%.2f, PageOne reports Left=%.2f", i, d.left, l.Left) } } }