diff --git a/example/example-node.js b/example/example-node.js index c2b9c9c9..4666997a 100644 --- a/example/example-node.js +++ b/example/example-node.js @@ -2031,6 +2031,15 @@ const htmlString = ` alt="3cm width, 1in height" /> + +

StrikeThrough Tests

+

This is a strikethough test.

+

This is a strikethough test.

+

This is a strikethough test.

+

This is a strikethough test.

+

This is a strikethough test.

+

This is a strikethough test.

+ `; diff --git a/example/react-example/src/App.js b/example/react-example/src/App.js index 67be552a..33968125 100644 --- a/example/react-example/src/App.js +++ b/example/react-example/src/App.js @@ -32,14 +32,14 @@ const htmlString = ` src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Test image with color and font-family styles" /> - +

Testing images with mixed dimensional and non-dimensional styles:

Test image with mixed styles - +

Testing images with various non-dimensional styles:

src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Test image with border and margin" /> + + +

SVG Image Support

+

Testing SVG images (automatically converted to PNG for maximum compatibility):

+ +

Simple SVG circle:

+ Blue circle SVG + +

SVG with different sizes:

+ Checkmark icon + Checkmark icon + +

Inline SVG icon: Success SVG works inline too!

+ +

SVG bar chart:

+ Sales chart

This is heading 1

@@ -1658,6 +1694,33 @@ const htmlString = `
  • Heading 3
  • +
    +

    One More test case

    +

    PHASE ONE
    CONFIDENTIAL
    HOURS: 1 – 2

    +

    TIER: 1

    +

    Scheduled Work: Business Hours
    Location: + Remote +

    + +

    Table with empty row that could corrupt

    @@ -1775,7 +1838,7 @@ const htmlString = `

    - +

    Testing aspect ratio safety with zero/invalid dimensions:

    @@ -1789,7 +1852,7 @@ const htmlString = ` src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Test image with zero width" /> - +

    Testing extreme aspect ratios:

    src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Very tall image" /> - +

    Testing auto dimensions with CSS styles:

    Auto dimensions with non-dimensional styles - +

    Testing images in figures (lineRule attribute fix):

    />
    Image with consistent lineRule processing
    - +

    Testing images with max-width/max-height constraints:

    Image with max constraints - +

    Testing images exceeding maximum document width (auto-scaling):

    alt="Oversized image that should auto-scale" />
    - + +
    + + + + + + + + + + + + + + + + +
    +

    testset

    +
    +

    test

    +
    +

    data1

    +
    +

    data2

    +
    +

    data3

    +
    +
    @@ -1841,7 +1933,7 @@ const htmlString = `

    Testing Image Width/Height Attribute Handling (Fix for TinyMCE dimensions)

    These test cases verify that width and height HTML attributes are properly honored in DOCX generation:

    Test image original size: 5,807 × 2,817 pixels (8.35 MB JPEG)

    - +

    Test 1: Image with explicit width and height attributes (should render as 100x100):

    height="100" alt="100x100 dimensions test" /> - +

    Test 2: Image with only width attribute (height should maintain aspect ratio):

    Width only test - +

    Test 3: Image with only height attribute (width should maintain aspect ratio):

    Height only test - +

    Test 4: Image with width/height and additional styles (TinyMCE scenario):

    height="60" alt="TinyMCE style with dimensions" /> - +

    Test 5: Image without dimensions (should use original image size - fallback behavior):

    No dimensions - fallback test - +

    Test 6: Larger image with custom dimensions (demonstrates actual resize):

    height="100" alt="Large image resized to 200x100" /> + +

    Unit Support Tests

    +

    Test 7: Image with pixel units (explicit px):

    + 180px x 90px image + +

    Test 8: Image with point units (pt):

    + 144pt x 72pt image (192px x 96px equivalent) + +

    Test 9: Image with centimeter units (cm):

    + 4cm x 2cm image + +

    Test 10: Image with inch units (in):

    + 1.5in x 0.75in image + +

    Test 11: Image with percentage units (% of original size):

    + 10% x 10% of original size + +

    Test 12: Mixed units - width in cm, height in inches:

    + 3cm width, 1in height
    + +

    StrikeThrough Tests

    +

    This is a strikethough test.

    +

    This is a strikethough test.

    +

    This is a strikethough test.

    +

    This is a strikethough test.

    +

    This is a strikethough test.

    +

    This is a strikethough test.

    + `; diff --git a/src/helpers/xml-builder.js b/src/helpers/xml-builder.js index 3d4dd6b0..ea010d61 100644 --- a/src/helpers/xml-builder.js +++ b/src/helpers/xml-builder.js @@ -874,6 +874,11 @@ const buildRun = async (vNode, attributes, docxDocumentInstance) => { case 'sup': tempAttributes.sup = true; break; + case 'del': + case 's': + case 'strike': + tempAttributes.strike = true; + break; } const formattingFragment = buildFormatting(tempVNode); formattingFragmentAttributes = modifiedStyleAttributesBuilder( diff --git a/tests/helpers/docx-assertions.js b/tests/helpers/docx-assertions.js index f5fb080d..f664e75a 100644 --- a/tests/helpers/docx-assertions.js +++ b/tests/helpers/docx-assertions.js @@ -127,3 +127,47 @@ export function assertParagraphProperty(parsed, paragraphIndex, propertyName, ex const para = validateParagraphIndex(parsed, paragraphIndex, 'assertParagraphProperty'); expect(para.properties[propertyName]).toBe(expectedValue); } + +/** + * Assert that a specific run in a paragraph has strikethrough formatting + * @param {Object} parsed - Parsed DOCX object from parseDOCX() + * @param {number} paragraphIndex - Zero-based paragraph index + * @param {number} runIndex - Zero-based run index within the paragraph + */ +export function assertRunHasStrike(parsed, paragraphIndex, runIndex = 0) { + const para = validateParagraphIndex(parsed, paragraphIndex, 'assertRunHasStrike'); + + if (para.runs.length === 0) { + throw new Error(`assertRunHasStrike: Paragraph ${paragraphIndex} has no text runs`); + } + + if (runIndex >= para.runs.length) { + throw new Error( + `assertRunHasStrike: Run index ${runIndex} out of range. Paragraph has ${para.runs.length} runs.` + ); + } + + expect(para.runs[runIndex].strike).toBe(true); +} + +/** + * Assert that a specific run in a paragraph does NOT have strikethrough formatting + * @param {Object} parsed - Parsed DOCX object from parseDOCX() + * @param {number} paragraphIndex - Zero-based paragraph index + * @param {number} runIndex - Zero-based run index within the paragraph + */ +export function assertRunNoStrike(parsed, paragraphIndex, runIndex = 0) { + const para = validateParagraphIndex(parsed, paragraphIndex, 'assertRunNoStrike'); + + if (para.runs.length === 0) { + throw new Error(`assertRunNoStrike: Paragraph ${paragraphIndex} has no text runs`); + } + + if (runIndex >= para.runs.length) { + throw new Error( + `assertRunNoStrike: Run index ${runIndex} out of range. Paragraph has ${para.runs.length} runs.` + ); + } + + expect(para.runs[runIndex].strike).toBeFalsy(); +} diff --git a/tests/helpers/docx-validator.js b/tests/helpers/docx-validator.js index c90f5514..0954aae0 100644 --- a/tests/helpers/docx-validator.js +++ b/tests/helpers/docx-validator.js @@ -154,6 +154,11 @@ export function extractRunProperties(paragraphXml) { runProps.italic = true; } + // Extract strikethrough + if (runXml.includes('')) { + runProps.strike = true; + } + // Extract font size const sizeMatch = runXml.match(FONT_SIZE_REGEX); if (sizeMatch) { diff --git a/tests/strikethrough.test.js b/tests/strikethrough.test.js new file mode 100644 index 00000000..0d344478 --- /dev/null +++ b/tests/strikethrough.test.js @@ -0,0 +1,398 @@ +/** + * Strikethrough Integration Tests + * Tests for strikethrough/strike-through text formatting using , , and tags + * + * Related PR: https://github.com/TurboDocx/html-to-docx/pull/157 + */ + +import HTMLtoDOCX from '../index.js'; +import { + parseDOCX, + assertParagraphCount, + assertParagraphText, + assertRunHasStrike, + assertRunNoStrike, +} from './helpers/docx-assertions.js'; + +describe('Strikethrough Formatting', () => { + describe('Basic strikethrough tags', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should apply strikethrough formatting with tag', async () => { + const htmlString = '

    deleted text

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'deleted text'); + assertRunHasStrike(parsed, 0, 0); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should apply strikethrough formatting with tag', async () => { + const htmlString = '

    strikethrough text

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'strikethrough text'); + assertRunHasStrike(parsed, 0, 0); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should apply strikethrough formatting with tag', async () => { + const htmlString = '

    strike text

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'strike text'); + assertRunHasStrike(parsed, 0, 0); + }); + }); + + describe('Mixed content with strikethrough', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough text mixed with regular text', async () => { + const htmlString = '

    This is a strikethrough test.

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'This is a strikethrough test.'); + + // First run should NOT have strikethrough (regular text) + assertRunNoStrike(parsed, 0, 0); + // Second run should have strikethrough + assertRunHasStrike(parsed, 0, 1); + // Third run should NOT have strikethrough (regular text) + assertRunNoStrike(parsed, 0, 2); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle tag mixed with regular text', async () => { + const htmlString = '

    This is a strikethrough test.

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'This is a strikethrough test.'); + assertRunHasStrike(parsed, 0, 1); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle tag mixed with regular text', async () => { + const htmlString = '

    This is a strikethrough test.

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'This is a strikethrough test.'); + assertRunHasStrike(parsed, 0, 1); + }); + }); + + describe('Strikethrough combined with other formatting', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough with bold formatting', async () => { + const htmlString = '

    bold strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'bold strikethrough'); + assertRunHasStrike(parsed, 0, 0); + expect(parsed.paragraphs[0].runs[0].bold).toBe(true); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough with italic formatting', async () => { + const htmlString = '

    italic strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'italic strikethrough'); + assertRunHasStrike(parsed, 0, 0); + expect(parsed.paragraphs[0].runs[0].italic).toBe(true); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough with bold and italic formatting', async () => { + const htmlString = '

    bold italic strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'bold italic strikethrough'); + assertRunHasStrike(parsed, 0, 0); + expect(parsed.paragraphs[0].runs[0].bold).toBe(true); + expect(parsed.paragraphs[0].runs[0].italic).toBe(true); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle nested formatting - bold inside del', async () => { + const htmlString = '

    bold inside del

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'bold inside del'); + assertRunHasStrike(parsed, 0, 0); + expect(parsed.paragraphs[0].runs[0].bold).toBe(true); + }); + }); + + describe('Multiple strikethrough elements', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle multiple strikethrough elements in same paragraph', async () => { + const htmlString = '

    first and second

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'first and second'); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough in multiple paragraphs', async () => { + const htmlString = ` +

    First strikethrough

    +

    Second strikethrough

    +

    Third strikethrough

    + `; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 3); + assertParagraphText(parsed, 0, 'First strikethrough'); + assertParagraphText(parsed, 1, 'Second strikethrough'); + assertParagraphText(parsed, 2, 'Third strikethrough'); + assertRunHasStrike(parsed, 0, 0); + assertRunHasStrike(parsed, 1, 0); + assertRunHasStrike(parsed, 2, 0); + }); + }); + + describe('Edge cases', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle empty strikethrough tag', async () => { + const htmlString = '

    BeforeAfter

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + // Empty del tag should not affect surrounding text + expect(parsed.paragraphs[0].text).toContain('Before'); + expect(parsed.paragraphs[0].text).toContain('After'); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough with whitespace', async () => { + const htmlString = '

    spaced text

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + expect(parsed.paragraphs[0].text).toContain('spaced text'); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough with special characters', async () => { + const htmlString = '

    & < > "quotes"

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, '& < > "quotes"'); + assertRunHasStrike(parsed, 0, 0); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle deeply nested strikethrough', async () => { + const htmlString = '

    deeply nested

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'deeply nested'); + }); + }); + + describe('Strikethrough in complex structures', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough in list items', async () => { + const htmlString = ` +
      +
    • deleted item
    • +
    • normal item
    • +
    + `; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + // List items are converted to paragraphs + expect(parsed.paragraphs.length).toBeGreaterThanOrEqual(2); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough in table cells', async () => { + const htmlString = ` + + + + + +
    deleted cellnormal cell
    + `; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + // Table content should be present + expect(parsed.xml).toContain('deleted cell'); + expect(parsed.xml).toContain('normal cell'); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle strikethrough in headings', async () => { + const htmlString = '

    Deleted Heading

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'Deleted Heading'); + }); + }); + + describe('All three strikethrough tag variants', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should produce equivalent output for del, s, and strike tags', async () => { + const htmlDel = '

    text

    '; + const htmlS = '

    text

    '; + const htmlStrike = '

    text

    '; + + const resultDel = await HTMLtoDOCX(htmlDel, {}); + const resultS = await HTMLtoDOCX(htmlS, {}); + const resultStrike = await HTMLtoDOCX(htmlStrike, {}); + + const parsedDel = await parseDOCX(resultDel); + const parsedS = await parseDOCX(resultS); + const parsedStrike = await parseDOCX(resultStrike); + + // All three should have the same text + assertParagraphText(parsedDel, 0, 'text'); + assertParagraphText(parsedS, 0, 'text'); + assertParagraphText(parsedStrike, 0, 'text'); + + // All three should have strikethrough formatting + assertRunHasStrike(parsedDel, 0, 0); + assertRunHasStrike(parsedS, 0, 0); + assertRunHasStrike(parsedStrike, 0, 0); + }); + }); + + describe('XML output verification', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should generate correct w:strike element in XML', async () => { + const htmlString = '

    strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + // Verify the raw XML contains the strike element + expect(parsed.xml).toContain('w:strike'); + }); + }); + + describe('CSS text-decoration: line-through', () => { + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should apply strikethrough via inline style text-decoration: line-through', async () => { + const htmlString = '

    strikethrough via CSS

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'strikethrough via CSS'); + assertRunHasStrike(parsed, 0, 0); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle CSS strikethrough mixed with regular text', async () => { + const htmlString = '

    This is a strikethrough test.

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'This is a strikethrough test.'); + // The strikethrough run should have strike formatting + assertRunHasStrike(parsed, 0, 1); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle CSS strikethrough on paragraph level', async () => { + const htmlString = '

    entire paragraph strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'entire paragraph strikethrough'); + // Verify strike is in the XML + expect(parsed.xml).toContain('w:strike'); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should produce equivalent output for CSS and HTML tag strikethrough', async () => { + const htmlTag = '

    text

    '; + const htmlCSS = '

    text

    '; + + const resultTag = await HTMLtoDOCX(htmlTag, {}); + const resultCSS = await HTMLtoDOCX(htmlCSS, {}); + + const parsedTag = await parseDOCX(resultTag); + const parsedCSS = await parseDOCX(resultCSS); + + // Both should have the same text + assertParagraphText(parsedTag, 0, 'text'); + assertParagraphText(parsedCSS, 0, 'text'); + + // Both should have strikethrough formatting + assertRunHasStrike(parsedTag, 0, 0); + assertRunHasStrike(parsedCSS, 0, 0); + }); + + // https://github.com/TurboDocx/html-to-docx/pull/157 + test('should handle combined underline and line-through', async () => { + const htmlString = '

    underline and strikethrough

    '; + + const result = await HTMLtoDOCX(htmlString, {}); + const parsed = await parseDOCX(result); + + assertParagraphCount(parsed, 1); + assertParagraphText(parsed, 0, 'underline and strikethrough'); + // Verify both underline and strike are in the XML + expect(parsed.xml).toContain('w:strike'); + expect(parsed.xml).toContain('w:u'); + }); + }); +});