Skip to content

Quoted font-family name is truncated and keeps its opening quote when the value has leading whitespace #239

Closed

Description

@fabianosobreira

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: &quot;Times New Roman&quot;, serif;">text</span></p>

Produced by the converter:

<w:rFonts w:ascii="&quot;Times New Rom" w:hAnsi="&quot;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: &quot;Times New Roman&quot;, 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 &quot; 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 = ',':

  1. Iteration 1 — IndexOfAny([',', '(', '\'', '"']) returns 1 (the "). The quote branch runs, but index == 0 is false, so startIndex stays 0: the opening quote is not excluded. The span is then advanced by index + 1 = 2, consuming the space and the quote, while startIndex remains 0.

  2. Iteration 2 — IndexOf('"') returns 15 (the closing quote, relative to the new slice). The range is written as

    destination[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 Rom on 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

  1. onizet commented on Sep 30, 2026

    @onizet
    Owner

    Bug fixed and will be available on next published release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions