package folioxml.export.html;
import folioxml.config.ExportLocations;
import folioxml.config.InfobaseSet;
import folioxml.core.InvalidMarkupException;
import folioxml.core.Pair;
import folioxml.core.TokenUtils;
import folioxml.css.CssUtils;
import folioxml.css.StylesheetBuilder;
import folioxml.export.ExportingNodeListProcessor;
import folioxml.export.FileNode;
import folioxml.export.LogStreamProvider;
import folioxml.export.NodeListProcessor;
import folioxml.text.TextLinesBuilder;
import folioxml.text.TextLinesSequencer;
import folioxml.text.VirtualCharSequence;
import folioxml.translation.FolioCssUtils;
import folioxml.xml.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FauxTabs implements NodeListProcessor, ExportingNodeListProcessor {
public FauxTabs(InfobaseSet config) {
if (!Boolean.TRUE.equals(config.getBool("faux_tabs"))) {
enabled = false;
}
Integer min = config.getInteger("faux_tabs_window_min");
Integer max = config.getInteger("faux_tabs_window_max");
this.minWidthChars = min == null ? 80 : min;
this.maxWidthChars = max == null ? 120 : max;
}
public FauxTabs(int minLineWidth, int maxLineWidth) {
this.minWidthChars = minLineWidth;
this.maxWidthChars = maxLineWidth;
}
@Override
public void setFileNode(FileNode fn) {
}
@Override
public void setLogProvider(LogStreamProvider provider) {
logs = provider;
}
LogStreamProvider logs;
@Override
public void setExportLocations(ExportLocations el) {
}
//We are going to assume left-aligned text, since Folio Views doesn't seem to support other text alignments.
//We are going to assume fixed-width font.
public class TabStop {
public String leaderPattern;
public TabStopPosition position;
public double inches;
public TabStopJustify justification;
}
public enum TabStopPosition {
CustomFromLeft,
Center,
Right
}
public enum TabStopJustify {
Left,
Center,
Right,
Decimal
}
public class FixedTabStop {
public int location;
public TabStopJustify just;
public String leader;
}
private boolean enabled = true;
private int minWidthChars;
private int maxWidthChars;
private String getLeaderOfLength(FixedTabStop ts, int lengthRequired) {
int patternRepetitions = (int) Math.ceil((double) lengthRequired / (double) ts.leader.length());
StringBuilder leader = new StringBuilder(patternRepetitions * ts.leader.length());
for (int i = 0; i < patternRepetitions; i++) leader.append(ts.leader);
return leader.substring(0, lengthRequired);
}
private void FakeTabsInLine(VirtualCharSequence line, List<FixedTabStop> tabStops, int paragraphWidth, int defaultTabSize) {
int tabAt = line.indexOf("\t", 0);
if (tabAt < 0) return;
int nextTab = line.indexOf("\t", tabAt + 1);
boolean moreTabs = true;
if (nextTab < 0) {
nextTab = line.length();
moreTabs = false;
}
//Find first tab stop with a location > tabAt.
FixedTabStop next = null;
for (FixedTabStop ts : tabStops) {
if (ts.location > tabAt) {
next = ts;
break;
}
}
if (next == null) {
//Default tab stops
next = new FixedTabStop();
int lastTabSet = tabStops.size() > 0 ? tabStops.get(tabStops.size() - 1).location : 0;
int offset = (int) Math.ceil((double) (tabAt + 1 - lastTabSet) / (double) defaultTabSize) * defaultTabSize;
next.location = lastTabSet + offset;
next.leader = TokenUtils.entityDecodeString(" ");
next.just = TabStopJustify.Left;
}
int offset = 0;
int textLength = (nextTab - tabAt - 1);
if (next.just == TabStopJustify.Center)
offset = textLength / -2;
if (next.just == TabStopJustify.Right)
offset = -textLength;
if (next.just == TabStopJustify.Decimal) {
int decimalAt = line.indexOf(".", tabAt + 1);
if (decimalAt < nextTab && decimalAt > -1) {
offset = decimalAt - nextTab - 1;
}
}
int leaderLength = Math.max(0, (next.location - tabAt) + offset);
String leader = getLeaderOfLength(next, leaderLength);
line.replace(tabAt, 1, leader);
//Recurse until all tabs are gone.
if (moreTabs) {
FakeTabsInLine(line, tabStops, paragraphWidth, defaultTabSize);
}
}
private void FakeTabs(Node paragraph, IFilter exclude) throws InvalidMarkupException {
ParagraphInfo pi = getParagraphStyle(paragraph);
if (pi == null) pi = new ParagraphInfo();
Double charWidth = pi.getCharWidthInches();
int defaultTabSize = (int) Math.ceil(0.5 / charWidth);
//Abstract tree as a flat set of tokens to edit, grouped by line.
List<VirtualCharSequence> lines = new TextLinesSequencer(new NodeList(paragraph), exclude).getLines(true);
//Determine our fixed width
int paragraphWidth = minWidthChars;
for (VirtualCharSequence line : lines) {
paragraphWidth = Math.max(minWidthChars, Math.min(maxWidthChars, line.length()));
}
//Get tab stops
List<TabStop> tabs = pi.stops;
//Assign inches to center/right tabs
List<FixedTabStop> fixedTabs = fixTabStops(tabs, charWidth, paragraphWidth);
//Replace tabs with leaders
for (VirtualCharSequence line : lines) {
FakeTabsInLine(line, fixedTabs, paragraphWidth, defaultTabSize);
}
}
private List<FixedTabStop> fixTabStops(List<TabStop> tabStops, double charWidthInches, int maxChars) {
if (tabStops == null) return new ArrayList<FixedTabStop>();
List<FixedTabStop> results = new ArrayList<FixedTabStop>(tabStops.size());
for (TabStop ts : tabStops) {
FixedTabStop fts = new FixedTabStop();
fts.leader = ts.leaderPattern;
fts.just = ts.justification;
fts.location = (int) Math.ceil(ts.inches / charWidthInches);
if (ts.position == TabStopPosition.Center)
fts.location = maxChars / 2;
if (ts.position == TabStopPosition.Right)
fts.location = maxChars;
results.add(fts);
}
return results;
}
private XmlRecord getInfobaseRootFor(Node p) throws InvalidMarkupException {
Node recordRoot = p.rootNode();
if (recordRoot instanceof XmlRecord) {
return ((XmlRecord) recordRoot).getRoot();
} else {
throw new InvalidMarkupException("The root Node is not an XmlRecord!");
}
}
private Map<String, ParagraphInfo> getInfobaseStyles(XmlRecord root) throws InvalidMarkupException {
Map<String, ParagraphInfo> results = new HashMap<String, ParagraphInfo>();
NodeList styleDefs = root.children.filterByTagName("style-def", true);
for (Node t : styleDefs.list()) {
String cls = t.get("class");
String style = t.get("style");
String type = t.get("type");
if (cls != null && style != null && "paragraph".equalsIgnoreCase(type)) {
results.put(cls, infoFromStyle(style));
}
}
return results;
}
private Map<XmlRecord, Map<String, ParagraphInfo>> infobaseStyleCache = new HashMap<XmlRecord, Map<String, ParagraphInfo>>();
private Map<String, ParagraphInfo> getInfobaseStylesCached(XmlRecord root) throws InvalidMarkupException {
if (infobaseStyleCache.containsKey(root)) {
return infobaseStyleCache.get(root);
} else {
Map<String, ParagraphInfo> data = getInfobaseStyles(root);
infobaseStyleCache.put(root, data);
return data;
}
}
private ParagraphInfo getParagraphStyle(Node p) throws InvalidMarkupException {
ParagraphInfo pi = infoFromStyle(p.get("style"));
String[] classes = p.get("class") == null ? null : p.get("class").split("\\s+");
if (classes == null || classes.length == 0) return pi;
Map<String, ParagraphInfo> definedStyles = getInfobaseStylesCached(getInfobaseRootFor(p));
for (String cls : classes) {
if (definedStyles.containsKey(cls)) {
pi = cascadeInfo(pi, definedStyles.get(cls));
}
}
return pi;
}
public class ParagraphInfo {
public List<TabStop> stops;
public String fontSize;
public String fontFamilies;
public Double cssWidth;
public void copyFrom(ParagraphInfo second) {
if (second == null) return;
if (second.fontFamilies != null) this.fontFamilies = second.fontFamilies;
if (second.fontSize != null) this.fontSize = second.fontSize;
if (second.cssWidth != null) this.cssWidth = second.cssWidth;
if (second.stops != null && second.stops.size() > 0) this.stops = second.stops; //SHALLOW COPY of tab stops
}
public Double getCharWidthInches() {
double fontInches = FolioCssUtils.toInches(StylesheetBuilder.DEFAULT_FONT_SIZE);
if (fontSize != null) fontInches = FolioCssUtils.toInches(fontSize, fontInches);
//We need to expand the tab positions to help account for the growth from variable-width to fixed-width font.
//Thus the 0.8
return fontSizeToCharWidthRatio() * fontInches * 0.8;
}
}
private Pattern tabStopSpec = Pattern.compile("\\A(Center|Right|[\\d.]+) (NM|CN|RT|CA) (NO|DS|DO|DA|UN)\\Z", Pattern.CASE_INSENSITIVE);
private TabStop parseTabStop(String folioSpec) throws InvalidMarkupException {
Matcher m = tabStopSpec.matcher(folioSpec);
if (!m.find()) throw new InvalidMarkupException("Invalid tab stop specification: " + folioSpec);
TabStop ts = new TabStop();
if ("Center".equalsIgnoreCase(m.group(1)))
ts.position = TabStopPosition.Center;
else if ("Right".equalsIgnoreCase(m.group(1)))
ts.position = TabStopPosition.Right;
else {
ts.position = TabStopPosition.CustomFromLeft;
ts.inches = Double.parseDouble(m.group(1));
}
if ("NM".equalsIgnoreCase(m.group(2)))
ts.justification = TabStopJustify.Left;
if ("CN".equalsIgnoreCase(m.group(2)))
ts.justification = TabStopJustify.Center;
if ("RT".equalsIgnoreCase(m.group(2)))
ts.justification = TabStopJustify.Right;
if ("CA".equalsIgnoreCase(m.group(2)))
ts.justification = TabStopJustify.Decimal;
if ("NO".equalsIgnoreCase(m.group(3)))
ts.leaderPattern = TokenUtils.entityDecodeString(" ");
if ("DS".equalsIgnoreCase(m.group(3)))
ts.leaderPattern = TokenUtils.entityDecodeString(" .");
if ("DO".equalsIgnoreCase(m.group(3)))
ts.leaderPattern = TokenUtils.entityDecodeString(".");
if ("DA".equalsIgnoreCase(m.group(3)))
ts.leaderPattern = TokenUtils.entityDecodeString(" -");
if ("UN".equalsIgnoreCase(m.group(3)))
ts.leaderPattern = TokenUtils.entityDecodeString("_");
return ts;
}
/*
var id = 0;
var sum = 0;
var fontname = "Courier New";
//This finds the ratio between inches (in font size) and character width of 'x' for a given font.
for (var i = 0.09; i < 1; i += 0.001){
document.write("<span style='font-family: " + fontname + ";font-size:" + i + "in' id='x" + id + "'>x</span>");
var width = document.getElementById("x" + id).offsetWidth
document.write(i.toString() + " -> " + width + " (" + (width / i) + ")");
sum += (width / i);
document.write("<br>");
id++;
}
document.write("<strong>Avg: " + (sum / id) + "px, " + (sum / id / 96) + "in</strong>");
*/
private double fontSizeToCharWidthRatio() {
//See the javascript above for calculating this magic value for any font
//This values was CourierNew on Chrome
return 0.6138741700947206;
}
private ParagraphInfo infoFromStyle(String style) throws InvalidMarkupException {
if (style == null) return null;
ParagraphInfo pi = new ParagraphInfo();
List<TabStop> stops = new ArrayList<TabStop>();
List<Pair<String, String>> css = CssUtils.parseCssAsList(style, false);
for (Pair<String, String> p : css) {
if (p.getFirst().equals("width")) {
pi.cssWidth = FolioCssUtils.toInches(p.getSecond(), null);
}
if (p.getFirst().equals("font-size")) {
pi.fontSize = p.getSecond();
}
if (p.getFirst().equals("font-family")) {
pi.fontFamilies = p.getSecond();
}
if (p.getFirst().equals("-folio-tab-set")) {
stops.add(parseTabStop(p.getSecond()));
}
}
pi.stops = stops;
return pi;
}
private ParagraphInfo cascadeInfo(ParagraphInfo first, ParagraphInfo second) {
ParagraphInfo pi = new ParagraphInfo();
pi.copyFrom(first);
pi.copyFrom(second);
return pi;
}
@Override
public NodeList process(NodeList nodes) throws InvalidMarkupException, IOException {
if (!enabled) return nodes;
//We need to exclude any popups from being adjusted.
IFilter exclusions = new NodeFilter("popup|table|td|th|note|div|record");
//For indentation-only use, we could apply a leader of " " (decoded) and leave the class name unchanged.
NodeList paragraphs = nodes.searchOuter(new NodeFilter("p"));
for (Node p : paragraphs.list()) {
List<StringBuilder> lines = new TextLinesBuilder().generateLines(new NodeList(p));
//Analyze tab usage
TextLinesBuilder.TabUsage tabs = new TextLinesBuilder().analyzeTabUsage(lines);
if (tabs == TextLinesBuilder.TabUsage.Indentation) {
FakeTabs(p, exclusions);
p.addClass("faux_tabs_indentation");
} else if (tabs == TextLinesBuilder.TabUsage.Tabulation) {
FakeTabs(p, exclusions);
p.addClass("faux_tabulation");
} else if (tabs == TextLinesBuilder.TabUsage.ListAlignment) {
p.addClass("tabs_list_alignment");
}
if (tabs != TextLinesBuilder.TabUsage.None) {
//Do before/after per line
List<StringBuilder> newLines = new TextLinesBuilder().generateLines(new NodeList(p));
for (int i = 0; i < lines.size() && i < newLines.size(); i++) {
logs.getNamedStream("faux_" + tabs.toString().toLowerCase()).append(newLines.get(i)).append("\n");
}
}
}
return nodes;
}
}