This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new c66a83c2b3 TIKA-5016: keep superscript/subscript as sup/sub in docx,
pptx, doc, RTF and HTML (#3467)
c66a83c2b3 is described below
commit c66a83c2b3be6a7595758ab3aae85e07ea23d248
Author: Tim Allison <[email protected]>
AuthorDate: Fri Oct 30 22:23:22 2026 -0400
TIKA-5016: keep superscript/subscript as sup/sub in docx, pptx, doc, RTF
and HTML (#3467)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
CHANGES.txt | 5 +
.../apache/tika/parser/html/DefaultHtmlMapper.java | 4 +
.../apache/tika/parser/html/HtmlParserTest.java | 10 ++
.../tika/parser/microsoft/FormattingUtils.java | 8 +-
.../microsoft/ooxml/FormattingTagManager.java | 37 ++++-
.../microsoft/ooxml/OOXMLTikaBodyPartHandler.java | 6 +
.../ooxml/OOXMLWordAndPowerPointTextHandler.java | 11 ++
.../tika/parser/microsoft/ooxml/RunProperties.java | 39 ++++++
.../microsoft/ooxml/xwpf/XWPFStylesShim.java | 23 ++++
.../tika/parser/microsoft/rtf/GroupState.java | 6 +
.../tika/parser/microsoft/rtf/TextExtractor.java | 71 +++++++++-
.../tika/parser/microsoft/WordParserTest.java | 3 +-
.../parser/microsoft/ooxml/OOXMLDocxSAXTest.java | 4 +-
.../parser/microsoft/ooxml/OOXMLPptxSAXTest.java | 4 +-
.../parser/microsoft/ooxml/OOXMLSupSubTest.java | 153 +++++++++++++++++++++
.../tika/parser/microsoft/rtf/RTFParserTest.java | 34 +++++
16 files changed, 407 insertions(+), 11 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 4b6e4ebaf6..0ed5b18d8c 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -50,6 +50,11 @@ Release 4.3.0 - ???
EXTRACTION AND FORMATS
+ * Superscript and subscript are kept as <sup>/<sub> in XHTML for docx
(w:vertAlign,
+ directly or through a character style), pptx (a:rPr baseline), doc, RTF
(\super,
+ \sub, \nosupersub) and HTML; they were dropped, so "x<sup>2</sup>" read
"x2" and
+ footnote markers fused with the word before them. .ppt is not covered
(TIKA-5016).
+
* VLM text recognizers (jina-ocr, OpenAI-compatible, Claude, Gemini) no
longer corrupt
LaTeX math in model output: $...$, $$...$$, \(...\) and \[...\] are kept
verbatim
instead of being read as Markdown, which dropped the backslash from \{,
\} and \|
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/main/java/org/apache/tika/parser/html/DefaultHtmlMapper.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/main/java/org/apache/tika/parser/html/DefaultHtmlMapper.java
index 0056106e87..e9c6c03264 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/main/java/org/apache/tika/parser/html/DefaultHtmlMapper.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/main/java/org/apache/tika/parser/html/DefaultHtmlMapper.java
@@ -79,6 +79,10 @@ public class DefaultHtmlMapper implements HtmlMapper {
put("PARAM", "param");
put("INS", "ins");
put("DEL", "del");
+
+ // not presentation: dropping them turns x<sup>2</sup> into "x2"
+ put("SUP", "sup");
+ put("SUB", "sub");
}};
private static final Set<String> DISCARDABLE_ELEMENTS = new
HashSet<String>() {{
add("STYLE");
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/test/java/org/apache/tika/parser/html/HtmlParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/test/java/org/apache/tika/parser/html/HtmlParserTest.java
index 20dd94097f..e39a8593d0 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/test/java/org/apache/tika/parser/html/HtmlParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-html-module/src/test/java/org/apache/tika/parser/html/HtmlParserTest.java
@@ -1431,4 +1431,14 @@ public class HtmlParserTest extends TikaTest {
assertContains("this is content", xml);
}
+
+ @Test
+ public void testSupSubKept() throws Exception {
+ String html = "<html><body><p>E = mc<SUP>2</SUP> and
H<sub>2</sub>O</p></body></html>";
+ String xml;
+ try (TikaInputStream tis = TikaInputStream.get(html.getBytes(UTF_8))) {
+ xml = getXML(tis, TikaTest.AUTO_DETECT_PARSER, new Metadata()).xml;
+ }
+ assertContains("E = mc<sup>2</sup> and H<sub>2</sub>O", xml);
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/FormattingUtils.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/FormattingUtils.java
index 8acb24a5bf..56c79253f1 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/FormattingUtils.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/FormattingUtils.java
@@ -89,6 +89,12 @@ public class FormattingUtils {
if (hwpfRun.getUnderlineCode() != 0) {
tags.add(Tag.U);
}
+ // 1 superscript, 2 subscript
+ if (hwpfRun.getSubSuperScriptIndex() == 1) {
+ tags.add(Tag.SUP);
+ } else if (hwpfRun.getSubSuperScriptIndex() == 2) {
+ tags.add(Tag.SUB);
+ }
}
return tags;
}
@@ -96,7 +102,7 @@ public class FormattingUtils {
public enum Tag {
// DON'T reorder elements to avoid breaking tests: EnumSet is iterated
in natural order
// as enum variants are declared
- B, I, S, U;
+ B, I, S, U, SUP, SUB;
public String tagName() {
return name().toLowerCase(Locale.ROOT);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
index baf80e24aa..abab92c4e9 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.microsoft.ooxml;
import java.util.Objects;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
+import org.apache.poi.xwpf.usermodel.VerticalAlign;
import org.xml.sax.SAXException;
import org.apache.tika.sax.XHTMLContentHandler;
@@ -26,7 +27,8 @@ import org.apache.tika.sax.XHTMLContentHandler;
/**
* Single owner of all run-scoped XHTML wrapper tags, ensuring proper nesting.
* Nesting order from outermost to innermost:
- * {@code <a href="..."><b><i><s><u>text</u></s></i></b></a>}.
+ * {@code <a href="..."><b><i><s><u><sup>text</sup></u></s></i></b></a>}
+ * ({@code <sub>} in place of {@code <sup>}).
* <p>
* Hyperlinks come from two OOXML sources with different lifecycles:
* <ul>
@@ -55,6 +57,8 @@ class FormattingTagManager {
private boolean isItalics = false;
private boolean isStrikeThrough = false;
private boolean isUnderline = false;
+ // "sup", "sub" or null; innermost
+ private String vertTag = null;
FormattingTagManager(XHTMLContentHandler xhtml) {
this.xhtml = xhtml;
@@ -139,6 +143,16 @@ class FormattingTagManager {
currentSpanClass = spanClass;
}
+ boolean runIsUnderlined = runProperties.getUnderline() !=
UnderlinePatterns.NONE;
+ String runVertTag = toVertTag(runProperties.getVerticalAlign());
+ if (vertTag != null && (!vertTag.equals(runVertTag) ||
runProperties.isBold() != isBold
+ || runProperties.isItalics() != isItalics
+ || runProperties.isStrikeThrough() != isStrikeThrough
+ || runIsUnderlined != isUnderline)) {
+ xhtml.endElement(vertTag);
+ vertTag = null;
+ }
+
if (runProperties.isBold() != isBold) {
// Close inner tags before flipping <b>. Nesting is <b><i><s><u>
// (outermost to innermost), so close innermost first: u, s, i.
@@ -193,7 +207,6 @@ class FormattingTagManager {
isStrikeThrough = runProperties.isStrikeThrough();
}
- boolean runIsUnderlined = runProperties.getUnderline() !=
UnderlinePatterns.NONE;
if (runIsUnderlined != isUnderline) {
if (runIsUnderlined) {
xhtml.startElement("u");
@@ -202,6 +215,22 @@ class FormattingTagManager {
}
isUnderline = runIsUnderlined;
}
+
+ if (runVertTag != null && vertTag == null) {
+ xhtml.startElement(runVertTag);
+ vertTag = runVertTag;
+ }
+ }
+
+ private static String toVertTag(VerticalAlign align) {
+ switch (align) {
+ case SUPERSCRIPT:
+ return "sup";
+ case SUBSCRIPT:
+ return "sub";
+ default:
+ return null;
+ }
}
/**
@@ -252,6 +281,10 @@ class FormattingTagManager {
}
private void closeFormattingTags() throws SAXException {
+ if (vertTag != null) {
+ xhtml.endElement(vertTag);
+ vertTag = null;
+ }
if (isUnderline) {
xhtml.endElement("u");
isUnderline = false;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
index f849f92d70..1c612e6b52 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
@@ -146,6 +146,12 @@ public class OOXMLTikaBodyPartHandler
if (!runProperties.isItalicsSet()) {
effectiveRunProperties.italics = styles.isItalics(styleId);
}
+ if (!runProperties.isVerticalAlignSet()) {
+ String styleVertAlign = styles.getVerticalAlign(styleId);
+ if (styleVertAlign != null) {
+ effectiveRunProperties.setVerticalAlign(styleVertAlign);
+ }
+ }
formattingTags.applyFormatting(effectiveRunProperties,
getSpanClass(styleId));
}
xhtml.characters(contents);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
index 569b4065ae..1a0a20bd15 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
@@ -69,6 +69,7 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
private final static String I = "i";
private final static String U = "u";
private final static String STRIKE = "strike";
+ private final static String VERT_ALIGN = "vertAlign";
private final static String NUM_PR = "numPr";
private final static String BR = "br";
private final static String NO_BREAK_HYPHEN = "noBreakHyphen";
@@ -277,6 +278,12 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
//yes, I know, likely premature optimization...
if (RPR.equals(localName)) {
inRPr = true;
+ if (inR && DRAWING_MAIN_NS.equals(uri)) {
+ String baseline = atts.getValue("baseline");
+ if (baseline != null) {
+ currRunProperties.setBaselineOffset(baseline);
+ }
+ }
} else if (O_MATH.equals(localName) && M_NS.equals(uri)) {
if (mathDepth++ == 0) {
bodyContentsHandler.startMath();
@@ -321,6 +328,10 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
if (inR && inRPr) {
currRunProperties.setUnderline(getStringVal(atts));
}
+ } else if (VERT_ALIGN.equals(localName)) {
+ if (inR && inRPr) {
+ currRunProperties.setVerticalAlign(getStringVal(atts));
+ }
} else if (TR.equals(localName)) {
bodyContentsHandler.startTableRow();
} else if (NUM_PR.equals(localName)) {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
index 56fbc6176e..e07c48d011 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
@@ -17,6 +17,7 @@
package org.apache.tika.parser.microsoft.ooxml;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
+import org.apache.poi.xwpf.usermodel.VerticalAlign;
/**
* WARNING: This class is mutable. Make a copy of it
@@ -30,6 +31,9 @@ public class RunProperties {
UnderlinePatterns underline = UnderlinePatterns.NONE;
+ VerticalAlign verticalAlign = VerticalAlign.BASELINE;
+ boolean verticalAlignSet = false;
+
// PPTX hlinkClick hyperlink URL — set from <a:hlinkClick> inside <a:rPr>
String hlinkClickUrl = null;
@@ -95,6 +99,37 @@ public class RunProperties {
}
}
+ public VerticalAlign getVerticalAlign() {
+ return verticalAlign;
+ }
+
+ /** docx w:vertAlign w:val: superscript, subscript, baseline. */
+ public void setVerticalAlign(String val) {
+ verticalAlignSet = true;
+ if ("superscript".equals(val)) {
+ verticalAlign = VerticalAlign.SUPERSCRIPT;
+ } else if ("subscript".equals(val)) {
+ verticalAlign = VerticalAlign.SUBSCRIPT;
+ } else {
+ verticalAlign = VerticalAlign.BASELINE;
+ }
+ }
+
+ public boolean isVerticalAlignSet() {
+ return verticalAlignSet;
+ }
+
+ /** pptx a:rPr baseline, in thousandths of a percent: above 0 raised,
below 0 lowered. */
+ public void setBaselineOffset(String baseline) {
+ try {
+ int v = Integer.parseInt(baseline.trim());
+ verticalAlign = v > 0 ? VerticalAlign.SUPERSCRIPT
+ : v < 0 ? VerticalAlign.SUBSCRIPT : VerticalAlign.BASELINE;
+ } catch (NumberFormatException e) {
+ verticalAlign = VerticalAlign.BASELINE;
+ }
+ }
+
public String getHlinkClickUrl() {
return hlinkClickUrl;
}
@@ -108,6 +143,8 @@ public class RunProperties {
bold = other.bold;
strikeThrough = other.strikeThrough;
underline = other.underline;
+ verticalAlign = other.verticalAlign;
+ verticalAlignSet = other.verticalAlignSet;
hlinkClickUrl = other.hlinkClickUrl;
styleID = other.styleID;
boldSet = other.boldSet;
@@ -119,6 +156,8 @@ public class RunProperties {
bold = false;
strikeThrough = false;
underline = UnderlinePatterns.NONE;
+ verticalAlign = VerticalAlign.BASELINE;
+ verticalAlignSet = false;
hlinkClickUrl = null;
styleID = null;
boldSet = false;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
index c8f77dff47..78d541cb56 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
@@ -109,11 +109,27 @@ public class XWPFStylesShim {
return false;
}
+ /**
+ * @return the w:vertAlign value of the style or the nearest style it is
based on
+ * that sets one; null if none does
+ */
+ public String getVerticalAlign(String styleId) {
+ StyleInfo info = styles.get(styleId);
+ for (int i = 0; info != null && i < MAX_BASED_ON_DEPTH; i++) {
+ if (info.vertAlign != null) {
+ return info.vertAlign;
+ }
+ info = info.basedOn == null ? null : styles.get(info.basedOn);
+ }
+ return null;
+ }
+
private static class StyleInfo {
String name;
String basedOn;
Boolean bold;
Boolean italics;
+ String vertAlign;
}
private static class EmptyXWPFStyles extends XWPFStylesShim {
@@ -132,6 +148,11 @@ public class XWPFStylesShim {
public boolean isItalics(String styleId) {
return false;
}
+
+ @Override
+ public String getVerticalAlign(String styleId) {
+ return null;
+ }
}
private class StylesStripper extends DefaultHandler {
@@ -172,6 +193,8 @@ public class XWPFStylesShim {
current.bold = getOnOff(atts);
} else if (depth == 2 && inStyleRPr && "i".equals(localName)) {
current.italics = getOnOff(atts);
+ } else if (depth == 2 && inStyleRPr &&
"vertAlign".equals(localName)) {
+ current.vertAlign = getVal(atts);
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/GroupState.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/GroupState.java
index 40ff44c94b..124bfa1479 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/GroupState.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/GroupState.java
@@ -22,9 +22,14 @@ import java.nio.charset.Charset;
* ... }. */
class GroupState {
+ static final int SUPER = 1;
+ static final int SUB = 2;
+
public int depth;
public boolean bold;
public boolean italic;
+ // 0 none, SUPER or SUB; nested inside <b><i>
+ public int vert;
// True if we are skipping all text in current group,
// eg if group leads with a \*:
public boolean ignore;
@@ -55,6 +60,7 @@ class GroupState {
public GroupState(GroupState other) {
bold = other.bold;
italic = other.italic;
+ vert = other.vert;
ignore = other.ignore;
ucSkip = other.ucSkip;
list = other.list;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/TextExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/TextExtractor.java
index 1288c1ff51..2f6f4e26b2 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/TextExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/rtf/TextExtractor.java
@@ -666,6 +666,12 @@ final class TextExtractor {
fieldState = 0;
hyperlinkAnchorDepth = -1;
}
+ if (groupState.vert != 0) {
+ end(vertTag(groupState.vert));
+ if (!preserveStyles) {
+ groupState.vert = 0;
+ }
+ }
if (groupState.italic) {
end("i");
groupState.italic = preserveStyles;
@@ -705,7 +711,7 @@ final class TextExtractor {
end(paragraphStack.pop());
}
}
- if (preserveStyles && (groupState.bold || groupState.italic)) {
+ if (preserveStyles && (groupState.bold || groupState.italic ||
groupState.vert != 0)) {
start(P);
pushParagraphTag(P);
startStyles(groupState);
@@ -973,6 +979,7 @@ final class TextExtractor {
if (fieldState == 0) {
if (groupState.bold) {
pushText();
+ endVert();
if (groupState.italic) {
end("i");
}
@@ -980,6 +987,7 @@ final class TextExtractor {
if (groupState.italic) {
start("i");
}
+ startVert();
groupState.bold = false;
}
}
@@ -990,8 +998,10 @@ final class TextExtractor {
if (fieldState == 0) {
if (groupState.italic) {
pushText();
+ endVert();
end("i");
groupState.italic = false;
+ startVert();
}
}
} else if (equals("f")) {
@@ -1212,8 +1222,9 @@ final class TextExtractor {
if (!groupState.bold) {
pushText();
lazyStartParagraph();
+ endVert();
if (groupState.italic) {
- // Make sure nesting is always <b><i>
+ // Make sure nesting is always <b><i><sup|sub>
end("i");
}
groupState.bold = true;
@@ -1224,8 +1235,25 @@ final class TextExtractor {
if (!groupState.italic) {
pushText();
lazyStartParagraph();
+ endVert();
groupState.italic = true;
start("i");
+ startVert();
+ }
+ } else if (equals("super") || equals("sub")) {
+ int vert = equals("super") ? GroupState.SUPER :
GroupState.SUB;
+ if (groupState.vert != vert) {
+ pushText();
+ lazyStartParagraph();
+ endVert();
+ groupState.vert = vert;
+ startVert();
+ }
+ } else if (equals("nosupersub")) {
+ if (groupState.vert != 0) {
+ pushText();
+ endVert();
+ groupState.vert = 0;
}
}
}
@@ -1240,7 +1268,7 @@ final class TextExtractor {
pendingListEnd();
}
} else if (equals("plain")) {
- if (groupState.italic || groupState.bold) {
+ if (groupState.italic || groupState.bold || groupState.vert != 0) {
// Reset styles
pushText();
endStyles(groupState);
@@ -1425,6 +1453,22 @@ final class TextExtractor {
}
}
+ private static String vertTag(int vert) {
+ return vert == GroupState.SUPER ? "sup" : "sub";
+ }
+
+ private void endVert() throws IOException, SAXException, TikaException {
+ if (groupState.vert != 0) {
+ end(vertTag(groupState.vert));
+ }
+ }
+
+ private void startVert() throws IOException, SAXException, TikaException {
+ if (groupState.vert != 0) {
+ start(vertTag(groupState.vert));
+ }
+ }
+
private void startStyles(GroupState groupState)
throws TikaException, IOException, SAXException {
//don't change styles within a <a > element
@@ -1438,13 +1482,21 @@ final class TextExtractor {
if (groupState.italic) {
start("i");
}
+ if (groupState.vert != 0) {
+ start(vertTag(groupState.vert));
+ }
}
+
private void endStyles(GroupState groupState) throws TikaException,
IOException, SAXException {
//don't change styles within a <a > element
if (fieldState != 0) {
return;
}
- // Ensure </i></b> order
+ // Ensure </sup|sub></i></b> order
+ if (groupState.vert != 0) {
+ end(vertTag(groupState.vert));
+ groupState.vert = 0;
+ }
if (groupState.italic) {
end("i");
groupState.italic = false;
@@ -1577,6 +1629,13 @@ final class TextExtractor {
}
//only modify styles if we're not in a hyperlink
if (fieldState == 0) {
+ // Close sup/sub if it or anything outside it changes
+ if (groupState.vert != 0 && (groupState.vert !=
outerGroupState.vert
+ || groupState.italic != outerGroupState.italic
+ || groupState.bold != outerGroupState.bold)) {
+ end(vertTag(groupState.vert));
+ groupState.vert = 0;
+ }
// Close italic, if outer does not have italic or
// bold changed:
if (groupState.italic) {
@@ -1600,6 +1659,10 @@ final class TextExtractor {
if (!groupState.italic && outerGroupState.italic) {
start("i");
}
+
+ if (groupState.vert == 0 && outerGroupState.vert != 0) {
+ start(vertTag(outerGroupState.vert));
+ }
}
groupState = outerGroupState;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
index 54f1dc737c..aaba2e2c96 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
@@ -221,7 +221,8 @@ public class WordParserTest extends TikaTest {
assertTrue(xml.contains("<b>Bold</b>"));
assertTrue(xml.contains("<i>italic</i>"));
assertTrue(xml.contains("<u>underline</u>"));
-
+ assertContains("<sup>superscript</sup>", xml);
+ assertContains("<sub>subscript", xml);
}
@Test
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
index aa0bdb7608..aaf20a109c 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
@@ -527,7 +527,9 @@ public class OOXMLDocxSAXTest extends TikaTest {
assertContains("<b>Bold</b>", xml);
assertContains("<i>italic</i>", xml);
assertContains("<u>underline</u>", xml);
- assertContains("<s>strikethrough</s>", xml);
+ // the run is also subscript
+ assertContains("<s><sub>strikethrough</sub></s>", xml);
+ assertContains("<sup>superscript</sup>", xml);
}
@Test
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLPptxSAXTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLPptxSAXTest.java
index 069a3ab6e6..f9921d5e03 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLPptxSAXTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLPptxSAXTest.java
@@ -153,12 +153,12 @@ public class OOXMLPptxSAXTest extends TikaTest {
Metadata metadata = new Metadata();
String xml = getXML("testPPT_various.pptx", metadata).xml;
assertContains("<p>Footnote appears here", xml);
- assertContains("<p>[1] This is a footnote.", xml);
+ assertContains("<p><sup>[1]</sup> This is a footnote.", xml);
assertContains("<p>This is the header text.</p>", xml);
assertContains("<p>This is the footer text.</p>", xml);
assertContains("<p>Here is a text box</p>", xml);
assertContains("<p>Bold", xml);
- assertContains("italic underline superscript subscript", xml);
+ assertContains("italic underline <sup>superscript</sup>
<sub>subscript</sub>", xml);
assertContains("<p>Here is a citation:", xml);
assertContains("Figure 1 This is a caption for Figure 1", xml);
assertContains("(Kramer)", xml);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLSupSubTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLSupSubTest.java
new file mode 100644
index 0000000000..95d10488f4
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLSupSubTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.ooxml;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.ByteArrayOutputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.utils.XMLReaderUtils;
+
+public class OOXMLSupSubTest extends TikaTest {
+
+ private static final String W =
+
"xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\"";
+
+ @Test
+ public void testDocx() throws Exception {
+ String styles = "<w:styles " + W + "><w:style w:type=\"character\"
w:styleId=\"Note\">"
+ + "<w:name w:val=\"Note Mark\"/><w:rPr><w:vertAlign
w:val=\"superscript\"/>"
+ + "</w:rPr></w:style></w:styles>";
+ String body = run("E = mc", "") + run("2", "<w:vertAlign
w:val=\"superscript\"/>")
+ + run(" and H", "") + run("2", "<w:vertAlign
w:val=\"subscript\"/>")
+ + run("O, note", "") + run("3", "<w:rStyle w:val=\"Note\"/>")
+ + run(" bold", "<w:b/>")
+ + run("x", "<w:b/><w:vertAlign w:val=\"superscript\"/>")
+ + run("y", "<w:vertAlign w:val=\"superscript\"/>")
+ // a direct baseline overrides the style
+ + run(" 4", "<w:rStyle w:val=\"Note\"/><w:vertAlign
w:val=\"baseline\"/>");
+ String xml = parse(docx("<w:document " + W + "><w:body><w:p>" + body
+ + "</w:p></w:body></w:document>", styles));
+ assertContains("E = mc<sup>2</sup> and H<sub>2</sub>O,
note<sup>3</sup>", xml);
+ // sup nests inside b, and closes before </b>
+ assertContains("<b> bold<sup>x</sup></b><sup>y</sup>", xml);
+ assertNotContained("<sup> 4", xml);
+ }
+
+ @Test
+ public void testPptx() throws Exception {
+ String xml = parse(pptx("<a:r><a:rPr lang=\"en-US\"/><a:t>E =
mc</a:t></a:r>"
+ + "<a:r><a:rPr lang=\"en-US\"
baseline=\"30000\"/><a:t>2</a:t></a:r>"
+ + "<a:r><a:rPr lang=\"en-US\"/><a:t> and H</a:t></a:r>"
+ + "<a:r><a:rPr lang=\"en-US\"
baseline=\"-25000\"/><a:t>2</a:t></a:r>"
+ + "<a:r><a:rPr lang=\"en-US\"
baseline=\"0\"/><a:t>O</a:t></a:r>"));
+ assertContains("E = mc<sup>2</sup> and H<sub>2</sub>O", xml);
+ }
+
+ private static String run(String text, String rPr) {
+ return "<w:r>" + (rPr.isEmpty() ? "" : "<w:rPr>" + rPr + "</w:rPr>")
+ + "<w:t xml:space=\"preserve\">" + text + "</w:t></w:r>";
+ }
+
+ private String parse(byte[] bytes) throws Exception {
+ String xml;
+ try (TikaInputStream tis = TikaInputStream.get(bytes)) {
+ xml = getXML(tis, AUTO_DETECT_PARSER, new Metadata()).xml;
+ }
+ XMLReaderUtils.parseSAX(TikaInputStream.get(xml.getBytes(UTF_8)), new
DefaultHandler(),
+ new ParseContext());
+ return xml;
+ }
+
+ private static byte[] docx(String document, String styles) throws
Exception {
+ Map<String, String> parts = new LinkedHashMap<>();
+ parts.put("[Content_Types].xml", "<Types
xmlns=\"http://schemas.openxmlformats.org/"
+ + "package/2006/content-types\">"
+ + "<Default Extension=\"rels\"
ContentType=\"application/vnd.openxmlformats-"
+ + "package.relationships+xml\"/>"
+ + "<Default Extension=\"xml\"
ContentType=\"application/xml\"/>"
+ + "<Override PartName=\"/word/document.xml\"
ContentType=\"application/"
+ +
"vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/>"
+ + "<Override PartName=\"/word/styles.xml\"
ContentType=\"application/"
+ +
"vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\"/>"
+ + "</Types>");
+ parts.put("_rels/.rels", rels("rId1",
"http://schemas.openxmlformats.org/officeDocument/"
+ + "2006/relationships/officeDocument", "word/document.xml"));
+ parts.put("word/_rels/document.xml.rels", rels("rId1",
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
+ "styles.xml"));
+ parts.put("word/document.xml", document);
+ parts.put("word/styles.xml", styles);
+ return zip(parts);
+ }
+
+ private static byte[] pptx(String runs) throws Exception {
+ String pml =
"http://schemas.openxmlformats.org/presentationml/2006/main";
+ String dml = "http://schemas.openxmlformats.org/drawingml/2006/main";
+ String rel =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";
+ Map<String, String> parts = new LinkedHashMap<>();
+ parts.put("[Content_Types].xml", "<Types
xmlns=\"http://schemas.openxmlformats.org/"
+ + "package/2006/content-types\">"
+ + "<Default Extension=\"rels\"
ContentType=\"application/vnd.openxmlformats-"
+ + "package.relationships+xml\"/>"
+ + "<Default Extension=\"xml\"
ContentType=\"application/xml\"/>"
+ + "<Override PartName=\"/ppt/presentation.xml\"
ContentType=\"application/"
+ +
"vnd.openxmlformats-officedocument.presentationml.presentation.main+xml\"/>"
+ + "<Override PartName=\"/ppt/slides/slide1.xml\"
ContentType=\"application/"
+ +
"vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
+ + "</Types>");
+ parts.put("_rels/.rels", rels("rId1", rel + "/officeDocument",
"ppt/presentation.xml"));
+ parts.put("ppt/_rels/presentation.xml.rels", rels("rId2", rel +
"/slide",
+ "slides/slide1.xml"));
+ parts.put("ppt/presentation.xml", "<p:presentation xmlns:p=\"" + pml +
"\" xmlns:r=\""
+ + rel + "\"><p:sldIdLst><p:sldId id=\"256\"
r:id=\"rId2\"/></p:sldIdLst>"
+ + "</p:presentation>");
+ parts.put("ppt/slides/slide1.xml", "<p:sld xmlns:p=\"" + pml + "\"
xmlns:a=\"" + dml
+ + "\"><p:cSld><p:spTree><p:sp><p:txBody><a:bodyPr/><a:p>" +
runs
+ + "</a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>");
+ return zip(parts);
+ }
+
+ private static String rels(String id, String type, String target) {
+ return "<Relationships
xmlns=\"http://schemas.openxmlformats.org/package/2006/"
+ + "relationships\"><Relationship Id=\"" + id + "\" Type=\"" +
type
+ + "\" Target=\"" + target + "\"/></Relationships>";
+ }
+
+ private static byte[] zip(Map<String, String> parts) throws Exception {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(bos)) {
+ for (Map.Entry<String, String> e : parts.entrySet()) {
+ zos.putNextEntry(new ZipEntry(e.getKey()));
+ zos.write(e.getValue().getBytes(UTF_8));
+ zos.closeEntry();
+ }
+ }
+ return bos.toByteArray();
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/rtf/RTFParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/rtf/RTFParserTest.java
index e9c5950027..ba14069a0e 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/rtf/RTFParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/rtf/RTFParserTest.java
@@ -46,6 +46,7 @@ import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
+import org.apache.tika.utils.XMLReaderUtils;
/**
* Junit test class for the Tika {@link RTFParser}
@@ -554,4 +555,37 @@ public class RTFParserTest extends TikaTest {
assertContains("this Agreement on today",
getXML("testRTFTIKA_2899.rtf").xml);
}
+
+ private String rtfXml(String body) throws Exception {
+ String rtf = "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Times;}}\n" + body +
"\n}";
+ String xml;
+ try (TikaInputStream tis =
TikaInputStream.get(rtf.getBytes(StandardCharsets.US_ASCII))) {
+ xml = getXML(tis, new RTFParser(), new Metadata()).xml;
+ }
+ // well-formed: sup/sub must nest inside b/i on every path
+
XMLReaderUtils.parseSAX(TikaInputStream.get(xml.getBytes(StandardCharsets.UTF_8)),
+ new DefaultHandler(), new ParseContext());
+ return xml;
+ }
+
+ @Test
+ public void testSupSub() throws Exception {
+ assertContains("<p>E = mc<sup>2</sup> and H<sub>2</sub>O,
note<sup>3</sup></p>",
+ rtfXml("\\pard E = mc{\\super 2} and H{\\sub 2}O, note{\\super
3}\\par"));
+ // toggled without groups
+ assertContains("<p>x<sup>2</sup>+y<sub>i</sub>z</p>",
+ rtfXml("\\pard x\\super 2\\nosupersub +y\\sub i\\plain
z\\par"));
+ }
+
+ @Test
+ public void testSupSubNestsInsideBoldItalic() throws Exception {
+ // sup opened before bold, bold toggled inside it, italic around it
+ String xml = rtfXml("\\pard a{\\super b\\b c\\b0 d}{\\i e{\\super
f}g}\\par"
+ + "\\pard{\\b\\super h\\par i}\\par");
+ assertContains("a<sup>b</sup><b><sup>c</sup></b><sup>d</sup>", xml);
+ assertContains("<i>e<sup>f</sup>g</i>", xml);
+ // carried across \\par with the bold around it
+ assertContains("<b><sup>h</sup></b></p>", xml);
+ assertContains("<b><sup>i</sup></b>", xml);
+ }
}