Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.testingisdocumenting.znai.extensions.ocaml;

import org.testingisdocumenting.znai.core.ComponentsRegistry;
import org.testingisdocumenting.znai.parser.MarkupParser;
import org.testingisdocumenting.znai.parser.MarkupParserResult;
import org.testingisdocumenting.znai.utils.RegexpUtils;
import org.testingisdocumenting.znai.utils.StringUtils;

import java.nio.file.Path;
import java.util.regex.Pattern;

class OcamlCommentExtractor {
private final String[] lines;
private final String content;

OcamlCommentExtractor(String content) {
this.content = content;
this.lines = content.split("\n");
}

String extractCommentBlock(String textToMatch) {
int idx = findIdxWithMatch(textToMatch);
int startBlockIdx = startCommentBlockIdx(idx);
int endBlockIdx = endCommentBlockIdx(startBlockIdx);

return removeCommentPrefixAndSuffix(extractBlock(startBlockIdx, endBlockIdx));
}

/**
* Converts an OCaml comment block to a list of DocElements.
* Handles OCaml-specific doc syntax like `[code]` and `{[ multi-line code ]}`.
*
* @param componentsRegistry registry containing the markdown parser
* @param filePath path to the OCaml file (for parser context)
* @param textToMatch text to find within a comment block
* @return list of DocElements representing the parsed comment
*/
MarkupParserResult extractCommentBlockAsDocElements(ComponentsRegistry componentsRegistry, Path filePath, String textToMatch) {
String commentText = extractCommentBlock(textToMatch);
String processedText = processOcamlDocSyntax(commentText);

MarkupParser parser = componentsRegistry.defaultParser();
return parser.parse(filePath, processedText);
}

private String removeCommentPrefixAndSuffix(String commentBlock) {
String trimmed = commentBlock.trim();

// Remove opening delimiter - either (** or (*
int startIndex = trimmed.startsWith("(**") ? 3 : 2;

// Remove closing delimiter - always *)
String withoutDelimiters = trimmed.substring(startIndex, trimmed.length() - 2);

return withoutDelimiters.trim();
}

private String extractBlock(int startBlockIdx, int endBlockIdx) {
StringBuilder result = new StringBuilder();
for (int i = startBlockIdx; i <= endBlockIdx; i++) {
result.append(lines[i]);
if (i < endBlockIdx) {
result.append("\n");
}
}

return result.toString();
}

private int startCommentBlockIdx(int idx) {
for (; idx >= 0; idx--) {
if (lines[idx].contains("(*")) {
return idx;
}
}

throw new IllegalArgumentException("can't find comment block start, starting idx: " + idx + contentPartForException());
}

private int endCommentBlockIdx(int idx) {
for (; idx < lines.length; idx++) {
if (lines[idx].contains("*)")) {
return idx;
}
}

throw new IllegalArgumentException("can't find comment block end, starting idx: " + idx + contentPartForException());
}

int findIdxWithMatch(String textToMatch) {
for (int idx = 0; idx < lines.length; idx++) {
if (lines[idx].contains(textToMatch)) {
return idx;
}
}

throw new IllegalArgumentException("can't find text: " + textToMatch + contentPartForException());
}

private String contentPartForException() {
return ", content:\n" + content;
}

/**
* Processes OCaml-specific documentation syntax and converts it to markdown.
* Converts:
* - `[code]` to `code`
* - `{[ multi-line code ]}` to ``` code blocks
*/
String processOcamlDocSyntax(String text) {
Pattern multiLineCodePattern = Pattern.compile("\\{\\[([^}]*?)]}", Pattern.DOTALL);
String afterMultiLine = RegexpUtils.replaceAll(text, multiLineCodePattern,
matcher -> {
String codeContent = matcher.group(1);
// Remove leading and trailing newlines but preserve internal spacing
codeContent = codeContent.replaceAll("^\\s*\\n", "").replaceAll("\\n\\s*$", "");
codeContent = StringUtils.stripIndentation(codeContent);
return "\n```\n" + codeContent + "\n```";
});

// Then handle inline code: [code]
// But avoid processing content inside code blocks (between ``` markers)
String[] parts = afterMultiLine.split("```", -1);
StringBuilder finalResult = new StringBuilder();

Pattern inlineCodePattern = Pattern.compile("\\[([^]]+?)]");

// this is quite hacky, be sure to change the approach when more OCaml doc elements
// need to be handled
for (int i = 0; i < parts.length; i++) {
if (i % 2 == 0) {
// Outside code block - process inline code
final String currentPart = parts[i];
String part = RegexpUtils.replaceAll(currentPart, inlineCodePattern, matcher -> {
String code = matcher.group(1);
return "`" + code + "`";
});
finalResult.append(part);
} else {
// Inside code block - don't process
finalResult.append(parts[i]);
}

if (i < parts.length - 1) {
finalResult.append("```");
}
}

return finalResult.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2025 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.testingisdocumenting.znai.extensions.ocaml;

import org.testingisdocumenting.znai.core.AuxiliaryFile;
import org.testingisdocumenting.znai.core.ComponentsRegistry;
import org.testingisdocumenting.znai.extensions.*;
import org.testingisdocumenting.znai.extensions.include.IncludePlugin;
import org.testingisdocumenting.znai.parser.MarkupParserResult;
import org.testingisdocumenting.znai.parser.ParserHandler;
import org.testingisdocumenting.znai.search.SearchScore;
import org.testingisdocumenting.znai.search.SearchText;

import java.nio.file.Path;
import java.util.stream.Stream;

public class OcamlCommentIncludePlugin implements IncludePlugin {
protected static final String COMMENT_LINE_KEY = "commentLine";
private Path ocamlPath;
private MarkupParserResult parserResult;

@Override
public String id() {
return "ocaml-comment";
}

@Override
public IncludePlugin create() {
return new OcamlCommentIncludePlugin();
}

@Override
public PluginParamsDefinition parameters() {
PluginParamsDefinition params = new PluginParamsDefinition();
params.add(COMMENT_LINE_KEY, PluginParamType.STRING, "text within a comment to match and identify the ocaml comment block", "\"to use this function\"");

return params;
}

@Override
public PluginResult process(ComponentsRegistry componentsRegistry,
ParserHandler parserHandler,
Path markupPath,
PluginParams pluginParams) {
String fileName = pluginParams.getFreeParam();
ocamlPath = componentsRegistry.resourceResolver().fullPath(fileName);
String text = componentsRegistry.resourceResolver().textContent(fileName);
String commentLine = pluginParams.getOpts().getRequiredString(COMMENT_LINE_KEY);

parserResult = new OcamlCommentExtractor(text).extractCommentBlockAsDocElements(componentsRegistry, markupPath, commentLine);
return PluginResult.docElements(parserResult.docElement().getContent().stream());
}

@Override
public SearchText textForSearch() {
return SearchScore.STANDARD.text(parserResult.getAllText());
}

@Override
public Stream<AuxiliaryFile> auxiliaryFiles(ComponentsRegistry componentsRegistry) {
return Stream.of(AuxiliaryFile.builtTime(ocamlPath));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ org.testingisdocumenting.znai.extensions.meta.MetaIncludePlugin
org.testingisdocumenting.znai.extensions.api.ApiParametersIncludePlugin
org.testingisdocumenting.znai.extensions.toc.PageTocIncludePlugin
org.testingisdocumenting.znai.extensions.json.JsonIncludePlugin
org.testingisdocumenting.znai.extensions.ocaml.OcamlCommentIncludePlugin
Loading