/******************************************************************************* * Copyright (c) 2007, 2017 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 *******************************************************************************/ package org.eclipse.mylyn.wikitext.confluence.internal.block; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.mylyn.wikitext.parser.Attributes; import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.BlockType; import org.eclipse.mylyn.wikitext.parser.markup.Block; /** * List block, matches blocks that start with <code>*</code>, <code>#</code> or <code>-</code> * * @author David Green */ public class ListBlock extends Block { private static final int LINE_REMAINDER_GROUP_OFFSET = 2; private static final Pattern LIST_PATTERN = Pattern.compile("\\s*((?:(?:\\*)|(?:#)|(?:-))+)\\s(.*+)"); //$NON-NLS-1$ private int blockLineCount = 0; private Matcher matcher; private Stack<ListState> listState; public ListBlock() { } @Override public int processLineContent(String line, int offset) { boolean continuation = false; if (blockLineCount == 0) { listState = new Stack<ListState>(); Attributes attributes = new Attributes(); String listSpec = matcher.group(1); int level = calculateLevel(listSpec); BlockType type = calculateType(listSpec); if (type == BlockType.BULLETED_LIST && "-".equals(listSpec)) { //$NON-NLS-1$ attributes.setCssStyle("list-style: square"); //$NON-NLS-1$ } // 0-offset matches may start with the "*** " prefix. offset = matcher.start(LINE_REMAINDER_GROUP_OFFSET); listState.push(new ListState(1, type)); builder.beginBlock(type, attributes); adjustLevel(listSpec, level, type); } else { Matcher matcher = LIST_PATTERN.matcher(line); if (!matcher.matches()) { boolean empty = offset == 0 && markupLanguage.isEmptyLine(line); boolean breaking = ParagraphBlock.paragraphBreakingBlockMatches(getMarkupLanguage(), line, offset); if (empty || breaking) { setClosed(true); return 0; } else { continuation = true; } } else { String listSpec = matcher.group(1); int level = calculateLevel(listSpec); BlockType type = calculateType(listSpec); offset = matcher.start(LINE_REMAINDER_GROUP_OFFSET); adjustLevel(listSpec, level, type); } } ++blockLineCount; ListState listState = this.listState.peek(); if (listState.openItem) { if (continuation) { builder.lineBreak(); } else { builder.endBlock(); listState.openItem = false; } } if (!listState.openItem) { listState.openItem = true; builder.beginBlock(BlockType.LIST_ITEM, new Attributes()); } markupLanguage.emitMarkupLine(getParser(), state, line, offset); return -1; } private void adjustLevel(String listSpec, int level, BlockType type) { for (ListState previousState = listState.peek(); level != previousState.level || previousState.type != type; previousState = listState.peek()) { if (level > previousState.level) { if (!previousState.openItem) { builder.beginBlock(BlockType.LIST_ITEM, new Attributes()); previousState.openItem = true; } Attributes blockAttributes = new Attributes(); if (type == BlockType.BULLETED_LIST && "-".equals(listSpec)) { //$NON-NLS-1$ blockAttributes.setCssStyle("list-style: square"); //$NON-NLS-1$ } listState.push(new ListState(previousState.level + 1, type)); builder.beginBlock(type, blockAttributes); } else { closeOne(); if (listState.isEmpty()) { Attributes blockAttributes = new Attributes(); if (type == BlockType.BULLETED_LIST && "-".equals(listSpec)) { //$NON-NLS-1$ blockAttributes.setCssStyle("list-style: square"); //$NON-NLS-1$ } listState.push(new ListState(1, type)); builder.beginBlock(type, blockAttributes); } } } } private int calculateLevel(String listSpec) { return listSpec.length(); } private BlockType calculateType(String listSpec) { return listSpec.charAt(listSpec.length() - 1) == '#' ? BlockType.NUMERIC_LIST : BlockType.BULLETED_LIST; } @Override public boolean canStart(String line, int lineOffset) { blockLineCount = 0; listState = null; matcher = LIST_PATTERN.matcher(line); matcher.region(lineOffset, line.length()); boolean matches = matcher.matches(); if (matches) { String listSpec = matcher.group(1); if (listSpec.charAt(0) == '-') { int level = calculateLevel(listSpec); if (level > 1) { // don't match hr, emdash, endash etc. // list block must start at level 1s return false; } } } return matches; } @Override public void setClosed(boolean closed) { if (closed && !isClosed()) { while (listState != null && !listState.isEmpty()) { closeOne(); } listState = null; } super.setClosed(closed); } private void closeOne() { ListState e = listState.pop(); if (e.openItem) { builder.endBlock(); } builder.endBlock(); } private static class ListState { int level; BlockType type; boolean openItem; private ListState(int level, BlockType type) { super(); this.level = level; this.type = type; } } }