/*=============================================================================#
# Copyright (c) 2015-2016 David Green and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
# David Green - initial API and implementation in Mylyn
# Stephan Wahlbrink (WalWare.de) - revised API and implementation
#=============================================================================*/
package de.walware.docmlet.wikitext.internal.commonmark.core.blocks;
import static com.google.common.base.Preconditions.checkState;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.mylyn.wikitext.core.parser.Attributes;
import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder;
import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.BlockType;
import org.eclipse.mylyn.wikitext.core.parser.ListAttributes;
import de.walware.jcommons.collections.ImCollections;
import de.walware.docmlet.wikitext.internal.commonmark.core.CommonmarkLocator;
import de.walware.docmlet.wikitext.internal.commonmark.core.FilterLineSequence;
import de.walware.docmlet.wikitext.internal.commonmark.core.Line;
import de.walware.docmlet.wikitext.internal.commonmark.core.LineSequence;
import de.walware.docmlet.wikitext.internal.commonmark.core.ProcessingContext;
import de.walware.docmlet.wikitext.internal.commonmark.core.SourceBlockItem;
import de.walware.docmlet.wikitext.internal.commonmark.core.SourceBlocks.SourceBlockBuilder;
public class ListBlock extends BlockWithNestedBlocks {
public static final Pattern PATTERN= Pattern.compile(
"([*+-]|([0-9]{1,9})[.)])(?:([ \t]+)(.+)?)?",
Pattern.DOTALL );
public static int computeItemLineIndent(final Line line, final Matcher matcher) {
final int markerEndColumn= line.getColumn(matcher.end(1));
if (matcher.start(3) != -1 && matcher.start(4) != -1) {
final int contentStartColumn= line.getColumn(matcher.start(4));
if (contentStartColumn - markerEndColumn <= 4) {
return contentStartColumn - line.getColumn();
}
}
return markerEndColumn + 1 - line.getColumn();
}
private static enum ListMode {
TIGHT, LOOSE, TIGHT_WITH_TRAILING_EMPTY_LINE
}
private static final class ListBlockItem extends SourceBlockItem<ListBlock> {
private char bulletType;
private String listStart;
public ListBlockItem(final ListBlock type, final SourceBlockBuilder builder) {
super(type, builder);
}
}
private static class ListLines extends FilterLineSequence {
private final SourceBlockBuilder builder;
private final ListItemBlock listItemBlock;
public ListLines(final LineSequence delegate, final SourceBlockBuilder builder,
final ListItemBlock blockItem) {
super(delegate);
this.builder= builder;
this.listItemBlock= blockItem;
}
public ListLines(final ListLines from) {
super(from.getDelegate().lookAhead());
this.builder= from.builder;
this.listItemBlock= from.listItemBlock;
}
@Override
public LineSequence lookAhead() {
return new ListLines(this);
}
@Override
protected Line filter(final Line line) {
if (!line.isBlank()) {
if (line.getIndent() >= this.listItemBlock.itemIdent
|| this.listItemBlock.canStart(line) ) {
return line;
}
if (isLazyContinuation(line)) {
return line.lazy();
}
}
else {
if (lookAheadSafeLine(getDelegate().lookAhead(line.getLineNumber())) != Integer.MIN_VALUE) {
return line;
}
}
return null;
}
private boolean isLazyContinuation(final Line line) {
final SourceBlockItem<?> currentItem= this.builder.getCurrentItem();
if (currentItem.getParent() != this.listItemBlock.listBlockItem
&& currentItem.isParagraph()) {
if (!(this.listItemBlock.canStart(line)
|| ((ParagraphBlock) currentItem.getType()).isAnotherBlockStart(
getDelegate().lookAhead(line.getLineNumber()), this.builder.getSourceBlocks(), currentItem ))) {
return true;
}
}
return false;
}
private int lookAheadSafeLine(final LineSequence lineSequence) {
while (true) {
final Line line= lineSequence.getCurrentLine();
if (line != null) {
if (line.isBlank()) {
lineSequence.advance();
continue;
}
if (line.getIndent() >= this.listItemBlock.itemIdent
|| this.listItemBlock.canStart(line) ) {
return line.getLineNumber();
}
}
return Integer.MIN_VALUE;
}
}
}
private static class ListItemBlock extends BlockWithNestedBlocks {
private final ListBlockItem listBlockItem;
private int itemIdent= 4;
public ListItemBlock(final ListBlockItem listBlockItem) {
this.listBlockItem= listBlockItem;
}
@Override
public boolean canStart(final LineSequence lineSequence, final SourceBlockItem<?> currentBlockItem) {
return canStart(lineSequence.getCurrentLine());
}
public boolean canStart(final Line startLine) {
if (startLine != null
&& !startLine.isBlank() && startLine.getIndent() < this.itemIdent) {
final ListBlock listBlock= this.listBlockItem.getType();
final Matcher matcher;
return ((matcher= startLine.setupIndent(listBlock.matcher)).matches()
&& (listBlock.bulletType(startLine, matcher) == this.listBlockItem.bulletType)
&& !listBlock.thematicBreakBlock.canStart(startLine) );
}
return false;
}
@Override
public void createItem(final SourceBlockBuilder builder, final LineSequence lineSequence) {
final SourceBlockItem<ListItemBlock> blockItem= new SourceBlockItem<>(this, builder);
final Line startLine= lineSequence.getCurrentLine();
final ListBlock listBlock= this.listBlockItem.getType();
this.itemIdent= computeItemLineIndent(startLine, listBlock.matcher);
final ListItemLines itemLineSequence= new ListItemLines(lineSequence, builder, blockItem,
startLine.getLineNumber(), this.itemIdent );
builder.createNestedItems(itemLineSequence, null);
}
@Override
public void emit(final ProcessingContext context, final SourceBlockItem<?> blockItem,
final CommonmarkLocator locator, final DocumentBuilder builder) {
throw new UnsupportedOperationException();
}
public void emit(final ProcessingContext context, final SourceBlockItem<?> blockItem,
final ListBlock.ListMode listMode,
final CommonmarkLocator locator, final DocumentBuilder builder) {
locator.setBlockBegin(blockItem);
builder.beginBlock(BlockType.LIST_ITEM, new Attributes());
for (final SourceBlockItem<?> contentBlockItem : blockItem.getNested()) {
if (listMode == ListBlock.ListMode.TIGHT
&& contentBlockItem.isParagraph()) {
((ParagraphBlock) contentBlockItem.getType())
.emit(context, contentBlockItem, false, locator, builder);
}
else {
contentBlockItem.getType()
.emit(context, contentBlockItem, locator, builder);
}
}
locator.setBlockEnd(blockItem);
builder.endBlock();
}
}
private static class ListItemLines extends FilterLineSequence {
private final SourceBlockBuilder builder;
private final SourceBlockItem<ListItemBlock> blockItem;
private final int markerLineNumber;
private final int indent;
public ListItemLines(final LineSequence delegate, final SourceBlockBuilder builder,
final SourceBlockItem<ListItemBlock> blockItem,
final int markerLineNumber, final int indent) {
super(delegate);
this.builder= builder;
this.blockItem= blockItem;
this.markerLineNumber= markerLineNumber;
this.indent= indent;
}
protected ListItemLines(final ListItemLines from) {
super(from.getDelegate().lookAhead());
this.builder= from.builder;
this.blockItem= from.blockItem;
this.markerLineNumber= from.markerLineNumber;
this.indent= from.indent;
}
@Override
public ListItemLines lookAhead() {
return new ListItemLines(this);
}
@Override
protected Line filter(final Line line) {
final List<SourceBlockItem<?>> nestedItems= this.blockItem.getNested();
if (nestedItems.size() == 1
&& nestedItems.get(0).isEmpty()
&& line.getLineNumber() > this.markerLineNumber + 1) {
return null;
}
// validity already checked in ListLines
if (line.isLazy()) {
return line;
}
if (line.getLineNumber() == this.markerLineNumber
|| line.isBlank()
|| line.getIndent() >= this.indent ) {
return line.segmentByIndent(this.indent);
}
return null;
}
}
private final Matcher matcher= PATTERN.matcher("");
private final ThematicBreakBlock thematicBreakBlock= new ThematicBreakBlock();
@Override
public boolean canStart(final LineSequence lineSequence,
final SourceBlockItem<?> currentBlockItem) {
final Line currentLine= lineSequence.getCurrentLine();
final Matcher matcher;
return (currentLine != null
&& !currentLine.isBlank() && currentLine.getIndent() < 4
&& (matcher= currentLine.setupIndent(this.matcher)).matches()
&& (currentBlockItem == null || canInterrupt(currentLine, matcher)) );
}
private boolean canInterrupt(final Line startLine, final Matcher matcher) {
return (listStart(startLine, matcher) == null
&& matcher.start(4) != -1 );
}
@Override
public void createItem(final SourceBlockBuilder builder, final LineSequence lineSequence) {
final ListBlockItem listBlockItem= new ListBlockItem(this, builder);
final Line startLine= lineSequence.getCurrentLine();
final Matcher matcher= startLine.setupIndent(this.matcher);
checkState(matcher.matches());
listBlockItem.bulletType= bulletType(startLine, matcher);
listBlockItem.listStart= listStart(startLine, matcher);
final ListItemBlock itemBlock= new ListItemBlock(listBlockItem);
builder.createNestedItems(new ListLines(lineSequence, builder, itemBlock),
ImCollections.newList(itemBlock) );
}
@Override
public void emit(final ProcessingContext context, final SourceBlockItem<?> blockItem,
final CommonmarkLocator locator, final DocumentBuilder builder) {
final ListBlockItem listBlockItem= (ListBlockItem) blockItem;
final ListAttributes listAttributes= new ListAttributes();
listAttributes.setStart(listBlockItem.listStart);
final ListMode listMode= (context.getMode() == ProcessingContext.PARSE_SOURCE_STRUCT) ?
ListMode.LOOSE :
calculateListMode(listBlockItem);
locator.setBlockBegin(blockItem);
builder.beginBlock(toBlockType(listBlockItem.bulletType), listAttributes);
for (final SourceBlockItem<?> nestedBlockItem : listBlockItem.getNested()) {
((ListItemBlock) nestedBlockItem.getType()).emit(context, nestedBlockItem, listMode,
locator, builder );
}
locator.setBlockEnd(blockItem);
builder.endBlock();
}
private ListMode calculateListMode(final ListBlockItem listBlockItem) {
ListMode listMode= ListMode.TIGHT;
for (final SourceBlockItem<?> itemBlockItem : listBlockItem.getNested()) {
switch (listMode) {
case LOOSE:
case TIGHT_WITH_TRAILING_EMPTY_LINE:
return ListMode.LOOSE;
case TIGHT:
listMode= getListItemListMode(itemBlockItem);
continue;
}
}
return (listMode == ListMode.TIGHT_WITH_TRAILING_EMPTY_LINE) ? ListMode.TIGHT : listMode;
}
private ListMode getListItemListMode(final SourceBlockItem<?> itemBlockItem) {
final List<SourceBlockItem<?>> contentBlockItems= itemBlockItem.getNested();
if (contentBlockItems.isEmpty()) {
return ListMode.TIGHT;
}
{ final SourceBlockItem<?> block= contentBlockItems.get(0);
if (block.isEmpty() && block.getLines().size() > 1) {
return ListMode.LOOSE;
}
}
for (int idx= 1; idx < contentBlockItems.size() - 1; idx++) {
final SourceBlockItem<?> block= contentBlockItems.get(idx);
if (block.isEmpty()) {
return ListMode.LOOSE;
}
}
if (contentBlockItems.size() > 1) {
final SourceBlockItem<?> block= contentBlockItems.get(contentBlockItems.size() - 1);
if (block.isEmpty()) {
return ListMode.TIGHT_WITH_TRAILING_EMPTY_LINE;
}
}
return ListMode.TIGHT;
}
private char bulletType(final Line line, final Matcher matcher) {
return line.getText().charAt(matcher.end(1) - 1);
}
private String listStart(final Line line, final Matcher matcher) {
String number= matcher.group(2);
if (number != null) {
int startIdx= 0;
while (startIdx < number.length() - 1) {
if (number.charAt(startIdx) == '0') {
startIdx++;
continue;
}
else {
break;
}
}
if (startIdx > 0) {
number= number.substring(startIdx);
}
if (number.equals("1")) {
return null;
}
return number;
}
return null;
}
private BlockType toBlockType(final char bulletType) {
switch (bulletType) {
case '*':
case '+':
case '-':
return BlockType.BULLETED_LIST;
default:
return BlockType.NUMERIC_LIST;
}
}
}