diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocGenerator.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocGenerator.java new file mode 100644 index 000000000..77f5a6779 --- /dev/null +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocGenerator.java @@ -0,0 +1,557 @@ +package org.jnario.maven; + +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtend.core.xtend.XtendClass; +import org.eclipse.xtend.core.xtend.XtendMember; +import org.eclipse.xtend.core.xtend.XtendPackage.Literals; +import org.eclipse.xtend2.lib.StringConcatenation; +import org.eclipse.xtext.xbase.lib.Procedures.Procedure1; +import org.jnario.ExampleTable; +import org.jnario.doc.AbstractDocGenerator; +import org.jnario.doc.Filter; +import org.jnario.doc.FilterExtractor; +import org.jnario.doc.FilteringResult; +import org.jnario.doc.HtmlFile; +import org.jnario.spec.naming.ExampleNameProvider; +import org.jnario.spec.spec.Example; +import org.jnario.spec.spec.ExampleGroup; +import org.jnario.util.Strings; + +import com.google.common.collect.Lists; +import com.google.inject.Inject; + +/** Generator of the HTML files from a Jnario specification + * based on the depth of the examples in the specification. + * + *

JnarioHierarchicalDocGenerate vs. JnarioDocGenerate

+ * + * The HTML generator has a different behavior than the one + * used by {@link JnarioDocGenerate}. Indeed, it create the HTML + * title tags according to the depth of the example in the specification + * file. {@link JnarioDocGenerate} generates the HTML title tags + * according to the semantic/type of the example (description, fact, etc.) + * + *

Generation of an outline

+ * + * This generator generates an outline and put it in the generated + * HTML files in place of: {@code <!-- OUTPUT OUTLINE -->}, + * {@code @outline}. + * + *

HTML Markers

+ * + * Several tags are recognized by this generator and replaced in the + * generated HTML pages: + * + * @author Stéphane Galland - Initial contribution and API + */ +class HierarchicalDocGenerator extends AbstractDocGenerator { + + /** Maximal depth that may be referred in the outline. + */ + protected static final int MAX_DEPTH_REFERENCING = 9; + + /** List of the markes that should be replaced by the outline. + */ + protected static final String[] OUTLINE_MARKERS = new String[] { + Matcher.quoteReplacement(""), //$NON-NLS-1$ + Matcher.quoteReplacement("@outline"), //$NON-NLS-1$ + }; + + /** Separator for the IDs. + */ + protected static final String ID_SEPARATOR = "_"; //$NON-NLS-1$ + + @Inject + private ExampleNameProvider nameProvider; + + @Inject + private FilterExtractor filterExtractor; + + /** + */ + public HierarchicalDocGenerator() { + // + } + + /** {@inheritDoc} + */ + @Override + public HtmlFile createHtmlFile(final XtendClass xtendClass) { + if (!(xtendClass instanceof ExampleGroup)) { + return HtmlFile.EMPTY_FILE; + } + final ExampleGroup exampleGroup = (ExampleGroup) xtendClass; + return HtmlFile.newHtmlFile(new Procedure1() { + @SuppressWarnings("synthetic-access") + @Override + public void apply(HtmlFile it) { + it.setName(HierarchicalDocGenerator.this.nameProvider.toJavaClassName(exampleGroup)); + it.setTitle(asTitle(exampleGroup)); + it.setContent(generateRootContent(exampleGroup)); + it.setRootFolder(root(exampleGroup)); + it.setSourceCode(pre(xtendClass.eContainer(), "lang-spec")); //$NON-NLS-1$ + it.setFileName(fileName(xtendClass)); + it.setExecutionStatus(executionStateClass(exampleGroup)); + } + }); + } + + /** Replies the HTML title of the given object. + * + * @param object - the object. + * @return the HTML title. + */ + protected String asTitle(EObject object) { + return toTitle(this.nameProvider.describe(object)); + } + + /** Replies the HTML code for the code part of a fact. + * + * @param example - the fact. + * @param filters - the filters to apply to the code of the fact. + * @return the HTML code. + */ + protected String toCodeBlock(Example example, List filters) { + String prefix = "
"; //$NON-NLS-1$
+        	prefix = apply(filters, prefix);
+        	String code = serialize(example.getExpression(), filters);
+        	if (code == null || code.isEmpty()) {
+        		return ""; //$NON-NLS-1$
+        	}
+       		return prefix + code + "
\n"; //$NON-NLS-1$ + } + + /** Replies the HTML tag that corresponds to a title at the given level + * for the given object. + * + * @param object - the provider of the title text. + * @param sectionNumber - the section numbering from the root to the current object. + * @return the HTML tag for the title. + */ + protected String makeTitleTag(EObject object, SectionNumber sectionNumber) { + int depth = sectionNumber.getDepth(); + if (depth < MAX_DEPTH_REFERENCING) { + String depthLevel = Integer.toString(depth + 1); + return "" + sectionNumber.toHTML() //$NON-NLS-1$ + + asTitle(object) + + "\n"; //$NON-NLS-1$//$NON-NLS-2$ + } + return "" + asTitle(object) //$NON-NLS-1$ + + "

\n"; //$NON-NLS-1$ + } + + /** Protect the value of an ID. + * This function is similar to {@link #id(String)}, + * except that the prefix id= is not appended. + * + * @param id - the id to protect. + * @return the protected ID. + */ + protected static String protectID(String id) { + if (id != null) { + String p = id.replaceAll("\\W+", ID_SEPARATOR); //$NON-NLS-1$ + if (p != null) { + return Strings.trim(p, ID_SEPARATOR.charAt(0)); + } + } + return ""; //$NON-NLS-1$ + } + + /** Replies the HTML tag that corresponds to a reference to a title. + * + * @param object - the provider of the title text. + * @param sectionNumber - the section numbering from the root to the current object. + * @param text - the text of the reference. + * @return the HTML tag for the title. + */ + protected String makeTitleHref(EObject object, SectionNumber sectionNumber, String text) { + return "" + sectionNumber.toHTML() //$NON-NLS-1$ + + text + ""; //$NON-NLS-1$ + } + + /** Generate the complete documentation for the given group. + * + * @param group - the object from which the documentation must be generated. + * @return the HTML representation of the table. + */ + @SuppressWarnings("synthetic-access") + private String generateRootContent(ExampleGroup group) { + List outline = Lists.newArrayList(); + StringConcatenation content = generateMembers(group, new SectionNumber(), outline); + String topDoc = postProcess(generateDoc(group), outline); + return topDoc + content; + } + + /** Post process the top documentation. + * + * @param text - the original value of the top documentation. + * @param outline - the outline entries. + * @return the top documentation to use. + */ + @SuppressWarnings("static-method") + protected String postProcess(String text, List outline) { + String outlineText; + if (outline != null && !outline.isEmpty()) { + StringBuilder b = new StringBuilder(); + b.append("
    "); //$NON-NLS-1$ + for (String line : outline) { + if (line != null && !line.isEmpty()) { + b.append("
  • "); //$NON-NLS-1$ + b.append(line); + b.append("
  • \n"); //$NON-NLS-1$ + } + } + b.append("
"); //$NON-NLS-1$ + outlineText = b.toString(); + } else { + outlineText = ""; //$NON-NLS-1$ + } + String refactoredText = text; + for (String pattern : OUTLINE_MARKERS) { + refactoredText = refactoredText.replaceAll(pattern, outlineText); + } + return refactoredText; + } + + /** Post process an HTML documentation. + * + * @param text - the original value of the documentation. + * @return the documentation to use. + */ + @SuppressWarnings("static-method") + protected String postProcess(String text) { + String refactoredText = text; + for (NoteTag tag : NoteTag.values()) { + refactoredText = tag.apply(refactoredText); + } + refactoredText = refactoredText.replaceAll("(

){2,}", "

"); //$NON-NLS-1$//$NON-NLS-2$ + refactoredText = refactoredText.replaceAll("(

){2,}", "

"); //$NON-NLS-1$//$NON-NLS-2$ + return refactoredText; + } + + /** Generate the HTML code from the Markdown text + * of the given object. + * + * @param object - the object from which the Markdown text should be extract. + * @return the HTML representation of the table. + */ + protected final String generateDoc(EObject object) { + String doc = documentation(object); + if (doc != null && !doc.isEmpty()) { + return postProcess(markdown2Html(doc)); + } + return ""; //$NON-NLS-1$ + } + + /** Generate the HTML code for the members of an example group.. + * + * @param group - the example group. + * @param sectionNumber - the section numbering from the root to the current object. + * @return the HTML representation of the table. + */ + protected final StringConcatenation generateMembers(ExampleGroup group, SectionNumber sectionNumber) { + return generateMembers(group, sectionNumber, null); + } + + /** Generate the HTML code for the members of an example group.. + * + * @param group - the example group. + * @param sectionNumber - the section numbering from the root to the current object. + * @param outline - the list of the entries for the outline. + * @return the HTML representation of the table. + */ + private StringConcatenation generateMembers(ExampleGroup group, SectionNumber sectionNumber, List outline) { + StringConcatenation result = new StringConcatenation(); + String content; + String hrefLabel; + SectionNumber childNumber; + boolean isRoot = sectionNumber.isRoot(); + boolean isFlatSections = sectionNumber.isMaxDepthReferencing(); + if (isFlatSections) { + result.append("
    "); //$NON-NLS-1$ + } + int position = 0; + for (XtendMember member : group.getMembers()) { + if (member instanceof Example) { + Example example = (Example) member; + childNumber = sectionNumber.getChild(position); + hrefLabel = makeTitleHref(example, childNumber, asTitle(example)); + content = generate(example, childNumber); + ++position; + } else if (member instanceof ExampleGroup) { + ExampleGroup sgroup = (ExampleGroup) member; + childNumber = sectionNumber.getChild(position); + hrefLabel = makeTitleHref(sgroup, childNumber, asTitle(sgroup)); + content = generate(sgroup, childNumber); + ++position; + } else if (member instanceof ExampleTable) { + ExampleTable table = (ExampleTable) member; + childNumber = sectionNumber.getChild(position); + hrefLabel = makeTitleHref(table, childNumber, asTitle(table)); + content = generate(table, childNumber); + ++position; + } else { + content = null; + hrefLabel = null; + } + if (content != null && !content.isEmpty()) { + if (isFlatSections) { + result.append("
  • "); //$NON-NLS-1$ + } + result.append(content); + if (isFlatSections) { + result.append("
  • "); //$NON-NLS-1$ + } else if (isRoot && outline != null + && hrefLabel != null && !hrefLabel.isEmpty()) { + outline.add(hrefLabel); + } + } + } + if (isFlatSections) { + result.append("
"); //$NON-NLS-1$ + } + return result; + } + + /** Generate the HTML code for the given example. + * + * @param example - the example. + * @param sectionNumber - the section numbering from the root to the current object. + * @return the HTML representation of the table. + */ + protected String generate(Example example, SectionNumber sectionNumber) { + String documentation = documentation(example); + List filters; + if (documentation != null && !documentation.isEmpty()) { + FilteringResult result = this.filterExtractor.apply(documentation); + filters = result.getFilters(); + documentation = result.getString(); + documentation = postProcess(markdown2Html(documentation)); + } else { + filters = Collections.emptyList(); + } + String exampleName = example.getName(); + StringBuilder result = new StringBuilder(); + if (exampleName != null && !exampleName.isEmpty()) { + result.append(makeTitleTag(example, sectionNumber)); + } + result.append(documentation); + if (!example.isPending() && example.eIsSet(Literals.XTEND_EXECUTABLE__EXPRESSION)) { + result.append(toCodeBlock(example, filters)); + } + result.append(errorMessage(example)); + return result.toString(); + } + + /** Generate the HTML code for the given example group. + * + * @param group - the example group. + * @param sectionNumber - the section numbering from the root to the current object. + * @return the HTML representation of the table. + */ + protected String generate(ExampleGroup group, SectionNumber sectionNumber) { + return makeTitleTag(group, sectionNumber) + + generateDoc(group) + + generateMembers(group, sectionNumber); + } + + /** Generate the HTML code for the given example table. + * + * @param table - the example table. + * @param sectionNumber - the section numbering from the root to the current object. + * @return the HTML representation of the table. + */ + protected String generate(ExampleTable table, SectionNumber sectionNumber) { + return "" //$NON-NLS-1$ + + toTitle(this.nameProvider.toFieldName(table)) + + "

" //$NON-NLS-1$ + + generateDoc(table) + + super.generate(table); + } + + /** A section number. + * + * @author Stéphane Galland - Initial contribution and API + */ + protected final class SectionNumber { + + private final SectionNumber parent; + private final int position; + private final int depth; + + /** Create the first root section. + */ + private SectionNumber() { + this(null, 0); + } + + /** Create the first section in the given parent. + * + * @param parent - parent number section. + * @param position - index in the given parent. + */ + private SectionNumber(SectionNumber parent, int position) { + this.parent = parent; + this.position = position; + if (this.parent != null) { + this.depth = parent.getDepth() + 1; + } else { + this.depth = 1; + } + } + + /** Replies the depth of the section. + * + * @return the depth of the section. + */ + public int getDepth() { + return this.depth; + } + + /** Replies the HTML representation of this section number. + * + * @return the HTML representation of this section number. + */ + public String toHTML() { + if (MavenConfig.isSectionNumbering() && this.position > 0) { + String txt = toString(); + if (txt != null && !txt.isEmpty()) { + return txt + " "; //$NON-NLS-1$ + } + return txt; + } + return ""; //$NON-NLS-1$ + } + + @Override + public String toString() { + if (MavenConfig.isSectionNumbering() && this.position > 0) { + StringBuilder b = new StringBuilder(); + if (this.parent != null) { + b.append(this.parent.toString()); + } + b.append(Integer.valueOf(this.position)); + b.append("."); //$NON-NLS-1$ + return b.toString(); + } + return ""; //$NON-NLS-1$ + } + + /** Replies the section number of the child at the given position. + * + * @param index - position of the child, starting from zero. + * @return the section number of the first child. + */ + public SectionNumber getChild(int index) { + return new SectionNumber(this, index + 1); + } + + /** Replies if the section number is greater to the max depth + * for referencing them. + * + * @return true if greater than max depth. + */ + public boolean isMaxDepthReferencing() { + return this.depth > MAX_DEPTH_REFERENCING; + } + + /** Replies if the section number is for a root section. + * + * @return true if the number is for root section. + */ + public boolean isRoot() { + return this.depth == 1; + } + + } + + /** HTML tags that could be used to put marked sections in the text. + * + * @author Stéphane Galland - Initial contribution and API + */ + protected static enum NoteTag { + /** A note. + */ + NOTE("note", "label-info", "Note"), //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + + /** An important note. + */ + IMPORTANT("importantnote", "label-warning", "Important"), //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + + /** A caution note. + */ + CAUTION("cautionnote", "label-warning", "Caution"), //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + + /** An very important note. + */ + DANGER("veryimportantnote", "label-danger", "Important"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ + + private final String htmlTag; + private final String htmlLabel; + private final String text; + + private NoteTag(String htmlTag, String htmlLabel, String text) { + this.htmlTag = htmlTag; + this.htmlLabel = htmlLabel; + this.text = text; + } + + /** Format a text according to the current note tag. + * + * @param text - the text to format. + * @return the formated string. + */ + public String apply(String text) { + Pattern pattern = Pattern.compile( + "<" + this.htmlTag //$NON-NLS-1$ + + "(?:\\s+label\\s*=\\s*\"(.*?)\")?" //$NON-NLS-1$ + + "\\s*>" //$NON-NLS-1$ + + "(.*?)" //$NON-NLS-1$ + + "", //$NON-NLS-1$//$NON-NLS-2$ + Pattern.DOTALL); + Matcher m = pattern.matcher(text); + StringBuffer b = new StringBuffer(); + while (m.find()) { + String label = m.group(1); + String htmlText = m.group(2).trim(); + if (label == null) { + label = this.text; + } else { + label = label.trim(); + } + String replacement = + "

" + label + " " //$NON-NLS-1$//$NON-NLS-2$ + + htmlText + "

"; //$NON-NLS-1$ + m.appendReplacement(b, replacement); + } + m.appendTail(b); + return b.toString(); + } + + } + +} diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocModule.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocModule.java new file mode 100644 index 000000000..66d4f7916 --- /dev/null +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocModule.java @@ -0,0 +1,27 @@ +package org.jnario.maven; + +import org.jnario.doc.AbstractDocGenerator; + +import com.google.inject.Binder; +import com.google.inject.Module; + +/** Injection module that permits to inject the documentation generator + * based on the depth of the examples in the specification. + * + * @author Stéphane Galland - Initial contribution and API + */ +class HierarchicalDocModule implements Module { + + /** + */ + public HierarchicalDocModule() { + // + } + + /** {@inheritDoc} + */ + public void configure(Binder binder) { + binder.bind(AbstractDocGenerator.class).to(HierarchicalDocGenerator.class); + } + +} diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocStandaloneSetup.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocStandaloneSetup.java new file mode 100644 index 000000000..c006ff604 --- /dev/null +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/HierarchicalDocStandaloneSetup.java @@ -0,0 +1,40 @@ +package org.jnario.maven; + +import org.eclipse.emf.ecore.EPackage; +import org.jnario.spec.SpecRuntimeModule; +import org.jnario.spec.SpecStandaloneSetupGenerated; +import org.jnario.spec.spec.SpecPackage; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.util.Modules; + +/** Class for setting up the injection mechanism in the Maven plugin. + * + * @author Stéphane Galland - Initial contribution and API + */ +class HierarchicalDocStandaloneSetup extends SpecStandaloneSetupGenerated { + + private static Injector injector; + + /** + */ + public HierarchicalDocStandaloneSetup() { + // + } + + /** {@inheritDoc} + */ + @Override + public Injector createInjectorAndDoEMFRegistration() { + if (injector != null) { + return injector; + } + EPackage.Registry.INSTANCE.put(SpecPackage.eINSTANCE.getNsURI(), SpecPackage.eINSTANCE); + injector = Guice.createInjector( + Modules.override(new SpecRuntimeModule()).with(new HierarchicalDocModule())); + new SpecStandaloneSetupGenerated().register(injector); + return injector; + } + +} diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioDocGenerate.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioDocGenerate.java index e88276ff9..47a0460b0 100755 --- a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioDocGenerate.java +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioDocGenerate.java @@ -62,7 +62,7 @@ public boolean accept(File dir, String name) { * @parameter default-value="${basedir}/target/jnario-doc" * @required */ - private String docOutputDirectory; + protected String docOutputDirectory; /** * Location of the generated JUnit XML reports. @@ -70,26 +70,26 @@ public boolean accept(File dir, String name) { * @parameter default-value="${basedir}/target/surefire-reports" * @required */ - private String reportsDirectory; + protected String reportsDirectory; /** * Location of the generated JUnit XML reports. * * @parameter */ - private String sourceDirectory; + protected String sourceDirectory; @Inject - private RuntimeWorkspaceConfigProvider workspaceConfigProvider; + protected RuntimeWorkspaceConfigProvider workspaceConfigProvider; - private Provider resourceSetProvider; + protected Provider resourceSetProvider; @Override protected void internalExecute() throws MojoExecutionException { getLog().info("Generating Jnario reports to " + docOutputDirectory); // the order is important, the suite compiler must be executed last - List injectors = createInjectors(new SpecStandaloneSetup(), new FeatureStandaloneSetup(), new SuiteStandaloneSetup()); + List injectors = createInjectors(getStandaloneSetups()); generateCssAndJsFiles(injectors); resourceSetProvider = new JnarioMavenProjectResourceSetProvider(project); @@ -98,6 +98,14 @@ protected void internalExecute() throws MojoExecutionException { generateDoc(injector, resultMapping); } } + + protected ISetup[] getStandaloneSetups() { + return new ISetup[] { + new SpecStandaloneSetup(), + new FeatureStandaloneSetup(), + new SuiteStandaloneSetup(), + }; + } protected HashBasedSpec2ResultMapping createSpec2ResultMapping(List injectors) throws MojoExecutionException { HashBasedSpec2ResultMapping resultMapping = injectors.get(2).getInstance(HashBasedSpec2ResultMapping.class); diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioHierarchicalDocGenerate.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioHierarchicalDocGenerate.java new file mode 100644 index 000000000..78186432b --- /dev/null +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/JnarioHierarchicalDocGenerate.java @@ -0,0 +1,39 @@ +package org.jnario.maven; + +import org.apache.maven.plugin.MojoExecutionException; +import org.eclipse.xtext.ISetup; +import org.jnario.feature.FeatureStandaloneSetup; +import org.jnario.suite.SuiteStandaloneSetup; + +/** Maven mojo that is generating the documentation from a Jnario specification + * based on the depth of the examples in the specification. + * + * @author Stéphane Galland - Initial contribution and API + * @requiresDependencyResolution test + * @goal generate2 + */ +public class JnarioHierarchicalDocGenerate extends JnarioDocGenerate { + + /** + * Location of the generated JUnit XML reports. + * + * @parameter default-value="true" + */ + private boolean sectionNumbering; + + @Override + protected void internalExecute() throws MojoExecutionException { + getLog().debug("Set section numbering: " + Boolean.toString(this.sectionNumbering)); //$NON-NLS-1$ + MavenConfig.setSectionNumbering(this.sectionNumbering); + super.internalExecute(); + } + + protected ISetup[] getStandaloneSetups() { + return new ISetup[] { + new HierarchicalDocStandaloneSetup(), + new FeatureStandaloneSetup(), + new SuiteStandaloneSetup(), + }; + } + +} diff --git a/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/MavenConfig.java b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/MavenConfig.java new file mode 100644 index 000000000..33992d8e7 --- /dev/null +++ b/plugins/jnario-maven-report-plugin/src/main/java/org/jnario/maven/MavenConfig.java @@ -0,0 +1,32 @@ +package org.jnario.maven; + + +/** Configuration flags from Maven. + * + * @author Stéphane Galland - Initial contribution and API + */ +final class MavenConfig { + + private static boolean isSectionNumbering; + + private MavenConfig() { + // + } + + /** Replies if the section are automatically numbered. + * + * @return true if sections are numbered. + */ + public static boolean isSectionNumbering() { + return isSectionNumbering; + } + + /** Set if the section are automatically numbered. + * + * @param e - true if sections are numbered. + */ + public static void setSectionNumbering(boolean e) { + isSectionNumbering = e; + } + +}