Repository navigation
Quoted font-family name is truncated and keeps its opening quote when the value has leading whitespace #239
Description
Quoted font-family name is truncated and keeps its opening quote when the value has leading whitespace
Describe the bug
When a CSS font-family declaration uses a quoted font name and there is any whitespace between the : and the opening quote, the resulting <w:rFonts> in the generated DOCX keeps the opening quote and the font name is truncated at the end.
This is the shape emitted by virtually every rich-text editor (ProseMirror/TipTap, TinyMCE, CKEditor) and by Word's "Save as HTML":
<p><span style="font-family: "Times New Roman", serif;">text</span></p>Produced by the converter:
<w:rFonts w:ascii=""Times New Rom" w:hAnsi=""Times New Rom" />The font name is invalid, so Word cannot resolve it and falls back to the document default.
The amount of truncation depends on how many characters precede the opening quote: with k characters before the quote, the captured name loses its last k + 1 characters and gains the quote at the front. Removing the space after the : makes the very same declaration work correctly, which is what makes the bug easy to miss.
Affected versions: reproduced on 3.3.1, 3.3.2, 3.4.1 and 3.5.0 (latest). The relevant code is unchanged on master.
Expected behavior
The quotes are part of the CSS syntax, not of the font name. font-family: "Times New Roman", serif and font-family:"Times New Roman",serif and font-family: Times New Roman, serif should all yield w:ascii="Times New Roman".
How to reproduce
Self-contained program (net8.0+ / net10.0, HtmlToOpenXml.dll 3.3.1 or later):
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using HtmlToOpenXml;
const string html =
"<p><span style=\"font-family: "Times New Roman", serif;\">text</span></p>";
using var mem = new MemoryStream();
using (var wordDoc = WordprocessingDocument.Create(mem, WordprocessingDocumentType.Document, true))
{
var mainPart = wordDoc.AddMainDocumentPart();
mainPart.Document = new Document(new Body());
new HtmlConverter(mainPart).ParseBody(html);
mainPart.Document.Save();
foreach (var fonts in mainPart.Document.Descendants<RunFonts>())
Console.WriteLine($"[{fonts.Ascii?.Value}]");
}
// actual: ["Times New Rom]
// expected: [Times New Roman]Measured results for the style attribute value (the " entities are decoded to " by the HTML parser before the CSS value is read):
style value |
w:rFonts/@w:ascii |
correct? |
|---|---|---|
font-family:"ABCDEF" |
ABCDEF |
yes |
font-family: "ABCDEF" |
"ABCD |
no |
font-family: "ABCDEF" |
"AB |
no |
font-family: "Times New Roman", serif |
"Times New Rom |
no |
font-family: 'Times New Roman', serif |
'Times New Rom |
no |
font-family: Times New Roman, serif |
Times New Roman |
yes |
Unquoted names are immune, with or without leading whitespace. The font shorthand (font: bold 12pt "Times New Roman") is affected by the same defect whenever a character precedes the quote inside the font-family token.
Root cause
Converter.ToFontFamily (src/Html2OpenXml/Utilities/Converter.cs, ~line 132) delegates the tokenizing to SplitCompositeAttribute and then just trims the resulting range:
Span<Range> tokens = stackalloc Range[1];
return span.SplitCompositeAttribute(tokens, ',') switch
{
1 => span.Slice(tokens[0]).Trim().ToString(),
_ => null
};The defect is in SpanExtensions.SplitCompositeAttribute (src/Html2OpenXml/Utilities/SpanExtensions.cs, ~line 139), in the branch that opens a quoted escaping sequence (~line 176):
else
{
endEscapingChar = ch; // ' or "
if (index == 0) startIndex++; // exclude the quote from the captured range
}startIndex is an absolute offset into the original span, but index is relative to the current, already re-sliced span, and at the bottom of the loop the span is advanced without startIndex following it:
span = span.Slice(index + 1);So the opening quote is excluded only when it happens to sit at position 0 of the current slice. Two things then go wrong at once when it does not.
Trace for span = ␣"Times New Roman", serif (one leading space), separator = ',':
-
Iteration 1 —
IndexOfAny([',', '(', '\'', '"'])returns1(the"). The quote branch runs, butindex == 0is false, sostartIndexstays0: the opening quote is not excluded. The span is then advanced byindex + 1 = 2, consuming the space and the quote, whilestartIndexremains0. -
Iteration 2 —
IndexOf('"')returns15(the closing quote, relative to the new slice). The range is written asdestination[matches] = new Range(startIndex, startIndex + offsetIndex + index);
i.e.
Range(0, 15), which mixes an absolute start with an index relative to the re-sliced span. That is␣"Times New Romon the original span;.Trim()yields"Times New Rom. The end of the range is short by exactly the number of characters skipped before the quote.
With no leading space, iteration 1 takes index == 0, startIndex becomes 1, and iteration 2 writes Range(1, 16) → Times New Roman, which is why the no-space form is correct.
Suggested fix
Make startIndex follow the characters the loop consumes before the opening quote, so that it always points just past the quote:
else
{
endEscapingChar = ch; // ' or "
- if (index == 0) startIndex++; // exclude the quote from the captured range
+ startIndex += index + 1; // exclude the quote and what precedes it
}For index == 0 this is identical to the current startIndex++, so the currently-passing cases are unaffected; for index > 0 the start now lands right after the opening quote and the Range end computed in the next iteration becomes consistent again. The leading characters that get dropped from the token are the whitespace that .Trim() would remove anyway.
Verified against the table above: every row yields the bare font name after this change, including the font shorthand form.
Test cases worth adding to test/HtmlToOpenXml.Tests:
font-family: "Times New Roman", serif -> Times New Roman
font-family: "Times New Roman" -> Times New Roman
font-family: 'Times New Roman', serif -> Times New Roman
font: bold 12pt "Times New Roman" -> Times New Roman
Activity
Address encoded quote when parsing css styles #239
onizet commentedon Sep 30, 2026
Bug fixed and will be available on next published release