diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/.classpath b/plugins/org.eclipse.n4js.jsdoc2spec/.classpath index a2e7d69e3d..3628e33687 100644 --- a/plugins/org.eclipse.n4js.jsdoc2spec/.classpath +++ b/plugins/org.eclipse.n4js.jsdoc2spec/.classpath @@ -6,7 +6,6 @@ - diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/build.properties b/plugins/org.eclipse.n4js.jsdoc2spec/build.properties index 0470e65b92..fb36160409 100644 --- a/plugins/org.eclipse.n4js.jsdoc2spec/build.properties +++ b/plugins/org.eclipse.n4js.jsdoc2spec/build.properties @@ -1,5 +1,4 @@ -source.. = src/,\ - xtend-gen/ +source.. = src/ output.. = bin/ bin.includes = META-INF/,\ .,\ diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/pom.xml b/plugins/org.eclipse.n4js.jsdoc2spec/pom.xml index 4750cb5dd4..b3206816bc 100644 --- a/plugins/org.eclipse.n4js.jsdoc2spec/pom.xml +++ b/plugins/org.eclipse.n4js.jsdoc2spec/pom.xml @@ -49,10 +49,6 @@ Contributors: --> - - org.eclipse.xtend - xtend-maven-plugin - diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.xtend b/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.java similarity index 60% rename from plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.xtend rename to plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.java index 21f14197a8..8b175069e6 100644 --- a/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.xtend +++ b/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocFactory.java @@ -10,9 +10,11 @@ */ package org.eclipse.n4js.jsdoc2spec.adoc; -import com.google.inject.Inject -import org.eclipse.n4js.jsdoc.N4JSDocHelper -import java.util.Map +import java.util.Map; + +import org.eclipse.n4js.jsdoc.N4JSDocHelper; + +import com.google.inject.Inject; /** * Creates AsciiDoc spec fragments for spec region entries. @@ -25,20 +27,21 @@ public class ADocFactory { @Inject ADocSerializer ADocSerializer; - /** * Creates the spec of the given entry for the AsciiDoc document. */ - public def CharSequence createSpecRegionString(SpecRequirementSection spec, Map specsByKey) { - return ADocSerializer.process(spec, specsByKey); + public CharSequence createSpecRegionString(SpecRequirementSection spec, + @SuppressWarnings("unused") Map specsByKey) { + return ADocSerializer.process(spec); } /** * Creates the spec of the given entry for the AsciiDoc document. */ - public def CharSequence createSpecRegionString(SpecIdentifiableElementSection spec, Map specsByKey) { - if (spec.getDoclet === null) { - spec.doclet = n4jsDocHelper.getDoclet(spec.identifiableElement); + public CharSequence createSpecRegionString(SpecIdentifiableElementSection spec, + Map specsByKey) { + if (spec.getDoclet() == null) { + spec.setDoclet(n4jsDocHelper.getDoclet(spec.getIdentifiableElement())); } return ADocSerializer.process(spec, specsByKey); } diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.java b/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.java new file mode 100644 index 0000000000..fcc0a417c7 --- /dev/null +++ b/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.java @@ -0,0 +1,726 @@ +/** + * Copyright (c) 2016 NumberFour AG. + * 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: + * NumberFour AG - Initial API and implementation + */ +package org.eclipse.n4js.jsdoc2spec.adoc; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_CODE; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_LINK; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_REQID; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_SPEC; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_SPECFROMDESCR; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_TASK; +import static org.eclipse.n4js.jsdoc.N4JSDocletParser.TAG_TODO; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.filter; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.groupBy; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.isNullOrEmpty; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.map; +import static org.eclipse.xtext.xbase.lib.IterableExtensions.sortBy; +import static org.eclipse.xtext.xbase.lib.StringExtensions.toFirstUpper; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.n4js.AnnotationDefinition; +import org.eclipse.n4js.jsdoc.dom.Composite; +import org.eclipse.n4js.jsdoc.dom.Doclet; +import org.eclipse.n4js.jsdoc.dom.InlineTag; +import org.eclipse.n4js.jsdoc.dom.LineTag; +import org.eclipse.n4js.jsdoc.dom.Literal; +import org.eclipse.n4js.jsdoc.dom.SimpleTypeReference; +import org.eclipse.n4js.jsdoc.dom.TagValue; +import org.eclipse.n4js.jsdoc.dom.Text; +import org.eclipse.n4js.jsdoc.dom.VariableReference; +import org.eclipse.n4js.jsdoc.tags.DefaultLineTagDefinition; +import org.eclipse.n4js.jsdoc2spec.KeyUtils; +import org.eclipse.n4js.jsdoc2spec.RepoRelativePath; +import org.eclipse.n4js.jsdoc2spec.SpecTestInfo; +import org.eclipse.n4js.ts.types.ContainerType; +import org.eclipse.n4js.ts.types.FieldAccessor; +import org.eclipse.n4js.ts.types.IdentifiableElement; +import org.eclipse.n4js.ts.types.SyntaxRelatedTElement; +import org.eclipse.n4js.ts.types.TAnnotation; +import org.eclipse.n4js.ts.types.TClassifier; +import org.eclipse.n4js.ts.types.TEnum; +import org.eclipse.n4js.ts.types.TFunction; +import org.eclipse.n4js.ts.types.TInterface; +import org.eclipse.n4js.ts.types.TMember; +import org.eclipse.n4js.ts.types.TMemberWithAccessModifier; +import org.eclipse.n4js.ts.types.TMethod; +import org.eclipse.n4js.ts.types.TN4Classifier; +import org.eclipse.n4js.ts.types.TVariable; +import org.eclipse.n4js.typesystem.utils.AllSuperTypesCollector; +import org.eclipse.n4js.utils.DeclMergingHelper; +import org.eclipse.n4js.utils.Strings; +import org.eclipse.n4js.validation.N4JSElementKeywordProvider; +import org.eclipse.n4js.validation.ValidatorMessageHelper; + +import com.google.inject.Inject; + +/** + * Print AsciiDoc code of specification JSDoc. Start and end markers are printed by client. + * + * Needs to be injected. + */ +class ADocSerializer { + @Inject + Html2ADocConverter html2aDocConverter; + @Inject + ValidatorMessageHelper validatorMessageHelper; + @Inject + N4JSElementKeywordProvider keywordProvider; + @Inject + RepoRelativePathHolder repoPathHolder; + @Inject + DeclMergingHelper declMergingHelper; + + String process(SpecRequirementSection spec) { + StringBuilder strb = new StringBuilder(); + appendSpecElementPost(strb, spec); + return Strings.stripAllTrailing(strb.toString()); + } + + String process(SpecIdentifiableElementSection spec, Map specsByKey) { + StringBuilder strb = new StringBuilder(); + appendSpecElementPre(strb, spec); + appendSpec(strb, spec); + appendSpecElementPost(strb, spec, specsByKey); + return Strings.stripAllTrailing(strb.toString()); + } + + private StringBuilder appendSpecElementPost(StringBuilder strb, SpecRequirementSection spec) { + if (!isNullOrEmpty(spec.getTestInfosForType())) { + Map> groupdTests = groupBy(spec.getTestInfosForType(), sti -> sti.testTitle); + appendApiConstraints(strb, groupdTests); + } + return strb; + } + + private StringBuilder appendSpec(StringBuilder strb, SpecIdentifiableElementSection spec) { + strb.append("\n"); + + boolean addedTaskLinks = false; + List taskTags = spec.getDoclet().lineTags(TAG_TASK.getTitle()); + for (LineTag tag : taskTags) { + String taskID = TAG_TASK.getValue(tag, ""); + if (!taskID.isEmpty()) { + if (taskID.startsWith("*")) { + appendTaskLink(strb, taskID.substring(1)); + } else { + appendTaskLink(strb, taskID); + } + strb.append(" "); + addedTaskLinks = true; + } + } + if (addedTaskLinks) + strb.append("\n\n"); + + appendSpecDescription(strb, spec); + return strb; + } + + private StringBuilder appendSpecDescription(StringBuilder strb, SpecIdentifiableElementSection spec) { + Doclet doclet = spec.getDoclet(); + boolean bSpecFromDescr = doclet.hasLineTag(TAG_SPECFROMDESCR.getTitle()); + String reqID = getReqId(doclet); + List specTags = doclet.lineTags(TAG_SPEC.getTitle()); + + if (specTags.isEmpty() && !bSpecFromDescr && reqID.isEmpty()) { + return strb; + } + + if (!(spec.idElement instanceof TN4Classifier || spec.idElement instanceof TEnum)) + strb.append("==== Description\n\n"); + + if (!reqID.isEmpty()) { + strb.append("See req:" + reqID + "[].\n"); + } + + appendContents(strb, doclet); + for (LineTag tag : specTags) { + appendContents(strb, tag.getValueByKey(DefaultLineTagDefinition.CONTENTS)); + } + + strb.append("\n"); + + return strb; + } + + private StringBuilder appendSpecDescriptions(StringBuilder strb, Iterable doclets) { + for (Doclet doclet : doclets) { + boolean bSpecFromDescr = doclet.hasLineTag(TAG_SPECFROMDESCR.getTitle()); + List specTags = doclet.lineTags(TAG_SPEC.getTitle()); + if (!specTags.isEmpty() || bSpecFromDescr) { + appendContents(strb, doclet); + for (LineTag tag : specTags) { + appendContents(strb, tag.getValueByKey(DefaultLineTagDefinition.CONTENTS)); + } + } + } + return strb; + } + + private StringBuilder appendContents(StringBuilder strb, Composite composite) { + boolean contentAdded = false; + for (EObject c : composite.eContents()) { + String newContent = processContent(c).toString(); + strb.append(newContent); + contentAdded |= !newContent.isBlank(); + } + if (contentAdded) { + strb.append("\n"); + } + return strb; + } + + private CharSequence processContent(EObject node) { + if (node instanceof Text) { + return processContent((Text) node); + } + if (node instanceof Literal) { + return processContent((Literal) node); + } + if (node instanceof SimpleTypeReference) { + return processContent((SimpleTypeReference) node); + } + if (node instanceof VariableReference) { + return processContent((VariableReference) node); + } + if (node instanceof InlineTag) { + return processContent((InlineTag) node); + } + + return ""; + } + + private CharSequence processContent(Text node) { + return html2aDocConverter.transformHTML(node.getText()); + } + + private CharSequence processContent(Literal node) { + return html2aDocConverter.transformHTML(node.getValue()); + } + + private CharSequence processContent(SimpleTypeReference node) { + return html2aDocConverter.passThenMonospace(html2aDocConverter.transformHTML(node.getTypeName())); + } + + private CharSequence processContent(VariableReference node) { + return html2aDocConverter.passThenMonospace(html2aDocConverter.transformHTML(node.getVariableName())); + } + + private CharSequence processContent(InlineTag node) { + if (Objects.equals(TAG_CODE.getTitle(), node.getTitle().getTitle())) { + return html2aDocConverter.passThenMonospace(html2aDocConverter.transformHTML(TAG_CODE.getValue(node, ""))); + } + if (Objects.equals(TAG_LINK.getTitle(), node.getTitle().getTitle())) { + return html2aDocConverter.passThenMonospace(html2aDocConverter.transformHTML(TAG_LINK.getValue(node, ""))); + } + + StringBuilder strb = new StringBuilder(); + for (TagValue tv : node.getValues()) { + appendContents(strb, tv); + } + return strb; + } + + private StringBuilder appendSpecElementPre(StringBuilder strb, SpecIdentifiableElementSection spec) { + IdentifiableElement element = spec.getIdentifiableElement(); + if (element instanceof TMember) { + return appendElementCodePre(strb, (TMember) element, spec); + } + if (element instanceof TMethod) { + return appendElementCodePre(strb, (TMethod) element, spec); + } + if (element instanceof TFunction) { + return appendElementCodePre(strb, (TFunction) element, spec); + } + if (element instanceof TVariable) { + return appendElementCodePre(strb, (TVariable) element, spec); + } + return appendElementCodePre(strb, element, spec); + } + + /** + * E.g. classes + */ + private StringBuilder appendElementCodePre(StringBuilder strb, + @SuppressWarnings("unused") IdentifiableElement element, SpecIdentifiableElementSection spec) { + + if (hasTodo(spec.getDoclet())) { + strb.append(getTodoLink(spec.getDoclet())); + } + return strb; + } + + private StringBuilder appendElementCodePre(StringBuilder strb, TMember element, + SpecIdentifiableElementSection spec) { + return appendMemberOrVarOrFuncPre(strb, + validatorMessageHelper.shortDescription(element), + element, + spec); + } + + private StringBuilder appendElementCodePre(StringBuilder strb, TMethod element, + SpecIdentifiableElementSection spec) { + return appendMemberOrVarOrFuncPre(strb, + validatorMessageHelper.shortDescription((TMember) element), + element, + spec); + } + + private StringBuilder appendElementCodePre(StringBuilder strb, TFunction element, + SpecIdentifiableElementSection spec) { + return appendMemberOrVarOrFuncPre(strb, + validatorMessageHelper.shortDescription(element), + element, + spec); + } + + private StringBuilder appendElementCodePre(StringBuilder strb, TVariable element, + SpecIdentifiableElementSection spec) { + return appendMemberOrVarOrFuncPre(strb, + validatorMessageHelper.shortDescription(element), + element, + spec); + } + + private StringBuilder appendMemberOrVarOrFuncPre(StringBuilder strb, String shortDescr, + SyntaxRelatedTElement element, SpecIdentifiableElementSection spec) { + + boolean isIntegratedFromPolyfill = !Objects.equals(spec.sourceEntry.trueFolder, spec.sourceEntry.folder); + String trueSrcFolder = spec.sourceEntry.repository + ":" + spec.sourceEntry.trueFolder; + String todoLink = hasTodo(spec.getDoclet()) ? "\n" + getTodoLink(spec.getDoclet()) : ""; + String polyfill = isIntegratedFromPolyfill + ? "\n\n[.small]#(Integrated from static polyfill aware class in: %s)#".formatted(trueSrcFolder) + : ""; + + strb.append(""" + + [[gsec:spec_%s]] + [role=memberdoc] + === %s%s%s + + ==== Signature + + %s + + """.formatted( + spec.sourceEntry.getAdocCompatibleAnchorID(), + html2aDocConverter.pass(toFirstUpper(shortDescr)), + todoLink, + polyfill, + codeLink(element))); + return strb; + } + + private CharSequence codeLink(EObject element) { + if (element instanceof TVariable) { + return codeLink((TVariable) element); + } + if (element instanceof TMethod) { + return codeLink((TMethod) element); + } + if (element instanceof TFunction) { + return codeLink((TFunction) element); + } + if (element instanceof TMember) { + return codeLink((TMember) element); + } + + throw new IllegalArgumentException(); + } + + private CharSequence codeLink(TMember member) { + return doCodeLink(member, fullSignature(member)); + } + + private CharSequence codeLink(TMethod method) { + return doCodeLink(method, fullSignature(method)); + } + + private CharSequence codeLink(TFunction func) { + return doCodeLink(func, fullSignature(func)); + } + + private CharSequence codeLink(TVariable tvar) { + return doCodeLink(tvar, fullSignature(tvar)); + } + + private String fullSignature(TMember member) { + StringBuilder strb = new StringBuilder(); + for (TAnnotation a : filter(member.getAnnotations(), + ann -> !AnnotationDefinition.INTERNAL.name.equals(ann.getName()))) { + + strb.append(a.getAnnotationAsString() + " "); + } + strb.append(keywordProvider.keyword(member.getMemberAccessModifier()) + " "); + if (member.isAbstract()) { + strb.append("@abstract "); + } + strb.append(member.getMemberAsString()); + + return strb.toString(); + } + + private String fullSignature(TMethod method) { + return validatorMessageHelper.fullFunctionSignature(method); + } + + private String fullSignature(TFunction func) { + return validatorMessageHelper.fullFunctionSignature(func); + } + + private String fullSignature(TVariable tvar) { + if (tvar.getTypeRef() == null) { + return tvar.getName(); + } + return tvar.getName() + ": " + tvar.getTypeRef().getTypeRefAsString(); + } + + private CharSequence doCodeLink(IdentifiableElement element, String signature) { + RepoRelativePath rrp = repoPathHolder.get(element); + StringBuilder strb = new StringBuilder(); + + if (rrp != null) { + SourceEntry se = SourceEntryFactory.create(repoPathHolder, rrp, element); + appendSourceLink(strb, se, html2aDocConverter.passThenMonospace(signature)); + } + + return strb.toString(); + } + + private boolean isInSpec(TMember member, Map specsByKey) { + if (member == null) { + return false; + } + return specsByKey.containsKey(KeyUtils.getSpecKey(repoPathHolder, member)); + } + + private String getFormattedID(Entry> entry, List superTypes) { + int index = superTypes.indexOf(entry.getKey().getContainingType()); + return String.format("%04d", index) + entry.getKey().getName(); + } + + private StringBuilder appendSpecElementPost(StringBuilder strb, SpecIdentifiableElementSection specRegion, + Map specsByKey) { + + IdentifiableElement element = specRegion.getIdentifiableElement(); + if (element instanceof TMember) { + return appendElementPost(strb, (TMember) element, specRegion, specsByKey); + } + if (element instanceof TMethod) { + return appendElementPost(strb, (TMethod) element, specRegion, specsByKey); + } + if (element instanceof TFunction) { + return appendElementPost(strb, (TFunction) element, specRegion, specsByKey); + } + if (element instanceof TVariable) { + return appendElementPost(strb, (TVariable) element, specRegion, specsByKey); + } + return appendElementPost(strb, element, specRegion, specsByKey); + } + + private StringBuilder appendElementPost(StringBuilder strb, + IdentifiableElement element, SpecIdentifiableElementSection specRegion, + Map specsByKey) { + + if (element instanceof ContainerType) { + Map> testsForInherited = specRegion.getTestInfosForInheritedMember(); + if (testsForInherited == null || testsForInherited.isEmpty()) { + return strb; + } + + String typeName = element.getName(); + List superTypes = AllSuperTypesCollector.collect((ContainerType) element, + declMergingHelper); + + Iterable>> tests = filter(testsForInherited.entrySet(), + e -> e.getValue() != null && !e.getValue().isEmpty()); + Iterable>> sortedTests = sortBy(tests, + e -> getFormattedID(e, superTypes)); + + for (Entry> tmemberSpecs : sortedTests) { + String shortDescr = validatorMessageHelper.shortDescription(tmemberSpecs.getKey()); + String secSpecLink = typeName + "." + validatorMessageHelper.shortQualifiedName(tmemberSpecs.getKey()); + String secSpecLinkEsc = SourceEntry.getEscapedAdocAnchorString(secSpecLink); + String description = validatorMessageHelper.description(tmemberSpecs.getKey().getContainingType()); + String shortQualName = validatorMessageHelper.shortQualifiedName(tmemberSpecs.getKey()); + String gsecSpec = isInSpec(tmemberSpecs.getKey(), specsByKey) + ? "\n\t<>\n".formatted(html2aDocConverter.pass(shortQualName)) + : ""; + + strb.append(""" + + [[gsec:spec_%s]] + [role=memberdoc] + === %s + + Inherited from + %s%s + + """.formatted( + secSpecLinkEsc, + html2aDocConverter.pass(toFirstUpper(shortDescr)), + html2aDocConverter.pass(description), + gsecSpec)); + + appendConstraints(strb, tmemberSpecs.getKey(), specRegion, tmemberSpecs.getValue(), false); + } + + } + return strb; + + } + + private StringBuilder appendElementPost(StringBuilder strb, TMember element, + SpecIdentifiableElementSection specRegion, + @SuppressWarnings("unused") Map specsByKey) { + + appendConstraints(strb, element, specRegion, specRegion.getTestInfosForMember(), + !hasReqId(specRegion.getDoclet())); + return strb; + } + + private StringBuilder appendElementPost(StringBuilder strb, TMethod element, + SpecIdentifiableElementSection specRegion, + @SuppressWarnings("unused") Map specsByKey) { + + appendConstraints(strb, element, specRegion, specRegion.getTestInfosForMember(), + !hasReqId(specRegion.getDoclet())); + return strb; + } + + private StringBuilder appendElementPost(StringBuilder strb, TFunction element, + SpecIdentifiableElementSection specRegion, + @SuppressWarnings("unused") Map specsByKey) { + + if (!isNullOrEmpty(specRegion.getTestInfosForType())) { + Map> groupdTests = groupBy(specRegion.getTestInfosForType(), + sti -> sti.testTitle); + + strb.append("==== Semantics\n"); + appendApiConstraints(strb, groupdTests); + } else { + + String reqID = getReqId(specRegion.getDoclet()); + if (reqID.isEmpty()) { + String todoLink = getTodoLink( + "Add tests specifying semantics for " + html2aDocConverter.passThenMonospace(element.getName()), + "test function " + html2aDocConverter.pass(element.getName())); + + strb.append(""" + + ==== Semantics + %s + """.formatted(todoLink)); + } else { + strb.append("% tests see " + reqID); + } + } + + return strb; + } + + private StringBuilder appendElementPost(StringBuilder strb, @SuppressWarnings("unused") TVariable element, + SpecIdentifiableElementSection specRegion, + @SuppressWarnings("unused") Map specsByKey) { + + if (!isNullOrEmpty(specRegion.getTestInfosForType())) { + Map> groupdTests = groupBy(specRegion.getTestInfosForType(), + sti -> sti.testTitle); + + strb.append("==== Semantics\n"); + appendApiConstraints(strb, groupdTests); + } + return strb; // test are optional for variables. + } + + private StringBuilder appendConstraints(StringBuilder strb, TMember element, + SpecIdentifiableElementSection specRegion, Set specTestInfos, boolean addTodo) { + + if (!isNullOrEmpty(specTestInfos)) { + Map> groupdTests = groupBy(specTestInfos, sti -> sti.testTitle); + strb.append("==== Semantics\n"); + appendApiConstraints(strb, groupdTests); + + } else if (addTodo) { + + if (elementMayNeedsTest(element, specRegion)) { + String todoLink = getTodoLink( + "Add tests specifying semantics for " + + html2aDocConverter.passThenMonospace(element.getMemberAsString()), + "test " + html2aDocConverter + .pass(element.getContainingType().getName() + "." + element.getName())); + + strb.append(""" + + ==== Semantics + %s + """.formatted(todoLink)); + } + } + return strb; + } + + private boolean elementMayNeedsTest(TMember element, SpecIdentifiableElementSection spec) { + // there are tests, so we show them + if (!isNullOrEmpty(spec.getTestInfosForType())) { + return true; + } + if ((element instanceof TMethod) || (element instanceof FieldAccessor)) { + if (element.getContainingType() instanceof TInterface) { + if (element instanceof TMemberWithAccessModifier) { + return !((TMemberWithAccessModifier) element).isHasNoBody(); + } + } + return !element.isAbstract(); + } + return false; + } + + /** + * List of tests in apiConstraint macros. + */ + private StringBuilder appendApiConstraints(StringBuilder strb, + Map> groupdTests) { + for (Map.Entry> group : sortBy(groupdTests.entrySet(), + e -> e.getKey().toString())) { + strb.append("\n"); + strb.append(". *"); + String key = group.getKey().toString(); + String keyWithoutPrecedingNumber = removePrecedingNumber(key); + strb.append(html2aDocConverter.pass(keyWithoutPrecedingNumber)); + strb.append("* ("); + Iterator iter = group.getValue().iterator(); + while (iter.hasNext()) { + SpecTestInfo testSpec = iter.next(); + strb.append(nfgitTest(testSpec)); + if (iter.hasNext()) { + strb.append(", \n"); + } + } + strb.append(")\n"); + Iterable doclets = map(filter(group.getValue(), spi -> spi.doclet != null), spi -> spi.doclet); + StringBuilder strbTmp = new StringBuilder(); + appendSpecDescriptions(strbTmp, doclets); + if (strbTmp.length() > 0) { + strb.append("+\n"); + strb.append("[.generatedApiConstraint]\n"); + strb.append("====\n\n"); + strb.append(strbTmp); + strb.append("\n====\n"); + } + } + return strb; + } + + private CharSequence nfgitTest(SpecTestInfo testInfo) { + StringBuilder strb = new StringBuilder(); + if (testInfo.rrp == null) { + strb.append(small(testInfo.testModuleSpec() + ".")); + strb.append(testInfo.testMethodTypeName() + "." + testInfo.testMethodName()); + } else { + SourceEntry pc = SourceEntryFactory.create(testInfo); + String strCase = "Test"; + if (!isNullOrEmpty(testInfo.testCase)) { + String formattedCase = removePrecedingNumber(testInfo.testCase); + if (isNullOrEmpty(formattedCase)) { + formattedCase = testInfo.testCase; + } + html2aDocConverter.pass(formattedCase); + } + StringBuilder strbTmp = new StringBuilder(); + appendSourceLink(strbTmp, pc, strCase); + strb.append(small(strbTmp)); + } + return strb.toString(); + } + + /** + * Reminder: Escaping the caption using the method {@link Html2ADocConverter#pass} is recommended. + */ + private StringBuilder appendSourceLink(StringBuilder strb, SourceEntry pc, String caption) { + strb.append("srclnk:++" + pc.toPQN() + "++[" + caption + "]"); + return strb; + } + + /** + * Returns req id, may be an empty string but never null. + */ + private String getReqId(Doclet doclet) { + return TAG_REQID.getValue(doclet, ""); + } + + /** + * Returns true, if spec contains a reference to a requirement id. + */ + private boolean hasReqId(Doclet doclet) { + return !getReqId(doclet).isEmpty(); + } + + /** + * Returns true, if spec contains a reference to a todo. + */ + private boolean hasTodo(Doclet doclet) { + return !getTodo(doclet).isEmpty(); + } + + /** + * Returns todo, may be an empty string but never null. + */ + private String getTodo(Doclet doclet) { + return TAG_TODO.getValue(doclet, ""); + } + + private String getTodoLink(String todoText, String sideText) { + String str = isNullOrEmpty(sideText) ? "" : ", title=\"" + sideText + "\""; + String todo = """ + + [TODO%s] + -- + %s + -- + """.formatted(str, todoText); + return todo; + } + + private String getTodoLink(Doclet doclet) { + return getTodoLink(getTodo(doclet), ""); + } + + private StringBuilder appendTaskLink(StringBuilder strb, String taskID) { + strb.append("task:" + taskID + "[]"); + return strb; + } + + private String small(CharSequence smallString) { + return "[.small]#" + smallString + "#"; + } + + private String removePrecedingNumber(String key) { + for (var i = 0; i < key.length(); i++) { + String stringAt = Character.toString(key.charAt(i)); + if (!"0123456789 ".contains(stringAt)) { + return key.substring(i); + } + } + return ""; + } + +} diff --git a/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.xtend b/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.xtend deleted file mode 100644 index b5bc565c81..0000000000 --- a/plugins/org.eclipse.n4js.jsdoc2spec/src/org/eclipse/n4js/jsdoc2spec/adoc/ADocSerializer.xtend +++ /dev/null @@ -1,607 +0,0 @@ -/** - * Copyright (c) 2016 NumberFour AG. - * 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: - * NumberFour AG - Initial API and implementation - */ -package org.eclipse.n4js.jsdoc2spec.adoc; - -import com.google.inject.Inject -import java.util.Collection -import java.util.List -import java.util.Map -import java.util.Map.Entry -import java.util.Set -import java.util.SortedSet -import org.eclipse.n4js.AnnotationDefinition -import org.eclipse.n4js.jsdoc.dom.Composite -import org.eclipse.n4js.jsdoc.dom.ContentNode -import org.eclipse.n4js.jsdoc.dom.Doclet -import org.eclipse.n4js.jsdoc.dom.InlineTag -import org.eclipse.n4js.jsdoc.dom.Literal -import org.eclipse.n4js.jsdoc.dom.SimpleTypeReference -import org.eclipse.n4js.jsdoc.dom.Text -import org.eclipse.n4js.jsdoc.dom.VariableReference -import org.eclipse.n4js.jsdoc.tags.DefaultLineTagDefinition -import org.eclipse.n4js.jsdoc2spec.KeyUtils -import org.eclipse.n4js.jsdoc2spec.RepoRelativePath -import org.eclipse.n4js.jsdoc2spec.SpecTestInfo -import org.eclipse.n4js.ts.types.ContainerType -import org.eclipse.n4js.ts.types.FieldAccessor -import org.eclipse.n4js.ts.types.IdentifiableElement -import org.eclipse.n4js.ts.types.SyntaxRelatedTElement -import org.eclipse.n4js.ts.types.TClassifier -import org.eclipse.n4js.ts.types.TEnum -import org.eclipse.n4js.ts.types.TFunction -import org.eclipse.n4js.ts.types.TInterface -import org.eclipse.n4js.ts.types.TMember -import org.eclipse.n4js.ts.types.TMemberWithAccessModifier -import org.eclipse.n4js.ts.types.TMethod -import org.eclipse.n4js.ts.types.TN4Classifier -import org.eclipse.n4js.ts.types.TVariable -import org.eclipse.n4js.typesystem.utils.AllSuperTypesCollector -import org.eclipse.n4js.utils.DeclMergingHelper -import org.eclipse.n4js.utils.Strings -import org.eclipse.n4js.validation.N4JSElementKeywordProvider -import org.eclipse.n4js.validation.ValidatorMessageHelper - -import static org.eclipse.n4js.jsdoc.N4JSDocletParser.* - -/** - * Print AsciiDoc code of specification JSDoc. Start and end markers are printed by client. - * - * Needs to be injectd. - */ -class ADocSerializer { - @Inject extension Html2ADocConverter; - @Inject ValidatorMessageHelper validatorMessageHelper; - @Inject N4JSElementKeywordProvider keywordProvider; - @Inject RepoRelativePathHolder repoPathHolder; - @Inject DeclMergingHelper declMergingHelper; - - - def String process(SpecRequirementSection spec, Map specsByKey) { - val strb = new StringBuilder(); - strb.appendSpecElementPost(spec, specsByKey); - return Strings.stripAllTrailing(strb.toString()); - } - - def String process(SpecIdentifiableElementSection spec, Map specsByKey) { - val strb = new StringBuilder(); - strb.appendSpecElementPre(spec); - strb.appendSpec(spec); - strb.appendSpecElementPost(spec, specsByKey); - return Strings.stripAllTrailing(strb.toString()); - } - - private def StringBuilder appendSpecElementPost(StringBuilder strb, SpecRequirementSection spec, Map map) { - if (! spec.getTestInfosForType.isNullOrEmpty) { - val Map> groupdTests = spec.getTestInfosForType.groupBy[testTitle]; - strb.appendApiConstraints(groupdTests); - } - return strb - } - - private def StringBuilder appendSpec(StringBuilder strb, SpecIdentifiableElementSection spec) { - strb.append("\n"); - - var addedTaskLinks = false; - val taskTags = spec.getDoclet.lineTags(TAG_TASK.title); - for (tag : taskTags) { - val taskID = TAG_TASK.getValue(tag, ""); - if (!taskID.empty) { - if (taskID.startsWith("*")) { - strb.appendTaskLink(taskID.substring(1)); - } else { - strb.appendTaskLink(taskID); - } - strb.append(" "); - addedTaskLinks = true; - } - } - if(addedTaskLinks) - strb.append("\n\n"); - - strb.appendSpecDescription(spec); - return strb - } - - private def StringBuilder appendSpecDescription(StringBuilder strb, SpecIdentifiableElementSection spec) { - val doclet = spec.getDoclet; - val bSpecFromDescr = doclet.hasLineTag(TAG_SPECFROMDESCR.title); - val reqID = getReqId(doclet); - val specTags = doclet.lineTags(TAG_SPEC.title); - - if (specTags.empty && !bSpecFromDescr && reqID.isEmpty) - return strb; - - if (!(spec.idElement instanceof TN4Classifier || spec.idElement instanceof TEnum)) - strb.append("==== Description\n\n"); - - if (!reqID.isEmpty) { - strb.append("See req:"+reqID +"[].\n"); - } - - strb.appendContents(doclet); - for (tag : specTags) { - strb.appendContents(tag.getValueByKey(DefaultLineTagDefinition.CONTENTS)); - } - - strb.append("\n"); - - return strb; - } - - private def StringBuilder appendSpecDescriptions(StringBuilder strb, Iterable doclets) { - for (doclet : doclets) { - var bSpecFromDescr = doclet.hasLineTag(TAG_SPECFROMDESCR.title); - val specTags = doclet.lineTags(TAG_SPEC.title); - if (! specTags.empty || bSpecFromDescr) { - strb.appendContents(doclet); - for (tag : specTags) { - strb.appendContents(tag.getValueByKey(DefaultLineTagDefinition.CONTENTS)); - } - } - } - return strb; - } - - private def StringBuilder appendContents(StringBuilder strb, Composite composite) { - for (c : composite.contents) { - strb.append(processContent(c)); - } - if (!composite.contents.isEmpty) { - strb.append("\n"); - } - return strb; - } - - - private def dispatch CharSequence processContent(ContentNode node) {} - private def dispatch CharSequence processContent(Text node) { - return transformHTML(node.text); - } - private def dispatch CharSequence processContent(Literal node) { - return transformHTML(node.value); - } - private def dispatch CharSequence processContent(SimpleTypeReference node) { - return passThenMonospace(transformHTML(node.typeName)); - } - private def dispatch CharSequence processContent(VariableReference node) { - return passThenMonospace(transformHTML(node.variableName)); - } - private def dispatch CharSequence processContent(InlineTag node) { - switch (node.title.title) { - case TAG_CODE.title: passThenMonospace(transformHTML(TAG_CODE.getValue(node, ""))) - case TAG_LINK.title: passThenMonospace(transformHTML(TAG_LINK.getValue(node, ""))) - default: { - val StringBuilder strb = new StringBuilder(); - node.values.forEach[strb.appendContents(it)]; - return strb; - } - } - } - - - private def StringBuilder appendSpecElementPre(StringBuilder strb, SpecIdentifiableElementSection spec) { - return strb.appendElementCodePre(spec.identifiableElement, spec); - } - - - /** - * E.g. classes - */ - private def dispatch StringBuilder appendElementCodePre(StringBuilder strb, IdentifiableElement element, SpecIdentifiableElementSection spec) { - if (hasTodo(spec.doclet)) - strb.append(getTodoLink(spec.doclet)); - return strb; - } - private def dispatch StringBuilder appendElementCodePre(StringBuilder strb, TMember element, SpecIdentifiableElementSection spec) { - return strb.appendMemberOrVarOrFuncPre( - validatorMessageHelper.shortDescription(element), - validatorMessageHelper.shortQualifiedName(element), - element.memberAsString, - element, - spec - ); - } - private def dispatch StringBuilder appendElementCodePre(StringBuilder strb, TMethod element, SpecIdentifiableElementSection spec) { - return strb.appendMemberOrVarOrFuncPre( - validatorMessageHelper.shortDescription(element as TMember), - validatorMessageHelper.shortQualifiedName(element as TMember), - element.memberAsString, - element, - spec - ); - } - private def dispatch StringBuilder appendElementCodePre(StringBuilder strb, TFunction element, SpecIdentifiableElementSection spec) { - return strb.appendMemberOrVarOrFuncPre( - validatorMessageHelper.shortDescription(element), - validatorMessageHelper.shortQualifiedName(element), - element.name, - element, - spec - ); - } - private def dispatch StringBuilder appendElementCodePre(StringBuilder strb, TVariable element, SpecIdentifiableElementSection spec) { - return strb.appendMemberOrVarOrFuncPre( - validatorMessageHelper.shortDescription(element), - validatorMessageHelper.shortQualifiedName(element), - element.name, - element, - spec - ); - } - - - private def StringBuilder appendMemberOrVarOrFuncPre(StringBuilder strb, String shortDescr, String shortQN, - String reqName, SyntaxRelatedTElement element, SpecIdentifiableElementSection spec) { - - val boolean isIntegratedFromPolyfill = spec.sourceEntry.trueFolder != spec.sourceEntry.folder; - val String trueSrcFolder = spec.sourceEntry.repository + ":" + spec.sourceEntry.trueFolder; - - strb.append( - ''' - - [[gsec:spec_«spec.sourceEntry.adocCompatibleAnchorID»]] - [role=memberdoc] - === «pass(shortDescr.toFirstUpper)» - «IF hasTodo(spec.doclet)» - «getTodoLink(spec.doclet)» - «ENDIF» - «IF isIntegratedFromPolyfill» - - [.small]#(Integrated from static polyfill aware class in: «trueSrcFolder»)# - «ENDIF» - - ==== Signature - - «codeLink(element)» - - '''); - return strb; - } - - - private def dispatch CharSequence codeLink(TMember member) { - return doCodeLink(member, fullSignature(member)); - } - private def dispatch CharSequence codeLink(TMethod method) { - return doCodeLink(method, fullSignature(method)); - } - private def dispatch CharSequence codeLink(TFunction func) { - return doCodeLink(func, fullSignature(func)); - } - private def dispatch CharSequence codeLink(TVariable tvar) { - return doCodeLink(tvar, fullSignature(tvar)); - } - - - private def fullSignature(TMember member) { - val StringBuilder strb = new StringBuilder(); - for (a: member.annotations.filter[it.name!=AnnotationDefinition.INTERNAL.name]) { - strb.append(a.annotationAsString + " "); - } - strb.append(keywordProvider.keyword(member.memberAccessModifier) + " "); - if (member.abstract) { - strb.append("@abstract "); - } - strb.append(member.memberAsString); - - return strb.toString; - } - private def fullSignature(TMethod method) { - return validatorMessageHelper.fullFunctionSignature(method); - } - private def fullSignature(TFunction func) { - return validatorMessageHelper.fullFunctionSignature(func); - } - private def fullSignature(TVariable tvar) { - if (tvar.typeRef===null) { - return tvar.name; - } - return '''«tvar.name»: «tvar.typeRef.typeRefAsString»'''; - } - - - private def CharSequence doCodeLink(IdentifiableElement element, String signature) { - val RepoRelativePath rrp = repoPathHolder.get(element); - val strb = new StringBuilder(); - - if (rrp !== null) { - val SourceEntry se = SourceEntryFactory.create(repoPathHolder, rrp, element); - strb.appendSourceLink(se, passThenMonospace(signature)); - } - - return strb.toString(); - } - - - private def StringBuilder appendSpecElementPost(StringBuilder strb, SpecIdentifiableElementSection spec, Map map) { - return strb.appendElementPost(spec.identifiableElement, spec, map); - } - - private def dispatch StringBuilder appendElementPost(StringBuilder strb, - IdentifiableElement element, SpecIdentifiableElementSection specRegion, Map specsByKey) { - - if (element instanceof ContainerType) { - var Map> testsForInherited = specRegion.getTestInfosForInheritedMember - if (testsForInherited === null || testsForInherited.empty) { - return strb; - } - - val typeName = element.name; - val superTypes = AllSuperTypesCollector.collect( - element, declMergingHelper - ) - - val tests = testsForInherited.entrySet.filter[value!==null && !value.empty]; - val sortedTests = tests.sortBy[getFormattedID(it, superTypes)]; - for (tmemberSpecs : sortedTests) { - val shortDescr = validatorMessageHelper.shortDescription(tmemberSpecs.key); - val secSpecLink = typeName + "." + validatorMessageHelper.shortQualifiedName(tmemberSpecs.key); - val secSpecLinkEsc = SourceEntry.getEscapedAdocAnchorString(secSpecLink); - val description = validatorMessageHelper.description(tmemberSpecs.key.containingType); - val shortQualName = validatorMessageHelper.shortQualifiedName(tmemberSpecs.key); - - strb.append( - ''' - - [[gsec:spec_«secSpecLinkEsc»]] - [role=memberdoc] - === «pass(shortDescr.toFirstUpper)» - - Inherited from - «pass(description)» - «IF isInSpec(tmemberSpecs.key, specsByKey)» - <> - «ENDIF» - - '''); - - strb.appendConstraints(tmemberSpecs.key, specRegion, tmemberSpecs.value, false); - } - } - return strb - } - - - private def boolean isInSpec(TMember member, Map specsByKey) { - if (member === null) { - return false; - } - return specsByKey.containsKey(KeyUtils.getSpecKey(repoPathHolder, member)); - } - - private def String getFormattedID(Entry> entry, List superTypes) { - val index = superTypes.indexOf(entry.key.containingType); - return String.format("%04d", index) + entry.key.name - } - - - private def dispatch StringBuilder appendElementPost(StringBuilder strb, TMember element, SpecIdentifiableElementSection specRegion, Map specsByKey) { - strb.appendConstraints(element, specRegion, specRegion.getTestInfosForMember, ! hasReqId(specRegion.getDoclet)); - return strb; - } - private def dispatch StringBuilder appendElementPost(StringBuilder strb, TMethod element, SpecIdentifiableElementSection specRegion, Map specsByKey) { - strb.appendConstraints(element, specRegion, specRegion.getTestInfosForMember, ! hasReqId(specRegion.getDoclet)); - return strb; - } - private def dispatch StringBuilder appendElementPost(StringBuilder strb, TFunction element, SpecIdentifiableElementSection specRegion, Map specsByKey) { - - if (! specRegion.getTestInfosForType.isNullOrEmpty) { - val Map> groupdTests = specRegion.getTestInfosForType.groupBy[testTitle]; - strb.append("==== Semantics\n"); - strb.appendApiConstraints(groupdTests); - } else { - - val reqID = getReqId(specRegion.getDoclet); - if (reqID.isEmpty) { - val String todoLink = getTodoLink( - "Add tests specifying semantics for " + passThenMonospace(element.name), - "test function " + pass(element.name)); - - strb.append( - ''' - - ==== Semantics - «todoLink» - '''); - } else { - strb.append('''% tests see «reqID»'''); - } - } - - return strb; - } - private def dispatch StringBuilder appendElementPost(StringBuilder strb, TVariable element, SpecSection specRegion, Map specsByKey) { - if (! specRegion.getTestInfosForType.isNullOrEmpty) { - val Map> groupdTests = specRegion.getTestInfosForType.groupBy[testTitle]; - strb.append("==== Semantics\n"); - strb.appendApiConstraints(groupdTests) - } - return strb; // test are optional for variables. - } - - - private def StringBuilder appendConstraints(StringBuilder strb, TMember element, SpecIdentifiableElementSection specRegion, Set specTestInfos, boolean addTodo) { - if (! specTestInfos.isNullOrEmpty) { - val Map> groupdTests = specTestInfos.groupBy[testTitle]; - strb.append("==== Semantics\n"); - strb.appendApiConstraints(groupdTests); - - } else if (addTodo) { - - if (elementMayNeedsTest(element, specRegion)) { - val String todoLink = getTodoLink( - "Add tests specifying semantics for " + passThenMonospace(element.memberAsString), - "test " + pass(element.containingType.name + "." + element.name)); - - strb.append( - ''' - - ==== Semantics - «todoLink» - '''); - } - } - return strb - } - - private def boolean elementMayNeedsTest(TMember element, SpecIdentifiableElementSection spec) { - // there are tests, so we show them - if (! spec.getTestInfosForType.isNullOrEmpty) { - return true; - } - if ((element instanceof TMethod) || (element instanceof FieldAccessor)) { - if (element.containingType instanceof TInterface) { - if (element instanceof TMemberWithAccessModifier) { - return ! element.hasNoBody - } - } - return !element.isAbstract; - } - return false; - } - - /** - * List of tests in apiConstraint macros. - */ - private def StringBuilder appendApiConstraints(StringBuilder strb, Map> groupdTests) { - for (group : groupdTests.entrySet.sortBy[it.key.toString]) { - strb.append("\n"); - strb.append(". *"); - val key = group.key.toString; - val keyWithoutPrecedingNumber = removePrecedingNumber(key); - strb.append(pass(keyWithoutPrecedingNumber)); - strb.append("* ("); - val iter = group.value.iterator; - while (iter.hasNext) { - val SpecTestInfo testSpec = iter.next; - strb.append(nfgitTest(testSpec)); - if (iter.hasNext) { - strb.append(", \n"); - } - } - strb.append(")\n"); - val Iterable doclets = group.value.filter[doclet !== null].map[doclet]; - val strbTmp = new StringBuilder(); - strbTmp.appendSpecDescriptions(doclets); - if (strbTmp.length > 0) { - strb.append("+\n"); - strb.append("[.generatedApiConstraint]\n"); - strb.append("====\n\n"); - strb.append(strbTmp); - strb.append("\n====\n"); - } - } - return strb - } - - private def CharSequence nfgitTest(SpecTestInfo testInfo) { - val strb = new StringBuilder(); - if (testInfo.rrp === null) { - strb.append(small(testInfo.testModuleSpec() + ".")); - strb.append(testInfo.testMethodTypeName() + "." + testInfo.testMethodName()); - } else { - val pc = SourceEntryFactory.create(testInfo); - val strCase = if (testInfo.testCase.nullOrEmpty) "Test" else { - var formattedCase = removePrecedingNumber(testInfo.testCase); - if (formattedCase.nullOrEmpty) { - formattedCase = testInfo.testCase; - } - pass(formattedCase); - } - val strbTmp = new StringBuilder(); - strbTmp.appendSourceLink(pc, strCase); - strb.append(small(strbTmp)); - } - return strb.toString(); - } - - /** - * Reminder: Escaping the caption using the method {@link Html2ADocConverter#pass} is recommended. - */ - private def StringBuilder appendSourceLink(StringBuilder strb, SourceEntry pc, String caption) { - strb.append( - '''srclnk:++« - pc.toPQN - »++[« - caption - »]'''); - return strb; - } - - /** - * Returns req id, may be an empty string but never null. - */ - private def String getReqId(Doclet doclet) { - return TAG_REQID.getValue(doclet, ""); - } - - /** - * Returns true, if spec contains a reference to a requirement id. - */ - private def boolean hasReqId(Doclet doclet) { - return ! getReqId(doclet).isEmpty; - } - - /** - * Returns true, if spec contains a reference to a todo. - */ - private def boolean hasTodo(Doclet doclet) { - return ! getTodo(doclet).isEmpty; - } - - /** - * Returns todo, may be an empty string but never null. - */ - private def String getTodo(Doclet doclet) { - return TAG_TODO.getValue(doclet, ""); - } - - private def String getTodoLink(String todoText, String sideText) { - val todo = - ''' - - [TODO« - IF !sideText.isNullOrEmpty - », title="«sideText»"« - ENDIF - »] - -- - «todoText» - -- - - ''' - return todo; - } - - private def String getTodoLink(Doclet doclet) { - return getTodoLink(getTodo(doclet), ""); - } - - private def StringBuilder appendTaskLink(StringBuilder strb, String taskID) { - strb.append('''task:«taskID»[]'''); - return strb; - } - - private def String small(CharSequence smallString) { - return '''[.small]#«smallString»#'''; - } - - private def String removePrecedingNumber(String key) { - for (var i=0; i