Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms-api/src/main/java/com/condation/cms/api/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static class ContentTypes {
public static final int DEFAULT_PAGE_SIZE = 5;

public static final String DEFAULT_CONTENT_TYPE = ContentTypes.HTML;
public static final List<String> DEFAULT_CONTENT_PIPELINE = List.of("markdown", "tags");
public static final List<String> DEFAULT_CONTENT_PIPELINE = List.of("markdown", "shortCodes");

public static final int DEFAULT_REDIRECT_STATUS = 301;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Tag {
public @interface ShortCode {
String value ();
String namespace() default Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@
*
* @author t.marx
*/
public abstract class RegisterTagsExtensionPoint extends AbstractExtensionPoint {
public abstract class RegisterShortCodesExtensionPoint extends AbstractExtensionPoint {

@Deprecated(since = "8.1.0", forRemoval = true)
public Map<String, Function<Parameter, String>> tags () {
public Map<String, Function<Parameter, String>> shortCodes () {
return Collections.emptyMap();
}

public List<Object> tagDefinitions () {
public List<Object> shortCodeDefinitions () {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum Hooks {
NAVIGATION_PATH("system/navigation/%s/path"),
NAVIGATION_LIST("system/navigation/%s/list"),
/* content */
CONTENT_TAGS("system/content/tags"),
CONTENT_SHORTCODES("system/content/shortCodes"),
CONTENT_FILTER("system/content/filter"),
LAYOUT_HEADER("system/layout/header"),
LAYOUT_FOOTER("system/layout/footer"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public class MediaUtils {
public enum Format {
PNG,
JPEG,
WEBP;
WEBP,
AVIF;
}

public static Format format4String(final String format) {
Expand All @@ -44,6 +45,8 @@ public static Format format4String(final String format) {
Format.JPEG;
case "png" ->
Format.PNG;
case "avif" ->
Format.AVIF;
default ->
throw new RuntimeException("unknown image format");
};
Expand All @@ -57,6 +60,8 @@ public static String mime4Format(final Format format) {
"image/png";
case WEBP ->
"image/webp";
case AVIF ->
"image/avif";
default ->
throw new RuntimeException("unknown image format");
};
Expand All @@ -70,6 +75,8 @@ public static String fileending4Format(final Format format) {
".png";
case WEBP ->
".webp";
case AVIF ->
".avif";
default ->
throw new RuntimeException("unknown image format");
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import com.condation.cms.api.feature.Feature;
import com.condation.cms.api.markdown.MarkdownRenderer;
import com.condation.cms.api.theme.Theme;
import com.condation.cms.content.tags.Tags;
import com.condation.cms.content.shortcodes.ShortCodes;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -35,7 +35,7 @@
*/
@Slf4j
@FeatureScope(FeatureScope.Scope.REQUEST)
public record RenderContext(MarkdownRenderer markdownRenderer, Tags tags, Theme theme)
public record RenderContext(MarkdownRenderer markdownRenderer, ShortCodes shortCodes, Theme theme)
implements AutoCloseable, Feature {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,32 @@
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/


/**
*
* @author t.marx
*/
public interface Block {
int start();
int end();

String render (InlineRenderer inlineRenderer);

/**
* Renders this block. {@code documentOffset} is the absolute position of
* this block's start in the original document — passed through to the
* {@link InlineRenderer} so inline elements can compute absolute positions.
*/
String render(InlineRenderer inlineRenderer, int documentOffset);

default String render(InlineRenderer inlineRenderer) {
return render(inlineRenderer, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
Expand All @@ -28,7 +28,8 @@

/**
* Block-level markdown tokenizer with recursion depth limit.
* Optimized to prevent stack overflow on pathological inputs.
* Returns {@link LocatedBlock} instances whose {@code absoluteStart}/
* {@code absoluteEnd} are correct offsets into the original document string.
*
* @author t.marx
*/
Expand All @@ -38,34 +39,38 @@ public class BlockTokenizer {
private final Options options;
private static final int MAX_RECURSION_DEPTH = 100;

protected List<Block> tokenize(final String original_md) throws IOException {
return tokenizeWithDepth(original_md, 0);
protected List<LocatedBlock> tokenize(final String original_md) throws IOException {
return tokenizeWithDepth(original_md, 0, 0);
}

/**
* Tokenizes markdown with recursion depth tracking.
* Throws exception if depth exceeds limit to prevent stack overflow.
* @param original_md the markdown substring to tokenize
* @param documentOffset cumulative character offset of this substring in the full document
* @param depth current recursion depth
*/
private List<Block> tokenizeWithDepth(final String original_md, int depth) throws IOException {
private List<LocatedBlock> tokenizeWithDepth(final String original_md, int documentOffset, int depth) throws IOException {
if (depth > MAX_RECURSION_DEPTH) {
throw new IOException("Maximum recursion depth exceeded in markdown parsing");
}

var md = original_md.replaceAll("\r\n", "\n");
StringBuilder mdBuilder = new StringBuilder(md);
int offset = documentOffset;

final List<Block> blocks = new ArrayList<>();
final List<LocatedBlock> blocks = new ArrayList<>();

for (var blockRule : options.blockElementRules) {
Block block = null;
while ((block = blockRule.next(mdBuilder.toString())) != null) {

if (block.start() != 0) {
var before = mdBuilder.substring(0, block.start());
blocks.addAll(tokenizeWithDepth(before, depth + 1));
blocks.addAll(tokenizeWithDepth(before, offset, depth + 1));
offset += block.start();
}

blocks.add(block);
blocks.add(new LocatedBlock(block, offset, offset + (block.end() - block.start())));
offset += block.end() - block.start();
mdBuilder.delete(0, block.end());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import com.condation.cms.api.request.RequestContextScope;
import com.condation.cms.content.markdown.rules.block.ParagraphBlockRule;
import com.condation.cms.content.markdown.rules.inline.TextInlineRule;
import com.condation.cms.content.markdown.utils.StringUtils;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

/**
Expand All @@ -38,7 +36,6 @@
public class CMSMarkdown {

private final BlockTokenizer blockTokenizer;

private final InlineElementTokenizer inlineTokenizer;

private final List<BlockElementRule> blockRules;
Expand All @@ -47,22 +44,10 @@ public class CMSMarkdown {
private final boolean parallelRendering;
private final int parallelThreshold;

/**
* Creates a markdown renderer with default settings (parallel rendering
* enabled for 10+ blocks).
*/
public CMSMarkdown(Options options) {
this(options, true, 10);
}

/**
* Creates a markdown renderer with custom parallel rendering configuration.
*
* @param options markdown rendering options
* @param parallelRendering enable parallel block rendering
* @param parallelThreshold minimum number of blocks to trigger parallel
* rendering
*/
public CMSMarkdown(Options options, boolean parallelRendering, int parallelThreshold) {
this.blockTokenizer = new BlockTokenizer(options);
this.inlineTokenizer = new InlineElementTokenizer(options);
Expand All @@ -74,66 +59,53 @@ public CMSMarkdown(Options options, boolean parallelRendering, int parallelThres
inlineRules.addLast(new TextInlineRule());
}

private String renderInlineElements(final String inline_md) throws IOException {
List<InlineBlock> blocks = inlineTokenizer.tokenize(inline_md);
private String renderInlineElements(final String inline_md, int documentOffset) throws IOException {
List<LocatedInlineBlock> blocks = inlineTokenizer.tokenize(inline_md, documentOffset);

// Pre-size StringBuilder based on input length to reduce allocations
final StringBuilder htmlBuilder = new StringBuilder(inline_md.length() + 128);

// Use simple loop instead of streams for better performance
for (InlineBlock block : blocks) {
htmlBuilder.append(block.render());
for (LocatedInlineBlock located : blocks) {
htmlBuilder.append(located.block().render(located.absoluteStart(), located.absoluteEnd()));
}

return htmlBuilder.toString();
}

private String renderBlock(LocatedBlock located, InlineRenderer inlineRenderer, BlockRenderer blockRenderer) {
Block block = located.block();
if (block instanceof BlockContainer blockContainer) {
return blockContainer.render(blockRenderer);
}
return block.render(inlineRenderer, located.absoluteStart());
}

public String render(final String md) throws IOException {
// Escape input markdown
String escapedMd = StringUtils.escape(md);
List<Block> blocks = blockTokenizer.tokenize(escapedMd);
List<LocatedBlock> blocks = blockTokenizer.tokenize(md);

// Pre-size StringBuilder based on input to reduce allocations
final StringBuilder htmlBuilder = new StringBuilder(md.length() + 256);

// Create renderers once instead of as lambdas
InlineRenderer inlineRenderer = (content) -> {
InlineRenderer inlineRenderer = (content, documentOffset) -> {
try {
return renderInlineElements(content);
return renderInlineElements(content, documentOffset);
} catch (IOException ioe) {
// Log error but don't break rendering
return "";
}
};
BlockRenderer blockRenderer = (content) -> {
try {
return this.render(content);
} catch (IOException e) {
// Log error but don't break rendering
return "";
}
};

// Use parallel rendering for large documents (10+ blocks)
// For small documents, sequential is faster due to parallel overhead
if (parallelRendering && blocks.size() >= parallelThreshold) {
// Capture ScopedValue on the calling thread BEFORE entering the parallel stream.
// ForkJoinPool worker threads do not inherit ScopedValue bindings, so we must
// capture the context here and explicitly re-bind it inside each worker lambda.
final var capturedContext = RequestContextScope.REQUEST_CONTEXT.isBound()
? RequestContextScope.REQUEST_CONTEXT.get()
: null;

// Parallel rendering: 2-4x faster on multi-core CPUs
List<String> renderedBlocks = blocks.parallelStream()
.map(block -> {
final Supplier<String> renderBlockSupplier = () -> {
if (block instanceof BlockContainer blockContainer) {
return blockContainer.render(blockRenderer);
} else {
return block.render(inlineRenderer);
}
};
.map(located -> {
final Supplier<String> renderBlockSupplier = () ->
renderBlock(located, inlineRenderer, blockRenderer);
try {
if (capturedContext != null) {
return ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, capturedContext)
Expand All @@ -147,23 +119,15 @@ public String render(final String md) throws IOException {
})
.toList();

// Append in order (toList() preserves order)
for (String rendered : renderedBlocks) {
htmlBuilder.append(rendered);
}
} else {
// Sequential rendering for small documents
for (Block block : blocks) {
String rendered;
if (block instanceof BlockContainer blockContainer) {
rendered = blockContainer.render(blockRenderer);
} else {
rendered = block.render(inlineRenderer);
}
htmlBuilder.append(rendered);
for (LocatedBlock located : blocks) {
htmlBuilder.append(renderBlock(located, inlineRenderer, blockRenderer));
}
}

return StringUtils.unescape(htmlBuilder.toString());
return htmlBuilder.toString();
}
}
Loading