| Author | DaveJarvis <email> |
|---|---|
| Date | 2025-08-27 13:58:57 GMT-0700 |
| Commit | abd63933fee3523f9047789c80b2ca72855dc501 |
| Parent | 7eeb127 |
| Delta | 284 lines added, 0 lines removed, 284-line increase |
| +/* Copyright 2025 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.spans; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | ||
| +import com.vladsch.flexmark.parser.Parser.Builder; | ||
| + | ||
| +/** | ||
| + * Extension for parsing and rendering bracketed span syntax: | ||
| + * [text]{.class key="val" #id} | ||
| + */ | ||
| +public final class BracketedSpanExtension extends MarkdownRendererExtension { | ||
| + private BracketedSpanExtension() { | ||
| + } | ||
| + | ||
| + public static BracketedSpanExtension create() { | ||
| + return new BracketedSpanExtension(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void extend( final Builder builder ) { | ||
| + builder.customInlineParserExtensionFactory( | ||
| + new BracketedSpanParserFactory() | ||
| + ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected NodeRendererFactory createNodeRendererFactory() { | ||
| + return BracketedSpanRenderer.factory(); | ||
| + } | ||
| +} | ||
| +/* Copyright 2025 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.spans; | ||
| + | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRendererContext; | ||
| +import com.vladsch.flexmark.util.ast.Node; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +import java.util.Map; | ||
| + | ||
| +public class BracketedSpanNode extends Node { | ||
| + private final String mClassName; | ||
| + private final String mId; | ||
| + private final Map<String, String> mAttributes; | ||
| + private final BasedSequence mText; | ||
| + | ||
| + public BracketedSpanNode( | ||
| + final String className, | ||
| + final String id, | ||
| + final Map<String, String> attributes, | ||
| + final BasedSequence text | ||
| + ) { | ||
| + this.mClassName = className; | ||
| + this.mId = id; | ||
| + this.mAttributes = attributes; | ||
| + this.mText = text; | ||
| + } | ||
| + | ||
| + public void render( | ||
| + final NodeRendererContext context, | ||
| + final HtmlWriter html | ||
| + ) { | ||
| + html.raw( "<span" ); | ||
| + | ||
| + if( mClassName != null ) { | ||
| + html.raw( " class=\"" ).raw( mClassName ).raw( "\"" ); | ||
| + } | ||
| + | ||
| + if( mId != null ) { | ||
| + html.raw( " id=\"" ).raw( mId ).raw( "\"" ); | ||
| + } | ||
| + | ||
| + for( var entry : mAttributes.entrySet() ) { | ||
| + html | ||
| + .raw( " " ).raw( entry.getKey() ) | ||
| + .raw( "=\"" ).raw( entry.getValue() ) | ||
| + .raw( "\"" ); | ||
| + } | ||
| + | ||
| + html.raw( ">" ); | ||
| + | ||
| + if( hasChildren() ) { | ||
| + for( var child : getChildren() ) { | ||
| + context.render( child ); | ||
| + } | ||
| + } | ||
| + else { | ||
| + html.raw( mText.toString() ); | ||
| + } | ||
| + | ||
| + html.raw( "</span>" ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public @NotNull BasedSequence[] getSegments() { | ||
| + return new BasedSequence[ 0 ]; | ||
| + } | ||
| +} | ||
| +/* Copyright 2025 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.spans; | ||
| + | ||
| +import com.vladsch.flexmark.parser.InlineParser; | ||
| +import com.vladsch.flexmark.parser.InlineParserExtension; | ||
| +import com.vladsch.flexmark.parser.LightInlineParser; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +import java.util.*; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +public class BracketedSpanParser implements InlineParserExtension { | ||
| + private static final Pattern ATTR_PATTERN = Pattern.compile( | ||
| + "(?:\\.([\\w-]+))|" + | ||
| + "(?:#([\\w-]+))|" + | ||
| + "(\\w+)=\"((?:\\\\\"|[^\"])*)\"" | ||
| + ); | ||
| + | ||
| + public BracketedSpanParser( final @NotNull LightInlineParser ignoredParser ) { | ||
| + } | ||
| + | ||
| + @Override | ||
| + public boolean parse( final @NotNull LightInlineParser parser ) { | ||
| + var result = false; | ||
| + | ||
| + final var input = parser.getInput(); | ||
| + final var index = parser.getIndex(); | ||
| + | ||
| + if( input.charAt( index ) == '[' ) { | ||
| + final var closingBracket = input.indexOf( ']', index ); | ||
| + final var braceStart = input.indexOf( '{', closingBracket ); | ||
| + final var braceEnd = input.indexOf( '}', braceStart ); | ||
| + | ||
| + if( closingBracket != -1 && braceStart != -1 && braceEnd != -1 ) { | ||
| + final var content = input.subSequence( index + 1, closingBracket ); | ||
| + final var span = getBracketedSpan( | ||
| + input, braceStart, braceEnd, | ||
| + content | ||
| + ); | ||
| + | ||
| + span.setChars( input.subSequence( index, braceEnd + 1 ) ); | ||
| + parser.flushTextNode(); | ||
| + parser.getBlock().appendChild( span ); | ||
| + parser.setIndex( braceEnd + 1 ); | ||
| + | ||
| + result = true; | ||
| + } | ||
| + } | ||
| + | ||
| + return result; | ||
| + } | ||
| + | ||
| + private static @NotNull BracketedSpanNode getBracketedSpan( | ||
| + final BasedSequence input, | ||
| + final int braceStart, | ||
| + final int braceEnd, | ||
| + final BasedSequence content | ||
| + ) { | ||
| + final var attrText = | ||
| + input.subSequence( braceStart + 1, braceEnd ).toString(); | ||
| + final var attrs = new HashMap<String, String>(); | ||
| + final var classes = new ArrayList<String>(); | ||
| + String id = null; | ||
| + | ||
| + final var matcher = ATTR_PATTERN.matcher( attrText ); | ||
| + while( matcher.find() ) { | ||
| + if( matcher.group( 1 ) != null ) { | ||
| + classes.add( matcher.group( 1 ) ); | ||
| + } | ||
| + else if( matcher.group( 2 ) != null ) { | ||
| + id = matcher.group( 2 ); | ||
| + } | ||
| + else if( matcher.group( 3 ) != null && matcher.group( 4 ) != null ) { | ||
| + final var key = matcher.group( 3 ); | ||
| + final var rawValue = matcher.group( 4 ).replace( "\\\"", "\"" ); | ||
| + attrs.put( key, rawValue ); | ||
| + } | ||
| + } | ||
| + | ||
| + final var className = classes.isEmpty() ? null : String.join( | ||
| + " ", | ||
| + classes | ||
| + ); | ||
| + return new BracketedSpanNode( className, id, attrs, content ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void finalizeDocument( final @NotNull InlineParser parser ) { | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void finalizeBlock( final @NotNull InlineParser parser ) { | ||
| + } | ||
| +} | ||
| +/* Copyright 2025 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.spans; | ||
| + | ||
| +import com.vladsch.flexmark.parser.InlineParserExtension; | ||
| +import com.vladsch.flexmark.parser.InlineParserExtensionFactory; | ||
| +import com.vladsch.flexmark.parser.LightInlineParser; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +import java.util.Set; | ||
| + | ||
| +/** | ||
| + * Factory for creating the bracketed span inline parser. | ||
| + */ | ||
| +public final class BracketedSpanParserFactory implements InlineParserExtensionFactory { | ||
| + public BracketedSpanParserFactory() { | ||
| + } | ||
| + | ||
| + /** | ||
| + * Characters that trigger this parser. In this case, '[' starts a | ||
| + * bracketed span. | ||
| + */ | ||
| + @Override | ||
| + public @NotNull CharSequence getCharacters() { | ||
| + return "["; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a new instance of the parser. | ||
| + */ | ||
| + @Override | ||
| + public @NotNull InlineParserExtension apply( @NotNull LightInlineParser parser ) { | ||
| + return new BracketedSpanParser( parser ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public @NotNull Set<Class<?>> getAfterDependents() { | ||
| + return Set.of(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public @NotNull Set<Class<?>> getBeforeDependents() { | ||
| + return Set.of(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public boolean affectsGlobalScope() { | ||
| + return false; | ||
| + } | ||
| +} | ||
| +/* Copyright 2025 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.spans; | ||
| + | ||
| +import com.vladsch.flexmark.html.renderer.NodeRenderer; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | ||
| + | ||
| +import java.util.Collections; | ||
| +import java.util.Set; | ||
| + | ||
| +public class BracketedSpanRenderer implements NodeRenderer { | ||
| + @Override | ||
| + public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | ||
| + return Collections.singleton( | ||
| + new NodeRenderingHandler<>( | ||
| + BracketedSpanNode.class, | ||
| + BracketedSpanNode::render | ||
| + ) | ||
| + ); | ||
| + } | ||
| + | ||
| + public static NodeRendererFactory factory() { | ||
| + return _ -> new BracketedSpanRenderer(); | ||
| + } | ||
| +} | ||