All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.magicwerk.brownies.html.content.HtmlTokenLineFormatter Maven / Gradle / Ivy

The newest version!
package org.magicwerk.brownies.html.content;

import java.util.Map;

import org.magicwerk.brownies.collections.GapList;
import org.magicwerk.brownies.collections.IList;
import org.magicwerk.brownies.collections.primitive.BooleanGapList;
import org.magicwerk.brownies.collections.primitive.IBooleanList;
import org.magicwerk.brownies.core.CollectionTools;
import org.magicwerk.brownies.core.CollectionTools.Indexer;
import org.magicwerk.brownies.core.CollectionTools.UniqueIndexer;
import org.magicwerk.brownies.core.TypeTools;
import org.magicwerk.brownies.core.strings.TokenLine;
import org.magicwerk.brownies.core.strings.TokenString;
import org.magicwerk.brownies.core.strings.lines.MatchToken;
import org.magicwerk.brownies.core.strings.text.TextRange;
import org.magicwerk.brownies.core.strings.text.TextTools;
import org.magicwerk.brownies.html.HtmlDoclet;
import org.magicwerk.brownies.html.HtmlFlow;
import org.magicwerk.brownies.html.HtmlResource;
import org.magicwerk.brownies.html.HtmlResources;
import org.magicwerk.brownies.html.HtmlTable;
import org.magicwerk.brownies.html.HtmlTools;
import org.magicwerk.brownies.html.HtmlTr;

/**
 * Class {@link HtmlTokenLineFormatter} formats source files.
 */
public class HtmlTokenLineFormatter {

	/** Class {@link TokenTextCreator} help to create a {@literal List>} */
	public static class TokenTextCreator {

		Object source;
		int row;
		IList tokenLines;

		//

		public TokenTextCreator setSource(Object source) {
			this.source = source;
			return this;
		}

		public TokenTextCreator setRow(int row) {
			this.row = row;
			return this;
		}

		public TokenTextCreator setTokenLines(IList tokenLines) {
			this.tokenLines = tokenLines;
			return this;
		}

		public IList getTokenLines() {
			return tokenLines;
		}

		//

		public IList createText(IList lines) {
			tokenLines = GapList.create();
			int r = row;
			for (String line : lines) {
				tokenLines.add(createTokenLine(source, r, line));
				r++;
			}
			return tokenLines;
		}

		public IList createText(String str) {
			tokenLines = GapList.create();
			int r = row;
			for (String line : new TextTools.LineIterator(str).setIncludeEol(false)) {
				tokenLines.add(createTokenLine(source, r, line));
				r++;
			}
			return tokenLines;
		}

		public void markRange(TextRange range, boolean mark) {
			markRange(range, getMark(mark));
		}

		public void markRange(TextRange range, Integer mark) {
			int startRow = range.getStart().getRow();
			int endRow = range.getEnd().getRow();
			if (startRow == endRow) {
				setToken(startRow, range.getStart().getCol(), range.getEnd().getCol(), mark);

			} else {
				TokenString ts = getTokenString(startRow);
				ts.setToken(range.getStart().getCol(), ts.length(), mark);

				for (int r = startRow + 1; r < endRow; r++) {
					markLine(r, mark);
				}

				ts = getTokenString(endRow);
				ts.setToken(0, range.getEnd().getCol(), mark);
			}
		}

		public void markLine(int row, boolean mark) {
			markLine(row, getMark(mark));
		}

		int getMark(boolean mark) {
			return (mark) ? 0 : -1;
		}

		void markLine(int row, Integer mark) {
			setToken(row, mark);
		}

		void setToken(int row, Integer token) {
			TokenString ts = tokenLines.get(row).getTokenString();
			ts.setToken(0, ts.length(), token);
		}

		TokenString getTokenString(int row) {
			return tokenLines.get(row).getTokenString();
		}

		void setToken(int row, int startCol, int endCol, Integer token) {
			TokenString ts = getTokenString(row);
			ts.setToken(startCol, endCol, token);
		}

		TokenLine createTokenLine(Object source, int row, String line) {
			TokenString ts = createTokenString(line);
			return new TokenLine(source, row, ts);
		}

		TokenString createTokenString(String line) {
			return new TokenString<>(line);
		}

	}

	public static final String CSS_CLASS_LINES = "lines";
	public static final String CSS_CLASS_LINE = "line";
	public static final String CSS_CLASS_LINE_HIDE = "hide";
	public static final String CSS_CLASS_LINE_NUM = "n";
	public static final String CSS_CLASS_LINE_TEXT = "t";

	//@formatter:off
	static final String CSS = 
			"/* Table containing lines, each line is a  element, consisting of two  elements for line number and text */\n" + 
			"table.lines { margin: 4px; border: 0px; border-collapse: collapse; } \n" + 
			"table.lines td { border: 0px; } \n" + 
			"/* Line number */\n" + 
			"table.lines tr.line td.n { vertical-align: top; text-align: right; background-color: #d9d9d9; font-family: monospace; user-select: none; } /* light grey 1 */\n" + 
			"/* Line text, wrapped */\n" + 
			"table.lines tr.line td.t { padding: 2px 4px; border: 0px; vertical-align: top; background-color: #f3f3f3; font-family: monospace; white-space:pre-wrap; tab-size:4; } /* #light grey 3 */\n" + 
			"/* Line text, not wrapped */\n" + 
			"table.lines.nowrap tr.line td.t { padding: 2px 4px; border: 0px; vertical-align: top; background-color: #f3f3f3; font-family: monospace; white-space:pre; tab-size:4; } /* #light grey 3 */\n" + 
			"\n" + 
			"/* Selection: Background of selected text */\n" + 
			"table.lines tr.line td.t span.sel9 { background-color: #cc4125; } /* light red berry 1 */\n" + 
			"table.lines tr.line td.t span.sel0 { background-color: #e06666; } /* light red 1 */\n" + 
			"table.lines tr.line td.t span.sel8 { background-color: #f6b26b; } /* light orange 1 */\n" + 
			"table.lines tr.line td.t span.sel1 { background-color: #ffd966; } /* light yellow 1 */\n" + 
			"table.lines tr.line td.t span.sel7 { background-color: #93c47d; } /* light green 1 */\n" + 
			"table.lines tr.line td.t span.sel2 { background-color: #76a5af; } /* light cyan 1 */\n" + 
			"table.lines tr.line td.t span.sel6 { background-color: #6d9eeb; } /* light cornflower blue 1 */\n" + 
			"table.lines tr.line td.t span.sel3 { background-color: #6fa8dc; } /* light blue 1 */\n" + 
			"table.lines tr.line td.t span.sel5 { background-color: #8e7cc3; } /* light purple 1 */\n" + 
			"table.lines tr.line td.t span.sel4 { background-color: #c27ba0; } /* light magenta 1 */\n" + 
			"\n" + 
			"/* Selection: Background of line with selected text */\n" + 
			"table.lines tr.line.sel9 td.t { background-color: #dd7e6b; } /* light red berry 2 */\n" + 
			"table.lines tr.line.sel0 td.t { background-color: #ea9999; } /* light red 2 */\n" + 
			"table.lines tr.line.sel8 td.t { background-color: #f9cb9c; } /* light orange 2 */\n" + 
			"table.lines tr.line.sel1 td.t { background-color: #ffe599; } /* light yellow 2 */\n" + 
			"table.lines tr.line.sel7 td.t { background-color: #b6d7a8; } /* light green 2 */\n" + 
			"table.lines tr.line.sel2 td.t { background-color: #a2c4c9; } /* light cyan 2 */\n" + 
			"table.lines tr.line.sel6 td.t { background-color: #a4c2f4; } /* light cornflower blue 2 */\n" + 
			"table.lines tr.line.sel3 td.t { background-color: #9fc5e8; } /* light blue 2 */\n" + 
			"table.lines tr.line.sel5 td.t { background-color: #b4a7d6; } /* light purple 2 */\n" + 
			"table.lines tr.line.sel4 td.t { background-color: #d5a6bd; } /* light magenta 2 */\n"; 

	static final String JS = 
			  "$(document).ready(function() { \n"
			+ "	$('.lines.hide .hide').hide(); \n"
			+ "	\n"
			+ "	$('.line').dblclick(function() { \n"
			+ "   var lines = $(this).closest('.lines'); \n"
			+ "	  $(lines).find('.hide').toggle(); \n"
			+ "	}); \n"
			+ "}); \n";
	//@formatter:on

	//

	/** True to enable focus for the lines with tokens (user can double-click on focused lines to see full text) */
	boolean focusEnabled = true;
	/** If true, only the focused lines are shown on start (only if {@link #focusEnabled} is set) */
	boolean focusOnStart = true;
	/** Size of window to include around focused lines (only if {@link #focusEnabled} is set) */
	int focusWindow = 1;
	Indexer indexer;

	//

	public HtmlTokenLineFormatter setFocusEnabled(boolean focusEnabled) {
		this.focusEnabled = focusEnabled;
		return this;
	}

	public HtmlTokenLineFormatter setFocusOnStart(boolean focusOnStart) {
		this.focusOnStart = focusOnStart;
		return this;
	}

	public HtmlTokenLineFormatter setFocusWindow(int focusWindow) {
		this.focusWindow = focusWindow;
		return this;
	}

	@SuppressWarnings("unchecked")
	public HtmlTokenLineFormatter setIndexer(Indexer indexer) {
		this.indexer = (Indexer) indexer;
		return this;
	}

	//

	/**
	 * Returns static HTML resources needed for formatting token lines.
	 */
	public static HtmlResources getHtmlResources() {
		HtmlResource res = new HtmlResource("TokenLineFormatter");
		res.getHead().addStyleCss(CSS);
		res.getHead().addScriptJs(JS);

		HtmlResources rs = new HtmlResources();
		rs.add(HtmlJs.getJqueryResource());
		rs.add(res);
		return rs;
	}

	public HtmlDoclet format(IList lines) {
		if (indexer == null) {
			indexer = new UniqueIndexer<>();
		}

		int size = lines.size();
		IBooleanList focuses = null;
		if (focusEnabled) {
			focuses = new BooleanGapList(size);
			focuses.initMult(size, false);
			for (int i = 0; i < lines.size(); i++) {
				TokenString ts = lines.get(i).getTokenString();
				if (ts.hasNonNullToken()) {
					setFocus(focuses, i);
				}
			}
		}

		// Add class "nowrap" to prevent wrapping
		HtmlTable tab = new HtmlTable();
		if (focusEnabled && focusOnStart) {
			tab.setClass(CSS_CLASS_LINES, CSS_CLASS_LINE_HIDE);
		} else {
			tab.setClass(CSS_CLASS_LINES);
		}
		for (int i = 0; i < size; i++) {
			boolean focus = (focusEnabled) ? focuses.get(i) : true;
			HtmlTr tr = createLineHtml(lines.get(i), focus);
			tab.addElem(tr);
		}

		return new HtmlDoclet(tab, getHtmlResources());
	}

	void setFocus(IBooleanList focuses, int row) {
		focuses.set(row, true);
		for (int i = 1; i <= focusWindow; i++) {
			int r = row - i;
			if (r >= 0) {
				focuses.set(r, true);
			}
			r = row + i;
			if (r < focuses.size()) {
				focuses.set(r, true);
			}
		}
	}

	HtmlTr createLineHtml(TokenLine tl, boolean focus) {
		HtmlTr tr = new HtmlTr();
		if (focus) {
			tr.setClass(CSS_CLASS_LINE);
		} else {
			tr.setClass(CSS_CLASS_LINE, CSS_CLASS_LINE_HIDE);
		}
		int num = tl.getRow() + 1;
		tr.newTd(TypeTools.format(num)).setClass(CSS_CLASS_LINE_NUM);

		HtmlFlow td = (HtmlFlow) tr.newTd().setClass(CSS_CLASS_LINE_TEXT);

		TokenString ts = tl.getTokenString();
		int n = ts.getNumRegions();
		int bestMatchIndex = Integer.MAX_VALUE;
		for (int i = 0; i < n; i++) {
			String s = ts.getRegionString(i);
			s = HtmlTools.sanitizeText(s);

			Object t = ts.getRegionToken(i);
			MatchToken.Type type = getMatchType(t, i);
			if (type == MatchToken.Type.LINE_MATCH) {
				int matchIndex = getMatchIndex(t);
				bestMatchIndex = Math.min(bestMatchIndex, matchIndex);
				td.newSpan(s).setClass(getClassName(matchIndex));
			} else {
				td.addText(s);
			}
		}

		if (bestMatchIndex != Integer.MAX_VALUE) {
			tr.addClass(getClassName(bestMatchIndex));
		}
		return tr;
	}

	/** Returns LINE_MATCH, LINE_WINDOW or null */
	MatchToken.Type getMatchType(Object token, int index) {
		if (isMatchTokenMap(token)) {
			MatchToken.Type type = null;
			@SuppressWarnings("unchecked")
			Map tokens = (Map) token;
			if (tokens != null) {
				for (MatchToken mt : tokens.values()) {
					if (mt.getType().isMatch()) {
						return MatchToken.Type.LINE_MATCH;
					} else {
						type = MatchToken.Type.LINE_WINDOW;
					}
				}
			}
			return type;
		} else {
			return (token != null) ? MatchToken.Type.LINE_MATCH : null;
		}
	}

	boolean isMatchTokenMap(Object token) {
		if (token instanceof Map) {
			@SuppressWarnings("rawtypes")
			Map map = (Map) token;
			return CollectionTools.getFirstValue(map) instanceof MatchToken;
		}
		return false;
	}

	String getClassName(int index) {
		return "sel" + index;
	}

	boolean isMatch(Object elem) {
		return elem != null;
	}

	int getMatchIndex(Object elem) {
		return indexer.get(elem);
	}

}