001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.datatype.markup.flexmark; // NOPMD AST processor
028
029import com.vladsch.flexmark.formatter.Formatter;
030import com.vladsch.flexmark.formatter.MarkdownWriter;
031import com.vladsch.flexmark.formatter.NodeFormatter;
032import com.vladsch.flexmark.formatter.NodeFormatterContext;
033import com.vladsch.flexmark.formatter.NodeFormatterFactory;
034import com.vladsch.flexmark.formatter.NodeFormattingHandler;
035import com.vladsch.flexmark.html.HtmlRenderer;
036import com.vladsch.flexmark.html.HtmlWriter;
037import com.vladsch.flexmark.html.renderer.NodeRenderer;
038import com.vladsch.flexmark.html.renderer.NodeRendererContext;
039import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
040import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
041import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
042import com.vladsch.flexmark.html2md.converter.HtmlMarkdownWriter;
043import com.vladsch.flexmark.html2md.converter.HtmlNodeConverterContext;
044import com.vladsch.flexmark.html2md.converter.HtmlNodeRenderer;
045import com.vladsch.flexmark.html2md.converter.HtmlNodeRendererFactory;
046import com.vladsch.flexmark.html2md.converter.HtmlNodeRendererHandler;
047import com.vladsch.flexmark.parser.InlineParser;
048import com.vladsch.flexmark.parser.InlineParserExtension;
049import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
050import com.vladsch.flexmark.parser.LightInlineParser;
051import com.vladsch.flexmark.parser.Parser;
052import com.vladsch.flexmark.util.ast.Node;
053import com.vladsch.flexmark.util.data.DataHolder;
054import com.vladsch.flexmark.util.data.DataKey;
055import com.vladsch.flexmark.util.data.MutableDataHolder;
056import com.vladsch.flexmark.util.misc.Extension;
057import com.vladsch.flexmark.util.sequence.BasedSequence;
058import com.vladsch.flexmark.util.sequence.CharSubSequence;
059
060import org.jsoup.nodes.Element;
061
062import java.util.Collections;
063import java.util.Set;
064import java.util.regex.Matcher;
065import java.util.regex.Pattern;
066
067import edu.umd.cs.findbugs.annotations.NonNull;
068
069public class InsertAnchorExtension
070    implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
071    Formatter.FormatterExtension, FlexmarkHtmlConverter.HtmlConverterExtension {
072  public static final DataKey<Boolean> ENABLE_INLINE_INSERT_ANCHORS
073      = new DataKey<>("ENABLE_INLINE_INSERT_ANCHORS", true);
074  public static final DataKey<Boolean> ENABLE_RENDERING = new DataKey<>("ENABLE_RENDERING", true);
075
076  public static Extension create() {
077    return new InsertAnchorExtension();
078  }
079
080  @Override
081  public void parserOptions(MutableDataHolder options) {
082    // do nothing
083  }
084
085  @Override
086  public void rendererOptions(MutableDataHolder options) {
087    // do nothing
088  }
089
090  @Override
091  public void extend(HtmlRenderer.Builder rendererBuilder, String rendererType) {
092    rendererBuilder.nodeRendererFactory(new InsertAnchorNodeRenderer.Factory());
093  }
094
095  @Override
096  public void extend(Parser.Builder parserBuilder) {
097    if (ENABLE_INLINE_INSERT_ANCHORS.get(parserBuilder)) {
098      parserBuilder.customInlineParserExtensionFactory(new InsertAnchorInlineParser.Factory());
099    }
100  }
101
102  @Override
103  public void extend(Formatter.Builder builder) {
104    builder.nodeFormatterFactory(new InsertAnchorFormatter.Factory());
105  }
106
107  @Override
108  public void extend(FlexmarkHtmlConverter.Builder builder) {
109    builder.htmlNodeRendererFactory(new InsertAnchorHtmlNodeRenderer.Factory());
110  }
111
112  private static class InsertAnchorOptions {
113    public final boolean enableInlineInsertAnchors;
114    public final boolean enableRendering;
115
116    public InsertAnchorOptions(DataHolder options) {
117      enableInlineInsertAnchors = ENABLE_INLINE_INSERT_ANCHORS.get(options);
118      enableRendering = ENABLE_RENDERING.get(options);
119    }
120  }
121
122  private static class InsertAnchorNodeRenderer implements NodeRenderer {
123    private final InsertAnchorOptions options;
124
125    public InsertAnchorNodeRenderer(DataHolder options) {
126      this.options = new InsertAnchorOptions(options);
127    }
128
129    @Override
130    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
131      return Collections.singleton(new NodeRenderingHandler<>(InsertAnchorNode.class, this::render));
132    }
133
134    @SuppressWarnings("unused")
135    protected void render(InsertAnchorNode node, NodeRendererContext context, HtmlWriter html) {
136      if (options.enableRendering) {
137        html.attr("type", node.getType()).attr("id-ref", node.getIdReference()).withAttr().tagVoid("insert");
138      }
139    }
140
141    // @Override
142    // public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
143    // HashSet<NodeRenderingHandler<?>> set = new
144    // HashSet<NodeRenderingHandler<?>>();
145    // set.add(new NodeRenderingHandler<Macro>(Macro.class, new
146    // CustomNodeRenderer<Macro>() {
147    // @Override
148    // public void render(Macro node, NodeRendererContext context, HtmlWriter html)
149    // {
150    // MacroNodeRenderer.this.render(node, context, html); }
151    // }));
152    public static class Factory implements NodeRendererFactory {
153
154      @Override
155      public NodeRenderer apply(DataHolder options) {
156        return new InsertAnchorNodeRenderer(options);
157      }
158
159    }
160
161  }
162
163  private static class InsertAnchorInlineParser implements InlineParserExtension {
164    private static final Pattern PATTERN = Pattern.compile("\\{\\{\\s*insert:\\s*([^\\s]+),\\s*([^\\s]+)\\s*\\}\\}");
165
166    public InsertAnchorInlineParser(@SuppressWarnings("unused") LightInlineParser inlineParser) {
167      // do nothing
168    }
169
170    @Override
171    public void finalizeDocument(InlineParser inlineParser) {
172      // do nothing
173    }
174
175    @Override
176    public void finalizeBlock(InlineParser inlineParser) {
177      // do nothing
178    }
179
180    @Override
181    public boolean parse(LightInlineParser inlineParser) {
182      if (inlineParser.peek() == '{') {
183        BasedSequence input = inlineParser.getInput();
184        Matcher matcher = inlineParser.matcher(PATTERN);
185        if (matcher != null) {
186          BasedSequence type = input.subSequence(matcher.start(1), matcher.end(1));
187          BasedSequence idReference = input.subSequence(matcher.start(2), matcher.end(2));
188          assert type != null;
189          assert idReference != null;
190          inlineParser.appendNode(new InsertAnchorNode(type, idReference));
191          return true; // NOPMD - readability
192        }
193      }
194      return false;
195    }
196
197    public static class Factory implements InlineParserExtensionFactory {
198      @Override
199      public Set<Class<?>> getAfterDependents() {
200        return Collections.emptySet();
201      }
202
203      @Override
204      public CharSequence getCharacters() {
205        return "{";
206      }
207
208      @Override
209      public Set<Class<?>> getBeforeDependents() {
210        return Collections.emptySet();
211      }
212
213      @Override
214      public InlineParserExtension apply(LightInlineParser lightInlineParser) {
215        return new InsertAnchorInlineParser(lightInlineParser);
216      }
217
218      @Override
219      public boolean affectsGlobalScope() {
220        return false;
221      }
222    }
223  }
224
225  private static class InsertAnchorFormatter implements NodeFormatter {
226    private final InsertAnchorOptions options;
227
228    public InsertAnchorFormatter(DataHolder options) {
229      this.options = new InsertAnchorOptions(options);
230    }
231
232    @Override
233    public Set<NodeFormattingHandler<?>> getNodeFormattingHandlers() {
234      return options.enableInlineInsertAnchors
235          ? Collections.singleton(new NodeFormattingHandler<>(InsertAnchorNode.class, this::render))
236          : Collections.emptySet();
237    }
238
239    @SuppressWarnings("unused")
240    protected void render(InsertAnchorNode node, NodeFormatterContext context, MarkdownWriter markdown) {
241      if (options.enableRendering) {
242        markdown.append("{{ insert: ");
243        markdown.append(node.getType());
244        markdown.append(", ");
245        markdown.append(node.getIdReference());
246        markdown.append(" }}");
247      }
248    }
249
250    @Override
251    public Set<Class<?>> getNodeClasses() {
252      return Collections.singleton(InsertAnchorNode.class);
253    }
254
255    public static class Factory implements NodeFormatterFactory {
256
257      @Override
258      public NodeFormatter create(DataHolder options) {
259        return new InsertAnchorFormatter(options);
260      }
261
262    }
263  }
264
265  private static class InsertAnchorHtmlNodeRenderer implements HtmlNodeRenderer {
266    private final InsertAnchorOptions options;
267
268    public InsertAnchorHtmlNodeRenderer(DataHolder options) {
269      this.options = new InsertAnchorOptions(options);
270    }
271
272    @Override
273    public Set<HtmlNodeRendererHandler<?>> getHtmlNodeRendererHandlers() {
274      return options.enableInlineInsertAnchors
275          ? Collections.singleton(new HtmlNodeRendererHandler<>("insert", Element.class, this::processInsert))
276          : Collections.emptySet();
277    }
278
279    private void processInsert( // NOPMD used as lambda
280        Element node,
281        @SuppressWarnings("unused") HtmlNodeConverterContext context,
282        HtmlMarkdownWriter out) {
283
284      String type = node.attr("type");
285      String idRef = node.attr("id-ref");
286
287      out.append("{{ insert: ");
288      out.append(type);
289      out.append(", ");
290      out.append(idRef);
291      out.append(" }}");
292    }
293
294    public static class Factory implements HtmlNodeRendererFactory {
295
296      @Override
297      public HtmlNodeRenderer apply(DataHolder options) {
298        return new InsertAnchorHtmlNodeRenderer(options);
299      }
300    }
301  }
302
303  public static class InsertAnchorNode
304      extends Node {
305
306    @NonNull
307    private BasedSequence type;
308    @NonNull
309    private BasedSequence idReference;
310
311    @SuppressWarnings("null")
312    public InsertAnchorNode(@NonNull String type, @NonNull String idReference) {
313      this(CharSubSequence.of(type), CharSubSequence.of(idReference));
314    }
315
316    public InsertAnchorNode(@NonNull BasedSequence type, @NonNull BasedSequence idReference) {
317      this.type = type;
318      this.idReference = idReference;
319    }
320
321    @NonNull
322    public BasedSequence getType() {
323      return type;
324    }
325
326    @NonNull
327    public BasedSequence getIdReference() {
328      return idReference;
329    }
330
331    public void setIdReference(@NonNull BasedSequence value) {
332      this.idReference = value;
333    }
334
335    @Override
336    @NonNull
337    public BasedSequence[] getSegments() {
338      @NonNull BasedSequence[] retval = { getType(), getIdReference() };
339      return retval;
340    }
341
342    @Override
343    public void getAstExtra(StringBuilder out) {
344      segmentSpanChars(out, getType(), "type");
345      segmentSpanChars(out, getIdReference(), "id-ref");
346    }
347  }
348}