Skip to content

Commit b3f4f2e

Browse files
committed
Implement step 7.3.1: Parse text box content
1 parent 2fe05b3 commit b3f4f2e

4 files changed

Lines changed: 116 additions & 22 deletions

File tree

PanoramicData.Render.Test/DrawingShapeRunElementTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,36 @@ public void Parse_AnchorDrawingWithDiamondShape_ReturnsDrawingShapeRunElement()
136136
shape.PresetKind.ToString().Should().Be("Diamond");
137137
}
138138

139+
[Fact]
140+
public void Parse_InlineDrawingWithDrawingMlTextFrame_ExtractsText()
141+
{
142+
var drawing = CreateInlineShapeWithTextBody("rect", 914400L, 457200L, ["First line", "Second line"]);
143+
var run = new Run(drawing);
144+
145+
var elements = RunElementParser.Parse(run);
146+
147+
var shape = elements.Should().ContainSingle()
148+
.Which.Should().BeOfType<DrawingShapeRunElement>()
149+
.Subject;
150+
shape.TextFrame.HasTextFrame.Should().BeTrue();
151+
shape.TextFrame.Text.Should().Be("First line\nSecond line");
152+
}
153+
154+
[Fact]
155+
public void Parse_InlineDrawingWithWordTextBoxContent_ExtractsText()
156+
{
157+
var drawing = CreateInlineShapeWithTextBoxContent("rect", 914400L, 457200L, ["Box line one", "Box line two"]);
158+
var run = new Run(drawing);
159+
160+
var elements = RunElementParser.Parse(run);
161+
162+
var shape = elements.Should().ContainSingle()
163+
.Which.Should().BeOfType<DrawingShapeRunElement>()
164+
.Subject;
165+
shape.TextFrame.HasTextFrame.Should().BeTrue();
166+
shape.TextFrame.Text.Should().Be("Box line one\nBox line two");
167+
}
168+
139169
// -------------------------------------------------------------------------
140170
// Helpers
141171
// -------------------------------------------------------------------------
@@ -204,4 +234,59 @@ private static Drawing CreateAnchorShape(string presetName, long widthEmu, long
204234
};
205235
return new Drawing(anchor);
206236
}
237+
238+
private static Drawing CreateInlineShapeWithTextBody(string presetName, long widthEmu, long heightEmu, IReadOnlyList<string> lines)
239+
{
240+
var presetGeom = new A.PresetGeometry();
241+
presetGeom.SetAttribute(new OpenXmlAttribute("prst", string.Empty, presetName));
242+
var textBody = new OpenXmlUnknownElement("txBody")
243+
{
244+
InnerXml = "<bodyPr/>" + string.Concat(lines.Select(line => $"<p><r><t>{System.Security.SecurityElement.Escape(line)}</t></r></p>"))
245+
};
246+
var spPr = new A.ShapeProperties(presetGeom);
247+
var graphicData = new A.GraphicData(spPr, textBody)
248+
{
249+
Uri = "http://schemas.openxmlformats.org/drawingml/2006/main"
250+
};
251+
var graphic = new A.Graphic(graphicData);
252+
var inline = new DW.Inline(
253+
new DW.Extent { Cx = widthEmu, Cy = heightEmu },
254+
graphic)
255+
{
256+
DistanceFromTop = 0,
257+
DistanceFromBottom = 0,
258+
DistanceFromLeft = 0,
259+
DistanceFromRight = 0
260+
};
261+
return new Drawing(inline);
262+
}
263+
264+
private static Drawing CreateInlineShapeWithTextBoxContent(string presetName, long widthEmu, long heightEmu, IReadOnlyList<string> lines)
265+
{
266+
var presetGeom = new A.PresetGeometry();
267+
presetGeom.SetAttribute(new OpenXmlAttribute("prst", string.Empty, presetName));
268+
var textBox = new OpenXmlUnknownElement("wps:txbx")
269+
{
270+
InnerXml = "<w:txbxContent xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">"
271+
+ string.Concat(lines.Select(line => $"<w:p><w:r><w:t>{System.Security.SecurityElement.Escape(line)}</w:t></w:r></w:p>"))
272+
+ "</w:txbxContent>"
273+
};
274+
var spPr = new A.ShapeProperties(presetGeom);
275+
var graphicData = new A.GraphicData(spPr, textBox)
276+
{
277+
Uri = "http://schemas.openxmlformats.org/drawingml/2006/main"
278+
};
279+
var graphic = new A.Graphic(graphicData);
280+
var inline = new DW.Inline(
281+
new DW.Extent { Cx = widthEmu, Cy = heightEmu },
282+
graphic)
283+
{
284+
DistanceFromTop = 0,
285+
DistanceFromBottom = 0,
286+
DistanceFromLeft = 0,
287+
DistanceFromRight = 0
288+
};
289+
return new Drawing(inline);
290+
}
291+
207292
}

PanoramicData.Render/ShapeTextFrameParser.cs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ public static ShapeTextFrameInfo Parse(OpenXmlElement drawingRoot)
1616
{
1717
ArgumentNullException.ThrowIfNull(drawingRoot);
1818

19+
var textBoxContent = drawingRoot.Descendants().FirstOrDefault(e => e.LocalName == "txbxContent");
20+
if (textBoxContent is not null)
21+
{
22+
var paragraphLines = textBoxContent.ChildElements
23+
.Where(e => e.LocalName == "p")
24+
.Select(ExtractText)
25+
.Where(text => text.Length > 0)
26+
.ToList();
27+
28+
return new ShapeTextFrameInfo
29+
{
30+
HasTextFrame = true,
31+
Text = string.Join("\n", paragraphLines)
32+
};
33+
}
34+
1935
var txBody = drawingRoot.Descendants().FirstOrDefault(e => e.LocalName == "txBody");
2036
if (txBody is null)
2137
{
@@ -24,15 +40,7 @@ public static ShapeTextFrameInfo Parse(OpenXmlElement drawingRoot)
2440

2541
var bodyPr = txBody.ChildElements.FirstOrDefault(e => e.LocalName == "bodyPr");
2642
var paragraphs = txBody.ChildElements.Where(e => e.LocalName == "p").ToList();
27-
var lines = new List<string>();
28-
for (var i = 0; i < paragraphs.Count; i++)
29-
{
30-
var text = string.Concat(paragraphs[i].Descendants().Where(d => d.LocalName == "t").Select(t => t.InnerText));
31-
if (text.Length > 0)
32-
{
33-
lines.Add(text);
34-
}
35-
}
43+
var lines = paragraphs.Select(ExtractText).Where(text => text.Length > 0).ToList();
3644

3745
var autoFitMode = ShapeTextAutoFitMode.None;
3846
if (bodyPr is not null)
@@ -63,6 +71,11 @@ public static ShapeTextFrameInfo Parse(OpenXmlElement drawingRoot)
6371
};
6472
}
6573

74+
private static string ExtractText(OpenXmlElement paragraphLikeElement)
75+
{
76+
return string.Concat(paragraphLikeElement.Descendants().Where(d => d.LocalName == "t").Select(t => t.InnerText));
77+
}
78+
6679
private static long ParseLongAttribute(OpenXmlElement? element, string localName)
6780
{
6881
if (element is null)

docs/plans/current-status.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,22 @@ Phase 7: Advanced Features — **IN PROGRESS**
1010

1111
## Current Step
1212

13-
Steps 7.2.1–7.2.8multi-level lists**COMPLETE**
13+
Step 7.3.1text box parsing**COMPLETE**
1414

15-
Completed multi-level list support milestone:
16-
- Added restart metadata (`RestartAfterLevel`) to resolved numbering level styles
17-
- Extended `NumberingStyleResolver` to map `w:lvlRestart`
18-
- Implemented `ListNumberingFormatter` for decimal/alpha/roman/bullet label text and `%n` pattern expansion
19-
- Implemented `ListNumberingState` for per-instance counters and restart-aware sequence tracking
20-
- Integrated list label emission into `RenderCommandEmitter` with hanging-indent style positioning
21-
- Added support for bullet label fonts via resolved/configured numbering style font family
22-
- Added unit tests for numbering sequences, restart behavior, label positioning, and bullet font rendering
15+
Completed initial text box parsing support:
16+
- Extended `ShapeTextFrameParser` to parse Wordprocessing text boxes via `w:txbxContent`
17+
- Preserved existing DrawingML `txBody` parsing while preferring explicit text box content when present
18+
- Added parser coverage for inline shapes containing DrawingML text bodies and Wordprocessing text box content
2319

24-
1748 tests passing (17431748, +5 list integration tests).
20+
1750 tests passing (17481750, +2 text box parsing tests).
2521

2622
## Next Step
2723

28-
Steps 7.3.1–7.3.7 — text boxes parsing, layout, and positioning
24+
Steps 7.3.2–7.3.7 — text box layout, margins, auto-size, and positioning
2925

3026
## Last Commit
3127

32-
Implement steps 7.2.5-7.2.8: List render integration and positioning (commit 9a4d948)
28+
Implement step 7.3.1: Parse text box content (commit pending)
3329

3430
## Implementation Notes
3531

docs/plans/phase-7-advanced-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Implement the remaining document features needed for high-fidelity rendering of
3535

3636
### 7.3 Text Boxes
3737

38-
- [ ] 7.3.1 — Parse text box elements (`w:txbxContent` inside `wsp:txbx` or VML `v:textbox`)
38+
- [x] 7.3.1 — Parse text box elements (`w:txbxContent` inside `wsp:txbx` or VML `v:textbox`)
3939
- [ ] 7.3.2 — Lay out text box content using the text layout engine (text boxes can contain paragraphs, tables, images)
4040
- [ ] 7.3.3 — Position text box as a floating object with anchor and wrapping
4141
- [ ] 7.3.4 — Handle text box internal margins

0 commit comments

Comments
 (0)