/*=============================================================================#
# 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.tex.internal.core.model;
import static de.walware.docmlet.tex.core.model.ITexSourceElement.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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.walware.jcommons.collections.ImCollections;
import de.walware.jcommons.collections.ImList;
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.tex.core.ast.ControlNode;
import de.walware.docmlet.tex.core.ast.Embedded;
import de.walware.docmlet.tex.core.ast.Environment;
import de.walware.docmlet.tex.core.ast.SourceComponent;
import de.walware.docmlet.tex.core.ast.TexAst;
import de.walware.docmlet.tex.core.ast.TexAst.NodeType;
import de.walware.docmlet.tex.core.ast.TexAstNode;
import de.walware.docmlet.tex.core.ast.TexAstVisitor;
import de.walware.docmlet.tex.core.ast.Text;
import de.walware.docmlet.tex.core.commands.IPreambleDefinitions;
import de.walware.docmlet.tex.core.commands.LtxPrintCommand;
import de.walware.docmlet.tex.core.commands.TexCommand;
import de.walware.docmlet.tex.core.model.EmbeddingReconcileItem;
import de.walware.docmlet.tex.core.model.ITexSourceElement;
import de.walware.docmlet.tex.core.model.ITexSourceUnit;
import de.walware.docmlet.tex.core.model.TexElementName;
import de.walware.docmlet.tex.core.model.TexNameAccess;
import de.walware.docmlet.tex.internal.core.model.LtxSourceElement.EmbeddedRef;
public class SourceAnalyzer extends TexAstVisitor {
private static final Integer ONE= 1;
private String input;
private LtxSourceElement.Container currentElement;
private final StringBuilder titleBuilder= new StringBuilder();
private boolean titleDoBuild;
private LtxSourceElement.Container titleElement;
private final Map<String, Integer> structNamesCounter= new HashMap<>();
private Map<String, NameAccessAccumulator<TexNameAccess>> labels= new HashMap<>();
private final List<EmbeddingReconcileItem> embeddedItems= new ArrayList<>();
private int minSectionLevel;
private int maxSectionLevel;
public void clear() {
this.input= null;
this.currentElement= null;
this.titleBuilder.setLength(0);
this.titleDoBuild= false;
this.titleElement= null;
if (this.labels == null || !this.labels.isEmpty()) {
this.labels= new HashMap<>();
}
this.embeddedItems.clear();
this.minSectionLevel= Integer.MAX_VALUE;
this.maxSectionLevel= Integer.MIN_VALUE;
}
public LtxSourceUnitModelInfo createModel(final ITexSourceUnit su, final String input,
final AstInfo ast,
Map<String, TexCommand> customCommands, Map<String, TexCommand> customEnvs) {
clear();
this.input= input;
if (!(ast.root instanceof TexAstNode)) {
return null;
}
final ITexSourceElement root= this.currentElement= new LtxSourceElement.SourceContainer(
ITexSourceElement.C2_SOURCE_FILE, su, (TexAstNode) ast.root);
try {
((TexAstNode) ast.root).acceptInTex(this);
final INameAccessSet<TexNameAccess> labels;
if (this.labels.isEmpty()) {
labels= NameAccessSet.emptySet();
}
else {
labels= new NameAccessSet<>(this.labels);
this.labels= null;
}
if (this.minSectionLevel == Integer.MAX_VALUE) {
this.minSectionLevel= 0;
this.maxSectionLevel= 0;
}
if (customCommands != null) {
customCommands= Collections.unmodifiableMap(customCommands);
}
else {
customCommands= Collections.emptyMap();
}
if (customEnvs != null) {
customEnvs= Collections.unmodifiableMap(customEnvs);
}
else {
customEnvs= Collections.emptyMap();
}
final LtxSourceUnitModelInfo model= new LtxSourceUnitModelInfo(ast, root,
this.minSectionLevel, this.maxSectionLevel, labels, customCommands, customEnvs );
return model;
}
catch (final InvocationTargetException e) {
throw new IllegalStateException();
}
}
public List<EmbeddingReconcileItem> getEmbeddedItems() {
return this.embeddedItems;
}
private void initElement(final LtxSourceElement.Container element) {
if (this.currentElement.fChildren.isEmpty()) {
this.currentElement.fChildren= new ArrayList<>();
}
this.currentElement.fChildren.add(element);
this.currentElement= element;
}
private void exitContainer(final int stop, final boolean forward) {
this.currentElement.fLength= ((forward) ?
readLinebreakForward((stop >= 0) ? stop : this.currentElement.fOffset + this.currentElement.fLength, this.input.length()) :
readLinebreakBackward((stop >= 0) ? stop : this.currentElement.fOffset + this.currentElement.fLength, 0) ) -
this.currentElement.fOffset;
final List<LtxSourceElement> children= this.currentElement.fChildren;
if (!children.isEmpty()) {
for (final LtxSourceElement 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.fOccurrenceCount= occ + 1) ));
}
}
}
this.structNamesCounter.clear();
}
this.currentElement= this.currentElement.getModelParent();
}
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 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.fName= TexElementName.create(TexElementName.TITLE, this.titleBuilder.toString());
this.titleBuilder.setLength(0);
this.titleElement= null;
this.titleDoBuild= false;
}
@Override
public void visit(final SourceComponent node) throws InvocationTargetException {
this.currentElement.fOffset= node.getOffset();
node.acceptInTexChildren(this);
if (this.titleElement != null) {
finishTitleText();
}
while ((this.currentElement.getElementType() & MASK_C1) != ITexSourceElement.C1_SOURCE) {
exitContainer(node.getEndOffset(), true);
}
exitContainer(node.getEndOffset(), true);
}
@Override
public void visit(final Environment node) throws InvocationTargetException {
final TexCommand command= node.getBeginNode().getCommand();
if ((command.getType() & TexCommand.MASK_C2) == TexCommand.C2_ENV_DOCUMENT_BEGIN) {
if (this.titleElement != null) {
finishTitleText();
}
while ((this.currentElement.getElementType() & MASK_C1) != ITexSourceElement.C1_SOURCE) {
exitContainer(node.getOffset(), false);
}
}
node.acceptInTexChildren(this);
if ((command.getType() & TexCommand.MASK_C2) == TexCommand.C2_ENV_DOCUMENT_BEGIN) {
if (this.titleElement != null) {
finishTitleText();
}
while ((this.currentElement.getElementType() & MASK_C1) != ITexSourceElement.C1_SOURCE) {
exitContainer((node.getEndNode() != null) ?
node.getEndNode().getOffset() : node.getEndOffset(), false );
}
}
{ final TexAstNode beginLabel= getLabelNode(node.getBeginNode());
if (beginLabel != null) {
final ImList<EnvLabelAccess> accessList;
final TexAstNode endLabel= getLabelNode(node.getEndNode());
if (endLabel != null) {
accessList= ImCollections.newList(
new EnvLabelAccess(node.getBeginNode(), beginLabel),
new EnvLabelAccess(node.getEndNode(), endLabel) );
}
else {
accessList= ImCollections.newList(
new EnvLabelAccess(node.getBeginNode(), endLabel) );
}
for (final EnvLabelAccess access : accessList) {
access.all= accessList;
access.getNode().addAttachment(access);
}
}
}
}
@Override
public void visit(final ControlNode node) throws InvocationTargetException {
final TexCommand command= node.getCommand();
COMMAND: if (command != null) {
switch (command.getType() & TexCommand.MASK_MAIN) {
case TexCommand.PREAMBLE:
if (command == IPreambleDefinitions.PREAMBLE_documentclass_COMMAND) {
if (this.titleElement != null) {
finishTitleText();
}
while ((this.currentElement.getElementType() & MASK_C1) != ITexSourceElement.C1_SOURCE) {
exitContainer(node.getOffset(), false);
}
initElement(new LtxSourceElement.StructContainer(
ITexSourceElement.C2_PREAMBLE, this.currentElement, node ));
this.currentElement.fName= TexElementName.create(TexElementName.TITLE, "Preamble");
}
break;
case TexCommand.SECTIONING:
if ((this.currentElement.getElementType() & MASK_C2) == ITexSourceElement.C2_PREAMBLE) {
exitContainer(node.getOffset(), false);
}
if ((this.currentElement.getElementType() & MASK_C2) == ITexSourceElement.C2_SECTIONING
|| (this.currentElement.getElementType() & MASK_C1) == ITexSourceElement.C1_SOURCE ) {
final int level= (command.getType() & 0xf0) >> 4;
if (level > 5) {
break COMMAND;
}
if (this.titleElement != null) {
finishTitleText();
break COMMAND;
}
while ((this.currentElement.getElementType() & MASK_C2) == ITexSourceElement.C2_SECTIONING
&& (this.currentElement.getElementType() & 0xf) >= level) {
exitContainer(node.getOffset(), false);
}
initElement(new LtxSourceElement.StructContainer(
ITexSourceElement.C2_SECTIONING | level, this.currentElement, node ));
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 TexAstNode titleNode= node.getChild(0);
this.titleElement.nameRegion= TexAst.getInnerRegion(titleNode);
node.getChild(0).acceptInTex(this);
if (this.titleElement != null) {
finishTitleText();
}
for (int i= 1; i < count; i++) {
node.getChild(i).acceptInTex(this);
}
}
else {
this.currentElement.fName= TexElementName.create(TexElementName.TITLE, ""); //$NON-NLS-1$
}
this.currentElement.fLength= Math.max(this.currentElement.fLength, node.getLength());
return;
}
break;
case TexCommand.LABEL:
if ((command.getType() & TexCommand.MASK_C2) == TexCommand.C2_LABEL_REFLABEL) {
final TexAstNode nameNode= getLabelNode(node);
if (nameNode != null) {
final String label= nameNode.getText();
NameAccessAccumulator<TexNameAccess> shared= this.labels.get(label);
if (shared == null) {
shared= new NameAccessAccumulator<>(label);
this.labels.put(label, shared);
}
final RefLabelAccess access= new RefLabelAccess(shared, node, nameNode);
if ((command.getType() & TexCommand.MASK_C3) == TexCommand.C3_LABEL_REFLABEL_DEF) {
access.flags |= RefLabelAccess.A_WRITE;
}
node.addAttachment(access);
}
final boolean prevDoBuild= this.titleDoBuild;
this.titleDoBuild= false;
node.acceptInTexChildren(this);
if (prevDoBuild && this.titleElement != null) {
this.titleDoBuild= true;
}
this.currentElement.fLength= node.getEndOffset() - this.currentElement.getOffset();
return;
}
break;
case TexCommand.SYMBOL:
case TexCommand.MATHSYMBOL:
if (command instanceof LtxPrintCommand
&& command.getArguments().isEmpty()
&& this.titleDoBuild) {
final String text= ((LtxPrintCommand) command).getText();
if (text != null) {
if (text.length() == 1 && Character.getType(text.charAt(0)) == Character.NON_SPACING_MARK) {
final int size= this.titleBuilder.length();
node.acceptInTexChildren(this);
if (this.titleElement != null && this.titleBuilder.length() == size + 1) {
this.titleBuilder.append(text);
}
this.currentElement.fLength= node.getEndOffset() - this.currentElement.getOffset();
return;
}
this.titleBuilder.append(text);
}
}
break;
}
}
node.acceptInTexChildren(this);
this.currentElement.fLength= node.getEndOffset() - this.currentElement.getOffset();
}
@Override
public void visit(final Text node) throws InvocationTargetException {
if (this.titleDoBuild) {
this.titleBuilder.append(this.input, node.getOffset(), node.getEndOffset());
if (this.titleBuilder.length() >= 100) {
finishTitleText();
}
}
this.currentElement.fLength= node.getEndOffset() - this.currentElement.getOffset();
}
@Override
public void visit(final Embedded node) throws InvocationTargetException {
if ((node.getEmbedDescr() & 0xf) == 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.fChildren.isEmpty()) {
this.currentElement.fChildren= new ArrayList<>();
}
final EmbeddedRef element= new LtxSourceElement.EmbeddedRef(node.getText(),
this.currentElement, node );
element.fOffset= node.getOffset();
element.fLength= node.getLength();
element.fName= TexElementName.create(0, ""); //$NON-NLS-1$
this.currentElement.fChildren.add(element);
this.embeddedItems.add(new EmbeddingReconcileItem(node, element));
}
this.currentElement.fLength= node.getEndOffset() - this.currentElement.getOffset();
}
private TexAstNode getLabelNode(TexAstNode node) {
if (node != null && node.getNodeType() == NodeType.CONTROL && node.getChildCount() > 0) {
node= node.getChild(0);
if (node.getNodeType() == NodeType.LABEL) {
return node;
}
if (node.getNodeType() == NodeType.GROUP && node.getChildCount() > 0) {
node= node.getChild(0);
if (node.getNodeType() == NodeType.LABEL) {
return node;
}
}
}
return null;
}
}