/*=============================================================================#
# Copyright (c) 2009-2016 Stephan Wahlbrink (WalWare.de) 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:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.docmlet.wikitext.internal.core.model;
import static de.walware.docmlet.wikitext.core.model.IWikitextElement.C2_SECTIONING;
import static de.walware.ecommons.ltk.core.model.IModelElement.MASK_C1;
import static de.walware.ecommons.ltk.core.model.IModelElement.MASK_C2;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.text.Region;
import de.walware.ecommons.ltk.AstInfo;
import de.walware.ecommons.ltk.core.impl.NameAccessAccumulator;
import de.walware.ecommons.ltk.core.impl.NameAccessSet;
import de.walware.ecommons.ltk.core.model.INameAccessSet;
import de.walware.docmlet.wikitext.core.ast.Block;
import de.walware.docmlet.wikitext.core.ast.Embedded;
import de.walware.docmlet.wikitext.core.ast.Heading;
import de.walware.docmlet.wikitext.core.ast.Image;
import de.walware.docmlet.wikitext.core.ast.Label;
import de.walware.docmlet.wikitext.core.ast.Link;
import de.walware.docmlet.wikitext.core.ast.SourceComponent;
import de.walware.docmlet.wikitext.core.ast.Span;
import de.walware.docmlet.wikitext.core.ast.Text;
import de.walware.docmlet.wikitext.core.ast.WikitextAstNode;
import de.walware.docmlet.wikitext.core.ast.WikitextAstVisitor;
import de.walware.docmlet.wikitext.core.model.EmbeddingReconcileItem;
import de.walware.docmlet.wikitext.core.model.IWikitextSourceElement;
import de.walware.docmlet.wikitext.core.model.IWikitextSourceUnit;
import de.walware.docmlet.wikitext.core.model.WikitextElementName;
import de.walware.docmlet.wikitext.core.model.WikitextNameAccess;
import de.walware.docmlet.wikitext.internal.core.model.WikitextSourceElement.EmbeddedRef;
public class SourceAnalyzer extends WikitextAstVisitor {
private static final Integer ONE= 1;
private String input;
private WikitextSourceElement.Container currentElement;
private final StringBuilder titleBuilder= new StringBuilder();
private boolean titleDoBuild;
private WikitextSourceElement.Container titleElement;
private final Map<String, Integer> structNamesCounter= new HashMap<>();
private final List<EmbeddingReconcileItem> embeddedItems= new ArrayList<>();
private int minSectionLevel;
private int maxSectionLevel;
private Map<String, NameAccessAccumulator<WikitextNameAccess>> linkAnchorLabels= new HashMap<>();
private Map<String, NameAccessAccumulator<WikitextNameAccess>> linkDefLabels= new HashMap<>();
public void clear() {
this.input= null;
this.currentElement= null;
this.titleBuilder.setLength(0);
this.titleDoBuild= false;
this.titleElement= null;
this.embeddedItems.clear();
this.minSectionLevel= Integer.MAX_VALUE;
this.maxSectionLevel= Integer.MIN_VALUE;
}
public WikidocSourceUnitModelInfo createModel(final IWikitextSourceUnit su, final String input,
final AstInfo ast) {
clear();
this.input= input;
if (!(ast.root instanceof WikitextAstNode)) {
return null;
}
if (!this.linkAnchorLabels.isEmpty()) {
this.linkAnchorLabels.clear();
}
if (!this.linkDefLabels.isEmpty()) {
this.linkDefLabels.clear();
}
final IWikitextSourceElement root= this.currentElement= new WikitextSourceElement.SourceContainer(
IWikitextSourceElement.C2_SOURCE_FILE, su, (WikitextAstNode) ast.root);
try {
((WikitextAstNode) ast.root).acceptInWikitext(this);
if (this.minSectionLevel == Integer.MAX_VALUE) {
this.minSectionLevel= 0;
this.maxSectionLevel= 0;
}
final INameAccessSet<WikitextNameAccess> linkAnchorLabels;
final INameAccessSet<WikitextNameAccess> linkDefLabels;
if (this.linkAnchorLabels.isEmpty()) {
linkAnchorLabels= NameAccessSet.emptySet();
}
else {
linkAnchorLabels= new NameAccessSet<>(this.linkAnchorLabels);
this.linkAnchorLabels= new HashMap<>();
}
if (this.linkDefLabels.isEmpty()) {
linkDefLabels= NameAccessSet.emptySet();
}
else {
linkDefLabels= new NameAccessSet<>(this.linkDefLabels);
this.linkDefLabels= new HashMap<>();
}
final WikidocSourceUnitModelInfo model= new WikidocSourceUnitModelInfo(ast, root,
linkAnchorLabels, linkDefLabels,
this.minSectionLevel, this.maxSectionLevel );
return model;
}
catch (final InvocationTargetException e) {
throw new IllegalStateException();
}
}
public List<EmbeddingReconcileItem> getEmbeddedItems() {
return this.embeddedItems;
}
private void initElement(final WikitextSourceElement.Container element) {
if (this.currentElement.children.isEmpty()) {
this.currentElement.children= new ArrayList<>();
}
this.currentElement.children.add(element);
this.currentElement= element;
}
private void exitContainer(final int stop, final boolean forward) {
this.currentElement.length= ((forward) ?
readLinebreakForward((stop >= 0) ? stop : this.currentElement.offset + this.currentElement.length, this.input.length()) :
readLinebreakBackward((stop >= 0) ? stop : this.currentElement.offset + this.currentElement.length, 0) ) -
this.currentElement.offset;
final List<WikitextSourceElement> children= this.currentElement.children;
if (!children.isEmpty()) {
for (final WikitextSourceElement element : children) {
if ((element.getElementType() & MASK_C2) == C2_SECTIONING) {
final Map<String, Integer> names= this.structNamesCounter;
final String name= element.getElementName().getDisplayName();
final Integer occ= names.get(name);
if (occ == null) {
names.put(name, ONE);
}
else {
names.put(name, Integer.valueOf(
(element.occurrenceCount= occ + 1) ));
}
}
}
this.structNamesCounter.clear();
}
this.currentElement= this.currentElement.getModelParent();
}
private void finishTitleText() {
{ boolean wasWhitespace= false;
int idx= 0;
while (idx < this.titleBuilder.length()) {
if (this.titleBuilder.charAt(idx) == ' ') {
if (wasWhitespace) {
this.titleBuilder.deleteCharAt(idx);
}
else {
wasWhitespace= true;
idx++;
}
}
else {
wasWhitespace= false;
idx++;
}
}
}
this.titleElement.name= WikitextElementName.create(WikitextElementName.TITLE, this.titleBuilder.toString());
this.titleBuilder.setLength(0);
this.titleElement= null;
this.titleDoBuild= false;
}
private int readLinebreakForward(int offset, final int limit) {
if (offset < limit) {
switch(this.input.charAt(offset)) {
case '\n':
if (++offset < limit && this.input.charAt(offset) == '\r') {
return ++offset;
}
return offset;
case '\r':
if (++offset < limit && this.input.charAt(offset) == '\n') {
return ++offset;
}
return offset;
}
}
return offset;
}
private int readLinebreakBackward(int offset, final int limit) {
if (offset > limit) {
switch(this.input.charAt(offset-1)) {
case '\n':
if (--offset > limit && this.input.charAt(offset-1) == '\r') {
return --offset;
}
return offset;
case '\r':
if (--offset < limit && this.input.charAt(offset-1) == '\n') {
return --offset;
}
return offset;
}
}
return offset;
}
private RefLabelAccess addLinkAnchorAccess(final WikitextAstNode node) {
final String label= node.getLabel();
NameAccessAccumulator<WikitextNameAccess> shared= this.linkAnchorLabels.get(label);
if (shared == null) {
shared= new NameAccessAccumulator<>(label);
this.linkAnchorLabels.put(label, shared);
}
final RefLabelAccess access= new RefLabelAccess.LinkAnchor(shared, node, null);
node.addAttachment(access);
return access;
}
private RefLabelAccess addLinkDefAccess(final WikitextAstNode node, final Label labelNode) {
final String label= labelNode.getText();
NameAccessAccumulator<WikitextNameAccess> shared= this.linkDefLabels.get(label);
if (shared == null) {
shared= new NameAccessAccumulator<>(label);
this.linkDefLabels.put(label, shared);
}
final RefLabelAccess access= new RefLabelAccess.LinkDef(shared, node, labelNode);
node.addAttachment(access);
return access;
}
@Override
public void visit(final SourceComponent node) throws InvocationTargetException {
this.currentElement.offset= node.getOffset();
node.acceptInWikitextChildren(this);
if (this.titleElement != null) {
finishTitleText();
}
while ((this.currentElement.getElementType() & MASK_C1) != IWikitextSourceElement.C1_SOURCE) {
exitContainer(node.getEndOffset(), true);
}
exitContainer(node.getEndOffset(), true);
}
@Override
public void visit(final Block node) throws InvocationTargetException {
if (node.getLabel() != null) {
final RefLabelAccess access= addLinkAnchorAccess(node);
access.flags|= RefLabelAccess.A_WRITE;
}
node.acceptInWikitextChildren(this);
this.currentElement.length= node.getEndOffset() - this.currentElement.getOffset();
}
@Override
public void visit(final Heading node) throws InvocationTargetException {
if (node.getLabel() != null) {
final RefLabelAccess access= addLinkAnchorAccess(node);
access.flags|= RefLabelAccess.A_WRITE;
}
COMMAND: {
if ((this.currentElement.getElementType() & MASK_C2) == IWikitextSourceElement.C2_SECTIONING
|| (this.currentElement.getElementType() & MASK_C1) == IWikitextSourceElement.C1_SOURCE ) {
final int level= node.getLevel();
if (level > 5) {
break COMMAND;
}
if (this.titleElement != null) {
finishTitleText();
break COMMAND;
}
while ((this.currentElement.getElementType() & MASK_C2) == IWikitextSourceElement.C2_SECTIONING
&& (this.currentElement.getElementType() & 0xf) >= level) {
exitContainer(node.getOffset(), false);
}
initElement(new WikitextSourceElement.StructContainer(
IWikitextSourceElement.C2_SECTIONING | level, this.currentElement, node ));
node.addAttachment(this.currentElement);
this.minSectionLevel= Math.min(this.minSectionLevel, level);
this.maxSectionLevel= Math.max(this.maxSectionLevel, level);
final int count= node.getChildCount();
if (count > 0) {
this.titleElement= this.currentElement;
this.titleDoBuild= true;
final int nameOffset= node.getChild(0).getOffset();
final int nameStopOffset= readLinebreakBackward(node.getChild(count - 1).getEndOffset(), nameOffset);
this.titleElement.nameRegion= new Region(nameOffset, nameStopOffset - nameOffset);
node.acceptInWikitextChildren(this);
if (this.titleElement != null) {
finishTitleText();
}
}
else {
this.currentElement.name= WikitextElementName.create(WikitextElementName.TITLE, ""); //$NON-NLS-1$
this.currentElement.nameRegion= new Region(node.getOffset(), 0);
}
this.currentElement.length= Math.max(this.currentElement.length, node.getLength());
return;
}
}
node.acceptInWikitextChildren(this);
this.currentElement.length= node.getEndOffset() - this.currentElement.getOffset();
}
@Override
public void visit(final Span node) throws InvocationTargetException {
if (node.getLabel() != null) {
final RefLabelAccess access= addLinkAnchorAccess(node);
access.flags|= RefLabelAccess.A_WRITE;
}
node.acceptInWikitextChildren(this);
}
@Override
public void visit(final Text node) throws InvocationTargetException {
if (this.titleDoBuild) {
final String text= node.getText();
if (text != null) {
this.titleBuilder.append(text);
if (this.titleBuilder.length() >= 100) {
finishTitleText();
}
}
}
this.currentElement.length= node.getEndOffset() - this.currentElement.getOffset();
}
@Override
public void visit(final Link node) throws InvocationTargetException {
final RefLabelAccess access;
switch (node.getLinkType()) {
case Link.LINK_REF_DEFINITION:
access= addLinkDefAccess(node, node.getReferenceLabel());
access.flags|= RefLabelAccess.A_WRITE;
break;
case Link.LINK_BY_REF:
access= addLinkDefAccess(node, node.getReferenceLabel());
break;
default:
break;
}
super.visit(node);
}
@Override
public void visit(final Image node) throws InvocationTargetException {
final RefLabelAccess access;
switch (node.getImageType()) {
case Image.SRC_BY_REF:
access= addLinkDefAccess(node, node.getReferenceLabel());
break;
default:
break;
}
super.visit(node);
}
@Override
public void visit(final Embedded node) throws InvocationTargetException {
if (node.getForeignNode() == null) {
super.visit(node);
return;
}
if ((node.getEmbedDescr() & 0b0_00000011) == Embedded.EMBED_INLINE) {
if (this.titleDoBuild) {
this.titleBuilder.append(this.input, node.getOffset(), node.getEndOffset());
if (this.titleBuilder.length() >= 100) {
finishTitleText();
}
}
this.embeddedItems.add(new EmbeddingReconcileItem(node, null));
}
else {
if (this.titleElement != null) {
finishTitleText();
}
if (this.currentElement.children.isEmpty()) {
this.currentElement.children= new ArrayList<>();
}
final EmbeddedRef element= new WikitextSourceElement.EmbeddedRef(node.getText(),
this.currentElement, node );
element.offset= node.getOffset();
element.length= node.getLength();
element.name= WikitextElementName.create(0, ""); //$NON-NLS-1$
this.currentElement.children.add(element);
this.embeddedItems.add(new EmbeddingReconcileItem(node, element));
}
this.currentElement.length= node.getEndOffset() - this.currentElement.getOffset();
}
}