diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..cab8680 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,65 @@ +plugins { + java + application + id("io.freefair.lombok") version "6.6.1" +} + +group = "org.example" +version = "0.1.0-SNAPSHOT" + +val JUINT_VERSION = "5.8.2" + +repositories { + mavenLocal() + mavenCentral() + maven(url = "https://jitpack.io") +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + withSourcesJar() + withJavadocJar() +} + +java.sourceSets["main"].java { + srcDir("src/main/java") +} + +dependencies { + implementation("com.github.1c-syntax", "bsl-language-server", "0.20.0") + + implementation("com.github.1c-syntax", "mdclasses", "0.10.3") + implementation("io.github.1c-syntax", "bsl-common-library", "f6714e4e") + implementation("io.github.1c-syntax", "supportconf", "0.1.1") { + exclude("io.github.1c-syntax", "bsl-common-library") + } + implementation("com.github.1c-syntax", "bsl-parser", "167aaad827322e09ccde4658a71152dad234de4b") { + exclude("com.tunnelvisionlabs", "antlr4-annotations") + exclude("com.ibm.icu", "*") + exclude("org.antlr", "ST4") + exclude("org.abego.treelayout", "org.abego.treelayout.core") + exclude("org.antlr", "antlr-runtime") + exclude("org.glassfish", "javax.json") + } + + // template engine + implementation("com.github.jknack:handlebars:4.3.1") + implementation("org.apache.velocity:velocity-engine-core:2.3") + implementation("org.apache.velocity:velocity-tools:2.0") + + // cli + implementation("info.picocli:picocli:4.7.3") + + testImplementation("org.junit.jupiter:junit-jupiter:$JUINT_VERSION") + testImplementation("org.junit.jupiter:junit-jupiter-api:$JUINT_VERSION") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$JUINT_VERSION") +} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("passed", "skipped", "failed", "standard_error") + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..05679dc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..4d6d84d --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'bsldoc' + diff --git a/src/main/java/ru/alkoleft/bsl/doc/Main.java b/src/main/java/ru/alkoleft/bsl/doc/Main.java new file mode 100644 index 0000000..d0593a4 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/Main.java @@ -0,0 +1,11 @@ +package ru.alkoleft.bsl.doc; + +import picocli.CommandLine; +import ru.alkoleft.bsl.doc.commands.RenderCommand; + +public class Main { + + public static void main(String[] args) { + new CommandLine(new RenderCommand()).execute(args); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/BslContext.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/BslContext.java new file mode 100644 index 0000000..9d42d1d --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/BslContext.java @@ -0,0 +1,194 @@ +package ru.alkoleft.bsl.doc.bsl; + +import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.parser.BSLParser; +import com.github._1c_syntax.bsl.parser.BSLTokenizer; +import com.github._1c_syntax.bsl.types.MDOType; +import com.github._1c_syntax.mdclasses.Configuration; +import com.github._1c_syntax.mdclasses.mdo.MDCommonModule; +import org.antlr.v4.runtime.Token; +import ru.alkoleft.bsl.doc.bsl.symbols.MethodSymbol; +import ru.alkoleft.bsl.doc.bsl.symbols.MethodSymbolComputer; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BslContext { + Configuration configuration; + Filter filter; + + List modules = Collections.emptyList(); + + private static final Set VALID_TOKEN_TYPES_FOR_COMMENTS_SEARCH = Set.of( + BSLParser.LINE_COMMENT, + BSLParser.WHITE_SPACE, + BSLParser.AMPERSAND + ); + + public BslContext(Path path, Filter filter) { + this.filter = filter; + configuration = Configuration.create(path); + } + + public Stream getModules() { + return modules + .stream() + .map(this::buildModuleContext) + .filter(ModuleContext::isNotEmpty); + } + + public boolean contains(String name) { + return modules.stream().anyMatch(it -> it.getName().equalsIgnoreCase(name)); + } + + public void load() { + modules = configuration.getOrderedTopMDObjects().get(MDOType.COMMON_MODULE).stream() + .map(MDCommonModule.class::cast) + .map(this::buildModuleContext) + .collect(Collectors.toList()); + } + + private ModuleContext buildModuleContext(MDCommonModule module) { + var bslModules = module.getModules(); + var srcPath = Path.of(bslModules.get(0).getUri()); + List methods; + String description; + try { + var content = Files.readString(srcPath); + MethodSymbolComputer computer = new MethodSymbolComputer(); + var tokenizer = new BSLTokenizer(content); + methods = computer.compute(tokenizer); + description = computeModuleDescription(tokenizer); + } catch (Exception e) { + throw new RuntimeException(module.getMdoReference().getMdoRef() + ". Module parsing error", e); + } + + return ModuleContext.builder() + .owner(module) + .methods(methods) + .description(description) + .build(); + } + + private String computeModuleDescription(BSLTokenizer tokenizer) { + var token = tokenizer.getAst().getStart(); + + var isDescription = false; + List comments = null; + while (token != null && !isDescription) { + comments = getComments(tokenizer.getTokens(), token); + isDescription = !comments.isEmpty() && comments.stream().map(Token::getText).map(it -> it.substring(2)).map(String::trim).noneMatch(it -> it.contains("Copyright") || it.contains("Экспортные")); + token = comments.isEmpty() ? null : comments.get(0); + } + if (isDescription) { + var lines = comments.stream() + .map(Token::getText) + .map(it -> it.substring(2)) + .filter(it -> it.isEmpty() || !it.matches("/+")) + .collect(Collectors.toList()); + + var chars = lines.get(0).toCharArray(); + + for (int i = 1; i < lines.size(); i++) { + var line = lines.get(i); + + for (int j = 0; j < chars.length; j++) { + if (line.length() <= j || line.charAt(j) != chars[j]) { + chars = Arrays.copyOf(chars, j); + break; + } + } + if (chars.length == 0) { + break; + } + } + + if (chars.length > 0) { + char[] finalChars = chars; + return lines.stream() + .map(it -> it.substring(finalChars.length)) + .collect(Collectors.joining("\n")); + } else { + return String.join("\n", lines); + } + } else { + return ""; + } + } + + private ModuleContext buildModuleContext(ModuleContext module) { + + var stream = module.getMethods().stream(); + + if (filter.isExport()) { + stream = stream.filter(MethodSymbol::isExport); + } + + if (!filter.getRegions().isEmpty()) { + stream = stream.filter(this::regionFilter); + } + + return ModuleContext.builder() + .owner(module.getOwner()) + .methods(stream.collect(Collectors.toList())) + .description(module.getDescription()) + .build(); + } + + private boolean regionFilter(MethodSymbol m) { + var region = m.getRegion(); + + while (region != null) { + if (filter.getRegions().contains(region.getName())) { + return true; + } + region = region.getParent(); + } + return false; + } + + public static List getComments(List tokens, Token token) { + List comments = new ArrayList<>(); + fillCommentsCollection(tokens, token, comments); + return comments; + } + + private static void fillCommentsCollection(List tokens, Token currentToken, List lines) { + + int index = currentToken.getTokenIndex(); + + if (index == 0) { + return; + } + + var previousToken = tokens.get(index - 1); + + if (abortSearchComments(previousToken, currentToken)) { + return; + } + + fillCommentsCollection(tokens, previousToken, lines); + int type = previousToken.getType(); + if (type == BSLParser.LINE_COMMENT) { + lines.add(previousToken); + } + } + + private static boolean abortSearchComments(Token previousToken, Token currentToken) { + int type = previousToken.getType(); + return !VALID_TOKEN_TYPES_FOR_COMMENTS_SEARCH.contains(type) || isBlankLine(previousToken, currentToken); + } + + private static boolean isBlankLine(Token previousToken, Token currentToken) { + return previousToken.getType() == BSLParser.WHITE_SPACE + && (previousToken.getTokenIndex() == 0 + || (previousToken.getLine() + 1) != currentToken.getLine()); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/Filter.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/Filter.java new file mode 100644 index 0000000..8341efd --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/Filter.java @@ -0,0 +1,15 @@ +package ru.alkoleft.bsl.doc.bsl; + +import lombok.Builder; +import lombok.Singular; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class Filter { + boolean isExport; + @Singular + List regions; +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/ModuleContext.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/ModuleContext.java new file mode 100644 index 0000000..219270a --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/ModuleContext.java @@ -0,0 +1,25 @@ +package ru.alkoleft.bsl.doc.bsl; + +import com.github._1c_syntax.mdclasses.mdo.AbstractMDObjectBSL; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import ru.alkoleft.bsl.doc.bsl.symbols.MethodSymbol; + +import java.util.List; + +@Builder +@Value +public class ModuleContext { + AbstractMDObjectBSL owner; + List methods; + String description; + + public String getName() { + return owner.getName(); + } + + public boolean isNotEmpty() { + return methods != null && !methods.isEmpty(); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/DocumentationLink.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/DocumentationLink.java new file mode 100644 index 0000000..b5bd4e4 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/DocumentationLink.java @@ -0,0 +1,4 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +public class DocumentationLink { +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbol.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbol.java new file mode 100644 index 0000000..6e033cb --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbol.java @@ -0,0 +1,40 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription; +import com.google.common.base.Strings; +import lombok.Builder; +import lombok.Value; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Builder +@Value +public class MethodSymbol { + boolean deprecated; + boolean function; + boolean export; + String name; + Optional fullDescription; + List parameters; + RegionSymbol region; + + public String getDescription() { + return fullDescription.map(MethodDescription::getPurposeDescription).orElse(""); + } + + public List getExamples() { + return fullDescription.map(MethodDescription::getExamples).orElse(Collections.emptyList()); + } + + public TypeDescription getReturnedValue() { + var result = fullDescription.map(MethodDescription::getReturnedValue).filter(it -> !it.isEmpty()).map(it -> it.get(0)).orElse(null); + + if (result != null) { + return new TypeDescription(result.getName(), result.getDescription(), result.getParameters(), Strings.isNullOrEmpty(result.getLink()) ? null : "см. " + result.getLink(), result.isHyperlink()); + } else { + return null; + } + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbolComputer.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbolComputer.java new file mode 100644 index 0000000..e9e14ee --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/MethodSymbolComputer.java @@ -0,0 +1,180 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription; +import com.github._1c_syntax.bsl.languageserver.utils.Ranges; +import com.github._1c_syntax.bsl.languageserver.utils.Trees; +import com.github._1c_syntax.bsl.parser.BSLParser; +import com.github._1c_syntax.bsl.parser.BSLParserBaseVisitor; +import com.github._1c_syntax.bsl.parser.BSLTokenizer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.eclipse.lsp4j.Range; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Stack; +import java.util.stream.Collectors; + +public class MethodSymbolComputer extends BSLParserBaseVisitor { + private final List methods = new ArrayList<>(); + private BSLTokenizer tokenizer; + + Stack regions = new Stack<>(); + + public List compute(BSLTokenizer tokenizer) { + this.tokenizer = tokenizer; + methods.clear(); + visitFile(tokenizer.getAst()); + return methods; + } + + @Override + public ParseTree visitRegionStart(BSLParser.RegionStartContext ctx) { + var name = ctx.regionName().getText(); + + RegionSymbol region; + + if (regions.isEmpty()) { + region = new RegionSymbol(name, null); + } else { + region = new RegionSymbol(name, regions.peek()); + } + regions.push(region); + return super.visitRegionStart(ctx); + } + + @Override + public ParseTree visitRegionEnd(BSLParser.RegionEndContext ctx) { + regions.pop(); + return super.visitRegionEnd(ctx); + } + + @Override + public ParseTree visitFunction(BSLParser.FunctionContext ctx) { + BSLParser.FuncDeclarationContext declaration = ctx.funcDeclaration(); + + TerminalNode startNode = declaration.FUNCTION_KEYWORD(); + handleMethod(startNode, declaration.subName().getStart(), declaration.paramList(), true, declaration.EXPORT_KEYWORD() != null); + return super.visitFunction(ctx); + } + + @Override + public ParseTree visitProcedure(BSLParser.ProcedureContext ctx) { + BSLParser.ProcDeclarationContext declaration = ctx.procDeclaration(); + + TerminalNode startNode = declaration.PROCEDURE_KEYWORD(); + handleMethod(startNode, declaration.subName().getStart(), declaration.paramList(), false, declaration.EXPORT_KEYWORD() != null); + return super.visitProcedure(ctx); + } + + private void handleMethod(TerminalNode startNode, Token subName, BSLParser.ParamListContext paramList, boolean function, boolean export) { + Optional description = createDescription(startNode.getSymbol()); + boolean deprecated = description + .map(MethodDescription::isDeprecated) + .orElse(false); + + var method = MethodSymbol.builder() + .name(subName.getText().intern()) + .function(function) + .export(export) + .fullDescription(description) + .deprecated(deprecated) + .parameters(createParameters(paramList, description)) + .region(regions.peek()) + .build(); + methods.add(method); + } + + private static List createParameters(BSLParser.ParamListContext paramList, Optional description) { + if (paramList == null) { + return Collections.emptyList(); + } + + return paramList.param().stream() + .map((BSLParser.ParamContext param) -> { + String parameterName = getParameterName(param.IDENTIFIER()); + return ParameterDefinition.builder() + .name(parameterName) + .byValue(param.VAL_KEYWORD() != null) + .defaultValue(getDefaultValue(param)) + .description(getParameterDescription(parameterName, description)) + .build(); + }).collect(Collectors.toList()); + } + + private static ParameterDefinition.DefaultValue getDefaultValue(BSLParser.ParamContext param) { + if (param.defaultValue() == null) { + return ParameterDefinition.DefaultValue.EMPTY; + } + + var constValue = param.defaultValue().constValue(); + + ParameterDefinition.DefaultValue defaultValue; + if (constValue.DATETIME() != null) { + var value = constValue.DATETIME().getSymbol().getText(); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.DATETIME, value.intern()); + } else if (constValue.FALSE() != null) { + var value = constValue.FALSE().getSymbol().getText(); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.BOOLEAN, value.intern()); + } else if (constValue.TRUE() != null) { + var value = constValue.TRUE().getSymbol().getText(); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.BOOLEAN, value.intern()); + } else if (constValue.UNDEFINED() != null) { + var value = constValue.UNDEFINED().getSymbol().getText(); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.UNDEFINED, value.intern()); + } else if (constValue.NULL() != null) { + var value = constValue.NULL().getSymbol().getText(); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.NULL, value.intern()); + } else if (constValue.string() != null) { + var value = constValue.string().STRING().stream() + .map(TerminalNode::getSymbol) + .map(Token::getText) + .collect(Collectors.joining("\n")); + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.STRING, value.intern()); + } else if (constValue.numeric() != null) { + var value = constValue.numeric().getText(); + if (constValue.MINUS() != null) { + value = constValue.MINUS().getSymbol().getText() + value; + } + if (constValue.PLUS() != null) { + value = constValue.PLUS().getSymbol().getText() + value; + } + defaultValue = new ParameterDefinition.DefaultValue(ParameterDefinition.ParameterType.NUMERIC, value.intern()); + } else { + defaultValue = ParameterDefinition.DefaultValue.EMPTY; + } + + return defaultValue; + } + + private static String getParameterName(TerminalNode identifier) { + return Optional.ofNullable(identifier) + .map(ParseTree::getText) + .map(String::intern) + .orElse(""); + } + + private static Optional getParameterDescription(String parameterName, Optional description) { + + return description.map(MethodDescription::getParameters) + .stream() + .flatMap(Collection::stream) + .filter(parameterDescription -> parameterDescription.getName().equalsIgnoreCase(parameterName)) + .findFirst() + .map(p -> new ParameterDescription(p.getName(), p.getTypes(), p.getLink(), p.isHyperlink())); + + } + + private Optional createDescription(Token token) { + List comments = Trees.getComments(tokenizer.getTokens(), token); + if (comments.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(new MethodDescription(comments)); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDefinition.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDefinition.java new file mode 100644 index 0000000..61fe89d --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDefinition.java @@ -0,0 +1,56 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.TypeDescription; +import lombok.Builder; +import lombok.Value; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@Value +@Builder +public class ParameterDefinition { + String name; + Optional description; + String type; + DefaultValue defaultValue; + boolean byValue; + + public String getDescription() { + if (description.isEmpty()) { + return ""; + } + return description.get().getTypes() + .stream() + .findFirst() + .map(TypeDescription::getDescription).orElse(""); + } + + public List getTypes() { + if (description.isEmpty()) { + return Collections.emptyList(); + } else { + return description.get().getTypes(); + } + } + + public enum ParameterType { + DATETIME, + BOOLEAN, + UNDEFINED, + NULL, + STRING, + NUMERIC, + EMPTY + } + + @Value + public static class DefaultValue { + public static final DefaultValue EMPTY = new DefaultValue(ParameterType.EMPTY, ""); + + ParameterType type; + String value; + } + +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDescription.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDescription.java new file mode 100644 index 0000000..85c7e57 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/ParameterDescription.java @@ -0,0 +1,34 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.TypeDescription; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +/** + * Описание параметра из комментария - описания метода + */ +@AllArgsConstructor +@Value +public class ParameterDescription { + /** + * Имя параметра + */ + String name; + /** + * Возможные типы параметра. Может быть пустым + */ + List types; + /** + * Если описание параметров содержит только ссылку, то здесь будет ее значение + *

+ * TODO Временное решение, надо будет продумать в следующем релизе + */ + String link; + /** + * Признак того, что параметр является гиперссылкой + */ + boolean isHyperlink; + +} \ No newline at end of file diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/RegionSymbol.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/RegionSymbol.java new file mode 100644 index 0000000..a00668c --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/RegionSymbol.java @@ -0,0 +1,16 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import lombok.Value; + +@Value +public class RegionSymbol { + public static final String PUBLIC_REGION_RU = "ПрограммныйИнтерфейс"; + public static final String PUBLIC_REGION_EN = "Public"; + public static final String INTERNAL_REGION_RU = "СлужебныйПрограммныйИнтерфейс"; + public static final String INTERNAL_REGION_EN = "Internal"; + public static final String PRIVATE_REGION_RU = "СлужебныеПроцедурыИФункции"; + public static final String PRIVATE_REGION_EN = "Private"; + + String name; + RegionSymbol parent; +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/TypeDescription.java b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/TypeDescription.java new file mode 100644 index 0000000..f55df7c --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/bsl/symbols/TypeDescription.java @@ -0,0 +1,37 @@ +package ru.alkoleft.bsl.doc.bsl.symbols; + +import com.github._1c_syntax.bsl.languageserver.context.symbol.description.ParameterDescription; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; + +/** + * Описание типа параметра, прочитанного из описания метода + */ +@AllArgsConstructor +@Value +public class TypeDescription { + /** + * Имя типа. На данный момент может быть строковый массив перечисления типов а также гиперссылка на метод + */ + String name; + /** + * Описание типа. Может быть пустым + */ + String description; + /** + * Параметры (ключи или поля) типа для сложных типов данных. Может быть пустым + */ + List parameters; + /** + * Если описание параметров содержит только ссылку, то здесь будет ее значение + *

+ * TODO Временное решение, надо будет продумать в следующем релизе + */ + String link; + /** + * Признак того, что параметр является гиперссылкой + */ + boolean isHyperlink; +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/commands/RenderCommand.java b/src/main/java/ru/alkoleft/bsl/doc/commands/RenderCommand.java new file mode 100644 index 0000000..84d8b0f --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/commands/RenderCommand.java @@ -0,0 +1,56 @@ +package ru.alkoleft.bsl.doc.commands; + +import lombok.SneakyThrows; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import ru.alkoleft.bsl.doc.render.Render; +import ru.alkoleft.bsl.doc.bsl.BslContext; +import ru.alkoleft.bsl.doc.bsl.Filter; +import ru.alkoleft.bsl.doc.bsl.symbols.RegionSymbol; +import ru.alkoleft.bsl.doc.render.Factory; +import ru.alkoleft.bsl.doc.render.OutputFormat; +import ru.alkoleft.bsl.doc.render.RenderOptions; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@Command(helpCommand = true) +public class RenderCommand implements Runnable { + @Parameters(description = "source") + Path sources; + @Parameters(description = "destination") + Path destination; + + @Option(names = {"-f", "--format"}, defaultValue = "Markdown") + OutputFormat format; + @Option(names = {"-s", "--only-subsystems"}) + List onlySubsystems; + + @Option(names = {"-r", "--regions"}, split = " ", defaultValue = RegionSymbol.PUBLIC_REGION_RU + " " + RegionSymbol.PUBLIC_REGION_EN) + List regions; + + @SneakyThrows + @Override + public void run() { + + var filter = Filter.builder() + .isExport(true); + regions.forEach(filter::region); + + var options = RenderOptions.builder() + .outputFormat(format) + .subsystemHierarchy(true); + onlySubsystems.forEach(options::rootSubsystem); + + var renderContext = Factory.createRenderContext(options.build()); + var render = new Render(renderContext); + + BslContext bslContext = new BslContext(sources, filter.build()); + bslContext.load(); + + Files.createDirectories(destination); + render.render(bslContext, destination); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/Factory.java b/src/main/java/ru/alkoleft/bsl/doc/render/Factory.java new file mode 100644 index 0000000..3bf7b1c --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/Factory.java @@ -0,0 +1,20 @@ +package ru.alkoleft.bsl.doc.render; + +import lombok.experimental.UtilityClass; +import ru.alkoleft.bsl.doc.render.handlebars.HandlebarsRenderContext; +import ru.alkoleft.bsl.doc.render.velocity.VelocityRenderContext; + +@UtilityClass +public class Factory { + public Render createRender(RenderOptions options) { + return new Render(null); + } + + public RenderContext createRenderContext(RenderOptions options) { + return new HandlebarsRenderContext(options.getOutputFormat().getPath()); + } + + public RenderContext createVelocityRenderContext(RenderOptions options) { + return new VelocityRenderContext(options.getOutputFormat().getPath()); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/ItemRender.java b/src/main/java/ru/alkoleft/bsl/doc/render/ItemRender.java new file mode 100644 index 0000000..8c0ddde --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/ItemRender.java @@ -0,0 +1,10 @@ +package ru.alkoleft.bsl.doc.render; + +import java.io.IOException; +import java.nio.file.Path; + +public interface ItemRender { + void put(String key, Object value); + + void renderToFile(Path fileName) throws IOException; +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/OutputFormat.java b/src/main/java/ru/alkoleft/bsl/doc/render/OutputFormat.java new file mode 100644 index 0000000..0b5fd20 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/OutputFormat.java @@ -0,0 +1,15 @@ +package ru.alkoleft.bsl.doc.render; + +import lombok.Getter; + +public enum OutputFormat { + Docusaurus("docusaurus"), + Markdown("md"); + + @Getter + private final String path; + + OutputFormat(String path) { + this.path = path; + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/Render.java b/src/main/java/ru/alkoleft/bsl/doc/render/Render.java new file mode 100644 index 0000000..94b6a44 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/Render.java @@ -0,0 +1,46 @@ +package ru.alkoleft.bsl.doc.render; + +import com.github._1c_syntax.mdclasses.mdo.AbstractMDObjectBSL; +import lombok.SneakyThrows; +import ru.alkoleft.bsl.doc.bsl.BslContext; +import ru.alkoleft.bsl.doc.bsl.ModuleContext; +import ru.alkoleft.bsl.doc.render.RenderContext; + +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicInteger; + +public class Render { + + RenderContext renderContext; + + public Render(RenderContext renderContext) { + this.renderContext = renderContext; + } + + public void render(BslContext bslContext, Path output) { + AtomicInteger index = new AtomicInteger(); + + renderContext.setContext(bslContext); + bslContext.getModules().forEach(it -> renderModule(it, output, index.getAndIncrement())); + } + + @SneakyThrows + public void renderModule(ModuleContext module, Path outputPath, int index) { + var itemRender = renderContext.getRender("module"); + + itemRender.put("index", index); + itemRender.put("name", module.getOwner().getName()); + itemRender.put("present", getPresent(module.getOwner())); + itemRender.put("methods", module.getMethods()); + itemRender.put("description", module.getDescription()); + itemRender.renderToFile(outputPath.resolve(module.getOwner().getName() + ".md")); + } + + private String getPresent(AbstractMDObjectBSL object) { + if (object.getSynonyms().isEmpty()) { + return object.getName(); + } else { + return object.getSynonyms().get(0).getContent(); + } + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/RenderContext.java b/src/main/java/ru/alkoleft/bsl/doc/render/RenderContext.java new file mode 100644 index 0000000..81fe585 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/RenderContext.java @@ -0,0 +1,11 @@ +package ru.alkoleft.bsl.doc.render; + +import ru.alkoleft.bsl.doc.bsl.BslContext; + +import java.io.IOException; + +public interface RenderContext { + ItemRender getRender(String name) throws IOException; + + void setContext(BslContext context); +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/RenderOptions.java b/src/main/java/ru/alkoleft/bsl/doc/render/RenderOptions.java new file mode 100644 index 0000000..20f9758 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/RenderOptions.java @@ -0,0 +1,17 @@ +package ru.alkoleft.bsl.doc.render; + +import lombok.Builder; +import lombok.Singular; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class RenderOptions { + OutputFormat outputFormat; + + boolean subsystemHierarchy; + @Singular + List rootSubsystems; +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/StructureStrategy.java b/src/main/java/ru/alkoleft/bsl/doc/render/StructureStrategy.java new file mode 100644 index 0000000..553ab9a --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/StructureStrategy.java @@ -0,0 +1,64 @@ +package ru.alkoleft.bsl.doc.render; + +import com.github._1c_syntax.bsl.mdo.MD; +import com.github._1c_syntax.bsl.mdo.MDObject; +import com.github._1c_syntax.mdclasses.Configuration; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public abstract class StructureStrategy { + + protected Path path; + protected String extension; + + StructureStrategy(Path path, String extension) throws IOException { + this.path = path; + this.extension = extension; + + createIfNotExists(path); + } + + public Path getPath(MDObject object) throws IOException { + Path objectPath = path.resolve(getObjectPath(object)); + createIfNotExists(objectPath.getParent()); + return objectPath; + + } + + public Path getPath(MD object) { + return path.resolve(getFileName(object)); + } + + public String getLink(MDObject object) { + return "../" + getObjectPath(object); + } + + protected abstract String getObjectPath(MDObject object); + + public String getFileName(MD object) { + return String.format("%s.%s", object.getName(), extension); + } + + public String getFileName(Configuration object) { + return String.format("configuration.%s", extension); + } + + protected void createIfNotExists(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + static class Metadata extends StructureStrategy { + public Metadata(Path path, String extension) throws IOException { + super(path, extension); + } + + @Override + public String getObjectPath(MDObject object) { + return Path.of(object.getMdoType().getGroupName(), getFileName(object)).toString(); + } + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandleLinks.java b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandleLinks.java new file mode 100644 index 0000000..86a1585 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandleLinks.java @@ -0,0 +1,36 @@ +package ru.alkoleft.bsl.doc.render.handlebars; + +import com.github.jknack.handlebars.Helper; +import com.github.jknack.handlebars.Options; +import ru.alkoleft.bsl.doc.bsl.BslContext; + +import java.io.IOException; +import java.util.Locale; +import java.util.regex.Pattern; + +public class HandleLinks implements Helper { + Pattern pattern = Pattern.compile("см\\. (([\\wА-Яа-я\\.\\d]+)\\.)*([\\wА-Яа-я\\d]+)"); + + BslContext context; + + @Override + public Object apply(String context, Options options) throws IOException { + var matcher = pattern.matcher(context); + if (matcher.find()) { + return matcher.replaceAll(matchResult -> String.format("[%s](%s)", matchResult.group(0), getLink(matcher.group(2), matcher.group(3)))); + } + return context; + } + + private String getLink(String owner, String method) { + if (owner != null && method != null) { + return owner + "#" + method.toLowerCase(Locale.ROOT); + } else if (context.contains(method)) { + return method; + } else if (method != null) { + return "#" + method.toLowerCase(Locale.ROOT); + } else{ + return ""; + } + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarItemRender.java b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarItemRender.java new file mode 100644 index 0000000..80cc207 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarItemRender.java @@ -0,0 +1,33 @@ +package ru.alkoleft.bsl.doc.render.handlebars; + +import com.github.jknack.handlebars.Template; +import ru.alkoleft.bsl.doc.render.ItemRender; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +public class HandlebarItemRender implements ItemRender { + + private final Template template; + + HandlebarItemRender(Template template) { + this.template = template; + } + + Map context = new HashMap<>(); + + @Override + public void put(String key, Object value) { + context.put(key, value); + } + + @Override + public void renderToFile(Path fileName) throws IOException { + try (FileWriter writer = new FileWriter(fileName.toFile())) { + template.apply(context, writer); + } + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarsRenderContext.java b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarsRenderContext.java new file mode 100644 index 0000000..938c80c --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/HandlebarsRenderContext.java @@ -0,0 +1,45 @@ +package ru.alkoleft.bsl.doc.render.handlebars; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import ru.alkoleft.bsl.doc.bsl.BslContext; +import ru.alkoleft.bsl.doc.render.RenderContext; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class HandlebarsRenderContext implements RenderContext { + + private final String path; + private final Handlebars handlebars; + private final Map loadedTemplates = new HashMap<>(); + + private final HandleLinks linksRender; + + public HandlebarsRenderContext(String path) { + this.path = path; + handlebars = new Handlebars().with(value -> value); + handlebars.registerHelper("links", linksRender = new HandleLinks()); + handlebars.registerHelper("shift", new Shifter()); + } + + public void setContext(BslContext context) { + linksRender.context = context; + } + + @Override + public HandlebarItemRender getRender(String name) throws IOException { + return new HandlebarItemRender(getTemplate(name)); + } + + private Template getTemplate(String name) throws IOException { + if (loadedTemplates.containsKey(name)) { + return loadedTemplates.get(name); + } + var location = String.format("%s/%s", path, name); + var template = handlebars.compile(location); + loadedTemplates.put(name, template); + return template; + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/Shifter.java b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/Shifter.java new file mode 100644 index 0000000..703434b --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/handlebars/Shifter.java @@ -0,0 +1,26 @@ +package ru.alkoleft.bsl.doc.render.handlebars; + +import com.github.jknack.handlebars.Helper; +import com.github.jknack.handlebars.Options; + +import java.io.IOException; + +public class Shifter implements Helper { + private int shift = 0; + + @Override + public Object apply(Object context, Options options) throws IOException { + Options.Buffer buffer = options.buffer(); + int startedShift = shift; + shift++; + var content = options.fn(); + shift--; + if (startedShift > 0) { + var newChar = "\n" + new String(new char[startedShift]).replace('\0', '\t'); + buffer.append(content.toString().replace("\n", newChar)); + } else { + buffer.append(content); + } + return buffer; + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityItemRender.java b/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityItemRender.java new file mode 100644 index 0000000..b54d579 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityItemRender.java @@ -0,0 +1,42 @@ +package ru.alkoleft.bsl.doc.render.velocity; + +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import ru.alkoleft.bsl.doc.render.ItemRender; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.nio.file.Path; + +public class VelocityItemRender implements ItemRender { + private final Template template; + + VelocityItemRender(Template template) { + this.template = template; + } + + VelocityContext context = new VelocityContext(); + + public void put(String key, Object value) { + context.put(key, value); + } + + public void renderToFile(Path fileName) throws IOException { + try (FileWriter writer = new FileWriter(fileName.toFile())) { + template.merge(context, writer); + } + } + + public String renderToString() { + StringWriter writer = new StringWriter(); + template.merge(context, writer); + return writer.toString(); + } + + public void renderToConsole() { + template.merge(context, new BufferedWriter(new OutputStreamWriter(System.out))); + } +} diff --git a/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityRenderContext.java b/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityRenderContext.java new file mode 100644 index 0000000..2bfafb6 --- /dev/null +++ b/src/main/java/ru/alkoleft/bsl/doc/render/velocity/VelocityRenderContext.java @@ -0,0 +1,44 @@ +package ru.alkoleft.bsl.doc.render.velocity; + +import org.apache.velocity.Template; +import org.apache.velocity.app.VelocityEngine; +import ru.alkoleft.bsl.doc.bsl.BslContext; +import ru.alkoleft.bsl.doc.render.ItemRender; +import ru.alkoleft.bsl.doc.render.RenderContext; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.util.Properties; + +public class VelocityRenderContext implements RenderContext { + VelocityEngine velocityEngine; + String path; + + public VelocityRenderContext(String name) { + path = name; + + velocityEngine = new VelocityEngine(); + Properties properties = new Properties(); + properties.setProperty("resource.loaders", "file"); +// properties.setProperty("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.AvalonLogChute"); + properties.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + velocityEngine.init(properties); + velocityEngine.init(); + } + + public Template getTemplate(String name) { + return velocityEngine.getTemplate(String.format("%s/%s.vm", path, name)); + } + + @Override + public ItemRender getRender(String name) throws IOException { + return new VelocityItemRender(getTemplate(name)); + } + + @Override + public void setContext(BslContext context) { + + } +} diff --git a/src/main/resources/docusaurus/module.hbs b/src/main/resources/docusaurus/module.hbs new file mode 100644 index 0000000..9ed483f --- /dev/null +++ b/src/main/resources/docusaurus/module.hbs @@ -0,0 +1,51 @@ +--- +sidebar_position: {{index}} +title: {{name}} +--- + +# {{present}} ({{name}}) + +{{~#if description}} + +{{links description}} + +{{~/if}} + +## Методы модуля + +{{~#each methods}} + +--- +### `{{name}}` + +{{#if description}}{{links description}}{{/if}} +{{~#if parameters}} + +**Параметры метода** +{{#each parameters as | parameter|}} +* `{{parameter.name}}` + {{~#each parameter.types as | type typeIndex|}} + * `{{type.name}}` - {{links type.description}} + {{~else}} - {{links description}} + {{~/each}} +{{~/each}} + {{~/if}} +{{~#if returnedValue}} + +**Возвращает** + +{{#with returnedValue}}{{#block "docusaurus/value-definition"}}{{/block}}{{~/with}} +{{~/if}} + +{{~#if examples}} + +Примеры: + +```bsl +{{~#each examples}} +{{this}} +{{~/each}} +``` + +{{/if}} +{{~/each}} diff --git a/src/main/resources/docusaurus/value-definition.hbs b/src/main/resources/docusaurus/value-definition.hbs new file mode 100644 index 0000000..793e2c4 --- /dev/null +++ b/src/main/resources/docusaurus/value-definition.hbs @@ -0,0 +1,17 @@ +{{~#if link~}} +{{links link}} +{{~/if}} +{{~#if parameters}}
{{description}} ({{name}}) +{{~else}}`{{name}}` - {{links description}} +{{~/if}} +{{~#if parameters}} +{{/if}} + +{{~#each parameters}} +{{#shift}}* `{{name}}` - {{#each types~}}{{#block "docusaurus/value-definition"}}{{/block}}{{~/each}}{{/shift}} +{{~/each}} + +{{~#if parameters}} + +
+{{/if}} \ No newline at end of file diff --git a/src/main/resources/md/module.hbs b/src/main/resources/md/module.hbs new file mode 100644 index 0000000..7daf3c6 --- /dev/null +++ b/src/main/resources/md/module.hbs @@ -0,0 +1,35 @@ +# {{present}} ({{name}}) + +## Методы модуля + +{{~#each methods}} + +### `{{name}}({{#each parameters}}{{#if @index}}, {{/if}}{{name}}{{/each}})` + +{{#if description}}{{description}}{{/if}} + {{~#if parameters}} + +Параметры метода: + +| Имя | Тип | Описание | +|-----|-----|---------| +{{~#each parameters as | parameter|}} + {{~#each parameter.types as | type typeIndex|}} +| {{#unless typeIndex}}`{{parameter.name}}`{{/unless}} | `{{type.name}}` | {{type.description}} | + {{~else}} +| {{name}} | | {{description}} | + {{~/each}} +{{~/each}} + {{~/if}} +{{~#if returnedValue}} + +Возвращает: + +{{#if returnedValue.link}} +[{{returnedValue.link}}]({{returnedValue.link}}) + + {{~else}} +`{{returnedValue.name}}` - {{returnedValue.description}} + {{~/if}} +{{~/if}} +{{~/each}} diff --git a/src/main/resources/md/module.vm b/src/main/resources/md/module.vm new file mode 100644 index 0000000..3506296 --- /dev/null +++ b/src/main/resources/md/module.vm @@ -0,0 +1,18 @@ +#set( $H = '#' ) +$H $present ($name) + +$H$H Методы модуля +#foreach( $method in $methods) + +$H$H$H $method.name + +$method.description +#if (!$method.parameters.empty) + +**Параметры метода** +#foreach( $parameter in $method.parameters) + +* $parameter.name - $parameter.description +#end +#end +#end \ No newline at end of file