diff --git a/.gitignore b/.gitignore index 13b5e422a7a..b7b163b6bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ # include sourcecode except generated !src/ src/generated +out +build +!/libs/ +!/guidebook # include git important files !.gitmodules diff --git a/build.gradle b/build.gradle index 8935ee19fc5..0aff0c00064 100644 --- a/build.gradle +++ b/build.gradle @@ -25,56 +25,17 @@ plugins { id 'io.github.juuxel.loom-quiltflower' version '1.7.1' } -repositories { - mavenLocal() - mavenCentral() - maven { - url "https://maven.shedaniel.me/" - content { - includeGroup "me.shedaniel" - includeGroup "me.shedaniel.cloth" - includeGroup "dev.architectury" - } - } - maven { - url "https://maven.bai.lol" - content { - includeGroup "mcp.mobius.waila" - includeGroup "lol.bai" - } - } - maven { - url "https://maven.parchmentmc.net/" - content { - includeGroup "org.parchmentmc.data" - } - } - // For the "No Indium?" mod - maven { - url = 'https://maven.cafeteria.dev/releases/' - content { - includeGroup "me.luligabi" - } - } - - maven { - name 'modmaven' - url "https://modmaven.dev/" - content { - includeGroup "mezz.jei" - } - } - - maven { - name 'cursemaven' - url "https://www.cursemaven.com" - content { - includeGroup "curse.maven" - } - } +configurations { + includeInJar + portaforgyImplementation.extendsFrom(compileClasspath) + implementation.extendsFrom(includeInJar) } dependencies { + includeInJar project(path: ':libs:markdown', configuration: "archives") + includeInJar project(path: ':libs:shaded-snakeyaml', configuration: "shadow") + includeInJar project(path: ':libs:shaded-directory-watcher', configuration: "shadow") + // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings loom.layered() { @@ -84,14 +45,22 @@ dependencies { modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + // Always depend on the REI API to compile + modCompileOnly("me.shedaniel:RoughlyEnoughItems-api-fabric:${project.rei_version}") { + exclude group: "net.fabricmc.fabric-api" + exclude group: "org.yaml" // snakeyaml + exclude group: "blue.endless" // jankson + } + modCompileOnly("me.shedaniel:RoughlyEnoughItems-default-plugin-fabric:${project.rei_version}") { + exclude group: "net.fabricmc.fabric-api" + exclude group: "org.yaml" // snakeyaml + exclude group: "blue.endless" // jankson + } + if (project.runtime_itemlist_mod == "jei") { modLocalRuntime modCompileOnly("mezz.jei:jei-${jei_minecraft_version}-fabric:${jei_version}") { exclude group: "mezz.jei" } - - modCompileOnly("me.shedaniel:RoughlyEnoughItems-fabric:${project.rei_version}") { - exclude group: "net.fabricmc.fabric-api" - } } else if (project.runtime_itemlist_mod == "rei") { modCompileOnly("mezz.jei:jei-${jei_minecraft_version}-fabric:${jei_version}") { exclude group: "mezz.jei" @@ -146,24 +115,82 @@ dependencies { testImplementation("com.google.guava:guava-testlib:21.0") testImplementation("org.mockito:mockito-junit-jupiter:4.0.0") testImplementation("org.mockito:mockito-inline:4.0.0") - } -group = artifact_group archivesBaseName = artifact_basename -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) +allprojects { + group = artifact_group + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } -} -// ensure everything uses UTF-8 and not some random codepage chosen by gradle -compileJava.options.encoding = 'UTF-8' -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' - options.deprecation = false - options.compilerArgs << "-Xmaxerrs" << "9999" + repositories { + mavenLocal() + mavenCentral() + maven { + url "https://maven.shedaniel.me/" + content { + includeGroup "me.shedaniel" + includeGroup "me.shedaniel.cloth" + includeGroup "dev.architectury" + } + } + maven { + url "https://maven.bai.lol" + content { + includeGroup "mcp.mobius.waila" + includeGroup "lol.bai" + } + } + maven { + url "https://maven.parchmentmc.net/" + content { + includeGroup "org.parchmentmc.data" + } + } + // For the "No Indium?" mod + maven { + url = 'https://maven.cafeteria.dev/releases/' + content { + includeGroup "me.luligabi" + } + } + + maven { + name 'modmaven' + url "https://modmaven.dev/" + content { + includeGroup "mezz.jei" + } + } + + maven { + name 'cursemaven' + url "https://www.cursemaven.com" + content { + includeGroup "curse.maven" + } + } + + maven { + url "https://maven.blamejared.com" + content { + includeGroup "vazkii.patchouli" + } + } + } + + // ensure everything uses UTF-8 and not some random codepage chosen by gradle + compileJava.options.encoding = 'UTF-8' + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.deprecation = false + options.compilerArgs << "-Xmaxerrs" << "9999" + } } /////////////////// @@ -217,10 +244,6 @@ sourceSets { crowdin } -configurations { - portaforgyImplementation.extendsFrom(compileClasspath) -} - test { useJUnitPlatform() } @@ -239,6 +262,14 @@ loom { client { programArgs "--username", "AE2Dev" property "appeng.tests", "true" + property "appeng.guide-dev.sources", file("guidebook").absolutePath + } + guide { + client() + programArgs "--username", "AE2Dev" + property "appeng.tests", "true" + property "appeng.guide-dev.sources", file("guidebook").absolutePath + property "appeng.guide-dev.startup-page", "ae2:index.md" } datagen { client() @@ -282,6 +313,12 @@ jar { from sourceSets.main.output.classesDirs from sourceSets.main.output.resourcesDir + from configurations.includeInJar.collect { zipTree it } + + from('guidebook') { + into 'assets/ae2/ae2guide' + } + manifest { attributes([ "Specification-Title" : "Applied Energistics 2", diff --git a/gradle.properties b/gradle.properties index 05f3a4f0dd0..7212ee88334 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,12 +27,18 @@ no_indium_version=1.1.0+1.19 # Set to rei or jei to pick which tooltip mod gets picked at runtime # for the dev environment. -runtime_itemlist_mod=jei +runtime_itemlist_mod=rei # Set to wthit or jade to pick which tooltip mod gets picked at runtime # for the dev environment. runtime_tooltip_mod=jade +######################################################### +# Third party dependencies +######################################################### +snakeyaml_version=1.33 +directory_watcher_version=0.17.1 + ######################################################### # Deployment # ######################################################### diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023e..943f0cbfa75 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fbce071a31a..f398c33c4b0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c811..65dcd68d65c 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # 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 +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # 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" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # 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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +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 @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -106,80 +140,105 @@ 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 +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac 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 +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # 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 +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # 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\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg 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; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# 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" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c4e..93e3f59f135 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 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 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/guidebook.md b/guidebook.md new file mode 100644 index 00000000000..770e2dc5b27 --- /dev/null +++ b/guidebook.md @@ -0,0 +1,173 @@ +# Contributing to the Guidebook + +The guidebook is written in Markdown. You can find the files in [the guidebook folder](./guidebook). + +To contribute, you need to: + +* Install [Java Development Kit 17](https://www.microsoft.com/openjdk) +* Set the JAVA_HOME environment variable to where you installed OpenJDK +* Install [Git](https://git-scm.com/download/win) +* Check out this repository +* Run `gradlew runGuide` (to directly jump into the guidebook) or `gradlew runClient` (to jump in-game) + +**When you edit and save a file in the guidebook folder, the guidebook will automatically reload in-game.** + +## Authoring Pages + +Pages are written in Markdown and follow the [Commonmark](https://commonmark.org/) specification. +We also support [Github Tables](https://github.github.com/gfm/#tables-extension-). + +Every page should usually declare its title as a level 1 heading at the start (`# Page Title`). + +### Frontmatter + +Every page can have a header ("frontmatter") that defines metadata for the page in YAML format. + +Example: + +```yaml +--- +navigation: + title: Page Title +--- + +# Page Title + +Content +``` + +### Adding Pages to the Navigation Bar + +To include a page in the navigation sidebar, it needs to define the `navigation` key in its frontmatter as such: + +```yaml +--- +navigation: + # Title shown in the navigation bar + title: Page Title + # [OPTIONAL] Item ID for an icon + # defaults to the same namespace as the pages, so ae2 in our guidebook + icon: debug_card + # [OPTIONAL] The page ID of the parent this page should be sorted under as a child entry + # If it's in the same namespace as the current page, the namespace can be omitted, otherwise use "ae2:path/to/file.md" + parent: getting-started.md +--- +``` + +### Declaring Pages as ItemLink targets + +When using the `` tag, the guidebook will try to find the page that explains what the given item does. + +For this it searches all pages for the `item_ids` frontmatter key. If a page you write should be the primary page +for an item, list it in the `item_ids` frontmatter as such: + +```yaml +--- +item_ids: + - ae2:item_id + - ae2:other_item_id +--- +``` + +Using `` or `` will then link to this page, as will slots +in recipes that show that item. + +### Using Images + +To show an image, just put it (.png or .jpg) in the `guidebook/assets` folder and embed it either: + +* Using a normal Markdown image +* Using `` to have text wrap around the image. + Use align="left" to wrap text on the right and align="right" to wrap text on the left of the image. + To insert a break that prevents further text from wrapping from all previous floating images, + use `
`. + +### Custom Tags + +The following custom tags are supported in our Markdown pages. + +In all custom tags, item and page ids by default inherit the namespace of the page they're on. So if the +page is in AE2s guidebook, all ids automatically use the `ae2` namespace, unless specified. + +#### Item Links + +To automatically show the translated item name, including an appropriate tooltip, and have the item name link to the +primary guidebook page for that item, use the `` tag. The id can omit the `ae2` namespace. + +[Pages need to be set as the primary target for certain item ids manually](#declaring-pages-as-itemlink-targets). + +#### Recipes + +To show the recipes used to create a certain item, use the `` tag. + +#### Item Grids + +To show-case multiple related items in a grid-layout, use the following markup: + +```markdown + + + + +``` + +#### Category Index + +Pages can further be assigned to be part of multiple categories (orthogonal to the navigation bar). + +To do so, specify the following frontmatter key: + +```yaml +--- +categories: + - Category 1 + - Category 2 + - Category 3 +--- +``` + +A category can contain an unlimited number of pages. + +To automatically show a table of contents for a category, use the `` tag, +and specify the name of the category. It will then display a list of all pages that declare to be part of that +category. + +## For Addon Authors + +The guidebook will automatically load all pages that are in the `ae2guide` subfolder of all resource packs across +all namespaces (yes your addon mod id too). + +AE2 will merge your pages into the navigation tree as if they were within AE2 itself. + +If you want to develop the guidebook in your development environment where AE2 is only included as a dependency, +you can do so by passing certain system properties to the game. For an example, you can see AE2s +own [build.gradle](./build.gradle). + +For the standard client run-configuration you should include: + +```groovy +property "appeng.guide-dev.sources", file("guidebook").absolutePath +property "appeng.guide-dev.sources.namespace", "your-mod-id" +``` + +This will load the `guidebook` folder as if it was included in the resource-pack of your mod under the `ae2guide` +folder. +It will also automatically reload any pages that are changed in this folder, while the game is running. + +To automatically show the guidebook after launching the game, you can also set the `appeng.guide-dev.startup-page` +system property to the page that should be shown on startup. + +You can use this for a separate `runGuide` run configuration: + +```groovy +loom { + runs { + guide { + client() + property "appeng.guide-dev.sources", file("guidebook").absolutePath + property "appeng.guide-dev.sources.namespace", "your-mod-id" + property "appeng.guide-dev.startup-page", "your-mod-id:start-page.md" // or ae2:index.md + } + } +} +``` diff --git a/guidebook/api.md b/guidebook/api.md new file mode 100644 index 00000000000..31ebf4382ec --- /dev/null +++ b/guidebook/api.md @@ -0,0 +1,4 @@ +During the build process, this file will be overwritten by `API.md` from the `master` branch of the +main repository. + +See `get-releases.mjs` and and the source here: https://github.com/AppliedEnergistics/Applied-Energistics-2/blob/master/API.md diff --git a/guidebook/assets/large/VibrantQuartzGlassAni.gif b/guidebook/assets/large/VibrantQuartzGlassAni.gif new file mode 100644 index 00000000000..cb52fdfcc22 Binary files /dev/null and b/guidebook/assets/large/VibrantQuartzGlassAni.gif differ diff --git a/guidebook/assets/large/bright_illuminated_panel.png b/guidebook/assets/large/bright_illuminated_panel.png new file mode 100644 index 00000000000..1343aac05c9 Binary files /dev/null and b/guidebook/assets/large/bright_illuminated_panel.png differ diff --git a/guidebook/assets/large/cable_anchor.png b/guidebook/assets/large/cable_anchor.png new file mode 100644 index 00000000000..85c05eeebc7 Binary files /dev/null and b/guidebook/assets/large/cable_anchor.png differ diff --git a/guidebook/assets/large/cell_workbench.png b/guidebook/assets/large/cell_workbench.png new file mode 100644 index 00000000000..d97d2927e93 Binary files /dev/null and b/guidebook/assets/large/cell_workbench.png differ diff --git a/guidebook/assets/large/certus_ore.png b/guidebook/assets/large/certus_ore.png new file mode 100644 index 00000000000..86c87136cbc Binary files /dev/null and b/guidebook/assets/large/certus_ore.png differ diff --git a/guidebook/assets/large/certus_quartz_block.png b/guidebook/assets/large/certus_quartz_block.png new file mode 100644 index 00000000000..99b81e7114e Binary files /dev/null and b/guidebook/assets/large/certus_quartz_block.png differ diff --git a/guidebook/assets/large/certus_quartz_pillar.png b/guidebook/assets/large/certus_quartz_pillar.png new file mode 100644 index 00000000000..7459a82ebb1 Binary files /dev/null and b/guidebook/assets/large/certus_quartz_pillar.png differ diff --git a/guidebook/assets/large/charged_certus_ore.png b/guidebook/assets/large/charged_certus_ore.png new file mode 100644 index 00000000000..db2a4ae15af Binary files /dev/null and b/guidebook/assets/large/charged_certus_ore.png differ diff --git a/guidebook/assets/large/charged_quartz_fixture1.png b/guidebook/assets/large/charged_quartz_fixture1.png new file mode 100644 index 00000000000..5c1161828e9 Binary files /dev/null and b/guidebook/assets/large/charged_quartz_fixture1.png differ diff --git a/guidebook/assets/large/charged_quartz_fixture3.png b/guidebook/assets/large/charged_quartz_fixture3.png new file mode 100644 index 00000000000..4499589fe6b Binary files /dev/null and b/guidebook/assets/large/charged_quartz_fixture3.png differ diff --git a/guidebook/assets/large/charger.png b/guidebook/assets/large/charger.png new file mode 100644 index 00000000000..2b5cf095dfb Binary files /dev/null and b/guidebook/assets/large/charger.png differ diff --git a/guidebook/assets/large/charger_with_crank.jpg b/guidebook/assets/large/charger_with_crank.jpg new file mode 100644 index 00000000000..d3ee0d8db3c Binary files /dev/null and b/guidebook/assets/large/charger_with_crank.jpg differ diff --git a/guidebook/assets/large/chisled_certus_quartz.png b/guidebook/assets/large/chisled_certus_quartz.png new file mode 100644 index 00000000000..574e491f7f2 Binary files /dev/null and b/guidebook/assets/large/chisled_certus_quartz.png differ diff --git a/guidebook/assets/large/controller.png b/guidebook/assets/large/controller.png new file mode 100644 index 00000000000..e773ceef2c9 Binary files /dev/null and b/guidebook/assets/large/controller.png differ diff --git a/guidebook/assets/large/covered_cable.png b/guidebook/assets/large/covered_cable.png new file mode 100644 index 00000000000..9a3b5c71020 Binary files /dev/null and b/guidebook/assets/large/covered_cable.png differ diff --git a/guidebook/assets/large/crafting16k.png b/guidebook/assets/large/crafting16k.png new file mode 100644 index 00000000000..1d800b3e16c Binary files /dev/null and b/guidebook/assets/large/crafting16k.png differ diff --git a/guidebook/assets/large/crafting1k.png b/guidebook/assets/large/crafting1k.png new file mode 100644 index 00000000000..8937be3857a Binary files /dev/null and b/guidebook/assets/large/crafting1k.png differ diff --git a/guidebook/assets/large/crafting4k.png b/guidebook/assets/large/crafting4k.png new file mode 100644 index 00000000000..5212586b807 Binary files /dev/null and b/guidebook/assets/large/crafting4k.png differ diff --git a/guidebook/assets/large/crafting64k.png b/guidebook/assets/large/crafting64k.png new file mode 100644 index 00000000000..80506fef51a Binary files /dev/null and b/guidebook/assets/large/crafting64k.png differ diff --git a/guidebook/assets/large/crafting_terminal.png b/guidebook/assets/large/crafting_terminal.png new file mode 100644 index 00000000000..98a72f2ac47 Binary files /dev/null and b/guidebook/assets/large/crafting_terminal.png differ diff --git a/guidebook/assets/large/craftingco.png b/guidebook/assets/large/craftingco.png new file mode 100644 index 00000000000..8e52e39f2c7 Binary files /dev/null and b/guidebook/assets/large/craftingco.png differ diff --git a/guidebook/assets/large/craftingmonitor.png b/guidebook/assets/large/craftingmonitor.png new file mode 100644 index 00000000000..89907777751 Binary files /dev/null and b/guidebook/assets/large/craftingmonitor.png differ diff --git a/guidebook/assets/large/craftingunit.png b/guidebook/assets/large/craftingunit.png new file mode 100644 index 00000000000..286cb12df90 Binary files /dev/null and b/guidebook/assets/large/craftingunit.png differ diff --git a/guidebook/assets/large/dark_illuminated_panel.png b/guidebook/assets/large/dark_illuminated_panel.png new file mode 100644 index 00000000000..deb4badf777 Binary files /dev/null and b/guidebook/assets/large/dark_illuminated_panel.png differ diff --git a/guidebook/assets/large/debug-card-display.png b/guidebook/assets/large/debug-card-display.png new file mode 100644 index 00000000000..270bb2d84e1 Binary files /dev/null and b/guidebook/assets/large/debug-card-display.png differ diff --git a/guidebook/assets/large/dense_cable.png b/guidebook/assets/large/dense_cable.png new file mode 100644 index 00000000000..124cb289c3b Binary files /dev/null and b/guidebook/assets/large/dense_cable.png differ diff --git a/guidebook/assets/large/dense_energy_cell.png b/guidebook/assets/large/dense_energy_cell.png new file mode 100644 index 00000000000..2f6f3c2bf52 Binary files /dev/null and b/guidebook/assets/large/dense_energy_cell.png differ diff --git a/guidebook/assets/large/emitter2.png b/guidebook/assets/large/emitter2.png new file mode 100644 index 00000000000..feacbcc1e12 Binary files /dev/null and b/guidebook/assets/large/emitter2.png differ diff --git a/guidebook/assets/large/energy_accepter.png b/guidebook/assets/large/energy_accepter.png new file mode 100644 index 00000000000..47d1d2699f5 Binary files /dev/null and b/guidebook/assets/large/energy_accepter.png differ diff --git a/guidebook/assets/large/energy_cell.png b/guidebook/assets/large/energy_cell.png new file mode 100644 index 00000000000..704e49bede8 Binary files /dev/null and b/guidebook/assets/large/energy_cell.png differ diff --git a/guidebook/assets/large/export_bus.png b/guidebook/assets/large/export_bus.png new file mode 100644 index 00000000000..78e60aa9ca6 Binary files /dev/null and b/guidebook/assets/large/export_bus.png differ diff --git a/guidebook/assets/large/facade.png b/guidebook/assets/large/facade.png new file mode 100644 index 00000000000..b16449bce9c Binary files /dev/null and b/guidebook/assets/large/facade.png differ diff --git a/guidebook/assets/large/fluix_block.png b/guidebook/assets/large/fluix_block.png new file mode 100644 index 00000000000..1af94381272 Binary files /dev/null and b/guidebook/assets/large/fluix_block.png differ diff --git a/guidebook/assets/large/glass_cable.png b/guidebook/assets/large/glass_cable.png new file mode 100644 index 00000000000..a1104122ea9 Binary files /dev/null and b/guidebook/assets/large/glass_cable.png differ diff --git a/guidebook/assets/large/grinder.png b/guidebook/assets/large/grinder.png new file mode 100644 index 00000000000..7305e2810f8 Binary files /dev/null and b/guidebook/assets/large/grinder.png differ diff --git a/guidebook/assets/large/illuminated_panel.png b/guidebook/assets/large/illuminated_panel.png new file mode 100644 index 00000000000..0a9ebcdfe3d Binary files /dev/null and b/guidebook/assets/large/illuminated_panel.png differ diff --git a/guidebook/assets/large/import_bus.png b/guidebook/assets/large/import_bus.png new file mode 100644 index 00000000000..e6a959e24c3 Binary files /dev/null and b/guidebook/assets/large/import_bus.png differ diff --git a/guidebook/assets/large/inscriber2.png b/guidebook/assets/large/inscriber2.png new file mode 100644 index 00000000000..5e3d07e9d59 Binary files /dev/null and b/guidebook/assets/large/inscriber2.png differ diff --git a/guidebook/assets/large/interface.png b/guidebook/assets/large/interface.png new file mode 100644 index 00000000000..eb0024fef1a Binary files /dev/null and b/guidebook/assets/large/interface.png differ diff --git a/guidebook/assets/large/interface_module.png b/guidebook/assets/large/interface_module.png new file mode 100644 index 00000000000..5e808c4a1c9 Binary files /dev/null and b/guidebook/assets/large/interface_module.png differ diff --git a/guidebook/assets/large/io_port.png b/guidebook/assets/large/io_port.png new file mode 100644 index 00000000000..c0e9f9d1039 Binary files /dev/null and b/guidebook/assets/large/io_port.png differ diff --git a/guidebook/assets/large/matter_condenser.png b/guidebook/assets/large/matter_condenser.png new file mode 100644 index 00000000000..a5964fabc97 Binary files /dev/null and b/guidebook/assets/large/matter_condenser.png differ diff --git a/guidebook/assets/large/me_chest.png b/guidebook/assets/large/me_chest.png new file mode 100644 index 00000000000..a00df148900 Binary files /dev/null and b/guidebook/assets/large/me_chest.png differ diff --git a/guidebook/assets/large/me_drive.png b/guidebook/assets/large/me_drive.png new file mode 100644 index 00000000000..5c53a61eadc Binary files /dev/null and b/guidebook/assets/large/me_drive.png differ diff --git a/guidebook/assets/large/meteorite.png b/guidebook/assets/large/meteorite.png new file mode 100644 index 00000000000..c07c12d3535 Binary files /dev/null and b/guidebook/assets/large/meteorite.png differ diff --git a/guidebook/assets/large/meteorite_compass.png b/guidebook/assets/large/meteorite_compass.png new file mode 100644 index 00000000000..623111c3a52 Binary files /dev/null and b/guidebook/assets/large/meteorite_compass.png differ diff --git a/guidebook/assets/large/quantum_link_chamber.png b/guidebook/assets/large/quantum_link_chamber.png new file mode 100644 index 00000000000..bdca2439a77 Binary files /dev/null and b/guidebook/assets/large/quantum_link_chamber.png differ diff --git a/guidebook/assets/large/quantum_network_bridge.png b/guidebook/assets/large/quantum_network_bridge.png new file mode 100644 index 00000000000..6901b124a1b Binary files /dev/null and b/guidebook/assets/large/quantum_network_bridge.png differ diff --git a/guidebook/assets/large/quantum_ring.png b/guidebook/assets/large/quantum_ring.png new file mode 100644 index 00000000000..7db9aa2a967 Binary files /dev/null and b/guidebook/assets/large/quantum_ring.png differ diff --git a/guidebook/assets/large/quartz_glass.png b/guidebook/assets/large/quartz_glass.png new file mode 100644 index 00000000000..f147c8cc75b Binary files /dev/null and b/guidebook/assets/large/quartz_glass.png differ diff --git a/guidebook/assets/large/security_terminal.png b/guidebook/assets/large/security_terminal.png new file mode 100644 index 00000000000..c6752369ce8 Binary files /dev/null and b/guidebook/assets/large/security_terminal.png differ diff --git a/guidebook/assets/large/sky_stone.png b/guidebook/assets/large/sky_stone.png new file mode 100644 index 00000000000..1d03b166180 Binary files /dev/null and b/guidebook/assets/large/sky_stone.png differ diff --git a/guidebook/assets/large/sky_stone_block_chest.png b/guidebook/assets/large/sky_stone_block_chest.png new file mode 100644 index 00000000000..d465789944c Binary files /dev/null and b/guidebook/assets/large/sky_stone_block_chest.png differ diff --git a/guidebook/assets/large/sky_stone_brick.png b/guidebook/assets/large/sky_stone_brick.png new file mode 100644 index 00000000000..e72777a4f8c Binary files /dev/null and b/guidebook/assets/large/sky_stone_brick.png differ diff --git a/guidebook/assets/large/sky_stone_chest.png b/guidebook/assets/large/sky_stone_chest.png new file mode 100644 index 00000000000..093eac75efa Binary files /dev/null and b/guidebook/assets/large/sky_stone_chest.png differ diff --git a/guidebook/assets/large/sky_stone_small_brick.png b/guidebook/assets/large/sky_stone_small_brick.png new file mode 100644 index 00000000000..b5d52a952db Binary files /dev/null and b/guidebook/assets/large/sky_stone_small_brick.png differ diff --git a/guidebook/assets/large/skystone_block.png b/guidebook/assets/large/skystone_block.png new file mode 100644 index 00000000000..584c1debb06 Binary files /dev/null and b/guidebook/assets/large/skystone_block.png differ diff --git a/guidebook/assets/large/smart_cable.png b/guidebook/assets/large/smart_cable.png new file mode 100644 index 00000000000..13da5d3d7d9 Binary files /dev/null and b/guidebook/assets/large/smart_cable.png differ diff --git a/guidebook/assets/large/spatial_io_port.png b/guidebook/assets/large/spatial_io_port.png new file mode 100644 index 00000000000..b5ffd73348f Binary files /dev/null and b/guidebook/assets/large/spatial_io_port.png differ diff --git a/guidebook/assets/large/spatial_pylon.png b/guidebook/assets/large/spatial_pylon.png new file mode 100644 index 00000000000..afbd230d7d0 Binary files /dev/null and b/guidebook/assets/large/spatial_pylon.png differ diff --git a/guidebook/assets/large/terminal.png b/guidebook/assets/large/terminal.png new file mode 100644 index 00000000000..74bc8bb076c Binary files /dev/null and b/guidebook/assets/large/terminal.png differ diff --git a/guidebook/assets/large/tiny_tnt2.png b/guidebook/assets/large/tiny_tnt2.png new file mode 100644 index 00000000000..3d5f31e06df Binary files /dev/null and b/guidebook/assets/large/tiny_tnt2.png differ diff --git a/guidebook/assets/large/tunnelchannels.png b/guidebook/assets/large/tunnelchannels.png new file mode 100644 index 00000000000..eb8bfcd2592 Binary files /dev/null and b/guidebook/assets/large/tunnelchannels.png differ diff --git a/guidebook/assets/large/vibration_chamber.png b/guidebook/assets/large/vibration_chamber.png new file mode 100644 index 00000000000..3ea6c3576bf Binary files /dev/null and b/guidebook/assets/large/vibration_chamber.png differ diff --git a/guidebook/assets/large/wireless_access_point.png b/guidebook/assets/large/wireless_access_point.png new file mode 100644 index 00000000000..8f8f0570ccb Binary files /dev/null and b/guidebook/assets/large/wireless_access_point.png differ diff --git a/guidebook/customizing-ae2.md b/guidebook/customizing-ae2.md new file mode 100644 index 00000000000..f09fa1fd941 --- /dev/null +++ b/guidebook/customizing-ae2.md @@ -0,0 +1,149 @@ +--- +navigation: + title: Customizing AE2 + icon: certus_quartz_wrench +--- + +This page describes how AE2 can be tweaked by modpack authors or players to their own play-style. + +## Configuration + +### Channel Modes + +If you don't like playing with channels or just want a more laid back experience, see the +[channel modes section](./features/me-network/channels.md#channel-modes) for multiple options +to customize AE2's channels mechanic. + +### Faster Crystal Growth in Certain Fluids + +AE2 allows a fluid tag to be specified in `improvedFluidTag`, which will increase the speed at which crystal seeds +grow by `improvedFluidMultiplier` (default: 2) when they are submerged in this type of fluid. + +## Recipes + +AE2 uses standard JSON recipes. The easiest starting point is to download the jar file and unpack it. Recipes are +in `data/ae2/recipes`. + +### Special Recipe Types + +AE2 introduces a few custom recipe types that use a custom JSON format. They are described in the following sections. + +#### Inscriber + +Used by the . Example recipes can be found in `data/ae2/recipes/inscriber`. + +Please note that the inscriber will also allow each recipe to be flipped so that top and bottom slots are reversed, so +two recipes whose top/bottom are the same after flipping would result in a recipe conflict. + +The available JSON properties are as follows: + +| Property | Description | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `type` | Must be `ae2:inscriber` | +| `mode` | Defines whether the top and bottom ingredients are consumed (`press`) or not (`inscribe`) | +| `ingredients`.`top` | Ingredient for the top slot (optional). | +| `ingredients`.`middle` | Ingredient for the middle slot (required). | +| `ingredients`.`bottom` | Ingredient for the bottom slot (optional). | +| `result` | Recipe result | + +#### Entropy Manipulator + +The uses recipes to decide what it can be used on. +Example recipes can be found in `data/ae2/recipes/entropy`. + +Right-clicking with the entry manipulator uses recipes of type `heat`, while shift-right-clicking will use `cool`. +Placing an entropy manipulator in a dispenser will try both types (first `cool`, then `heat`). + +The available JSON properties are as follows: + +| Property | Description | +| --------------- | ---------------------------------------------------------------------------------- | +| `type` | Must be `ae2:entropy` | +| `mode` | The use-mode of the entropy manipulator this recipe applies to (`heat` or `cool`). | +| `input` | Which in-world block/fluid this recipe applies to. | +| `input`.`block` | Defines which blocks this recipe applies to (see below for details). | +| `input`.`fluid` | Defines which fluids this recipe applies to (see below for details). | +| `output` | Defines the result of using the item on `input`. | + +##### Defining Inputs + +The input for the entropy recipe type can be a block or fluid, or both at the same time, to match only +specific waterlogged blocks. + +Block and fluid inputs can be defined as follows: + +```json +{ + "input": { + "block": { + "id": "minecraft:cobblestone", + "property1": "value", + "property2": ["value1", "value2"], + "property3": { + "min": 1, + "max": 5 + } + }, + "fluid": { + "id": "minecraft:water", + "property1": "value", + "property2": ["value1", "value2"], + "property3": { + "min": 1, + "max": 5 + } + } + } +} +``` + +The `id` property is mandatory, while additional properties may be specified to match specific block state properties, +either directly, as a list of matching values, or as a range (between `min` and `max`). + +##### Defining Output + +Applying an entropy manipulator recipe can result in one or all of: + +- Changing the block +- Changing the fluid +- Dropping items + +```json +{ + "output": { + "block": { + "id": "minecraft:cobblestone", + "keep": true, + "property1": "value" + }, + "fluid": { + "id": "minecraft:water", + "property2": "value" + }, + "drops": [ + { + "item": "minecraft:snowball", + "count": 1 + } + ] + } +} +``` + +All three properties (block, fluid, drops) are optional, but can also be used together. +The special `keep` property for `block` and `fluid` will copy over the block state properties from the existing +block while changing the block or fluid `id`. Additionally, any extra properties will be interpreted as block state +properties and applied to the new block. + +If the operation should drop items, those should be specified as a list in `drops`. + +#### Matter Cannon Ammo + +The uses recipes to decide which items count as ammo, and what their damage value should +be. Example recipes can be found in `data/ae2/recipes/matter_cannon`. + +| Property | Description | +| -------- | -------------------------------------------------------------------------------------------------------------- | +| `type` | Must be `ae2:matter_cannon` | +| `ammo` | Ingredient identifying which item this recipe applies to. | +| `weight` | The weight of the ammo. This affects block penetration and damage. Damage is weight divided by 20, rounded up. | diff --git a/guidebook/debug-card.md b/guidebook/debug-card.md new file mode 100644 index 00000000000..9db5e1c5042 --- /dev/null +++ b/guidebook/debug-card.md @@ -0,0 +1,53 @@ +--- +navigation: + title: Debug Card + icon: debug_card +item_ids: + - ae2:debug_card +--- + +This page describes how to use the debug card to troubleshoot some issues. +You probably don't need to read this unless you are an AE2 developer / addon developer, +or we requested that you read it to help troubleshoot an issue, or you are curious about how AE2 works internally. + +### Setup + +You need to enable `unsupportedDeveloperTools` in the AE2 config. +Be very careful with some of these, there is a reason they are behind a config option containing **unsupported**. +The item is reasonably safe to use, and you should give one to yourself and hold it in your main hand. + +Also make sure that have installed WTHIT (Fabric) or Jade (Forge). + +When looking at an AE2 block or part, you should seem something that looks like this: +![A picture of debug card display.](assets/large/debug-card-display.png) + +### Node connectivity + +The **Node Exposed** indicator highlights from which sides this device allows external connections. +That should match what is visible in world, otherwise it's a bug. + +### Tick rates + +To minimize lag, most ticking AE2 devices sleep when they have no work to do, and then they slowly "wake up" over time. +That is why import busses and export busses take a while to reach their maximum speed. +This information is visible in the debug information display: + +#### Tick Status + +- **Sleeping**: the device is currently sleeping, i.e. not going to do anything until it is awakened. +- **Awake**: the device is not sleeping, i.e. it should have some work scheduled. +- **Alertable**: the device is allowed to go from sleeping to awake if it wants. +- **Queued**: the device definitely has some work scheduled for later. + +**If a device is awake, but not queued, it's a bug!** + +#### Tick Rate + +The "current speed" of the device, i.e. how many game ticks (1 tick = 0.05s) it waits before two actions. +This should go down as the device wakes up, and then back up when the device has no work to do. + +#### Last + +The last time the device was ticked. + +**If a device is awake, but the last tick happened longer ago than the tick rate should allow, it's a bug!** diff --git a/guidebook/features/advanced-tools.md b/guidebook/features/advanced-tools.md new file mode 100644 index 00000000000..539a4cc6aff --- /dev/null +++ b/guidebook/features/advanced-tools.md @@ -0,0 +1,10 @@ +--- +navigation: + title: Advanced Tools +--- + +# Advanced Tools + +Applied Energistics 2 offers a variety of unique advanced tools + + diff --git a/guidebook/features/advanced-tools/configuration/biometric-card.md b/guidebook/features/advanced-tools/configuration/biometric-card.md new file mode 100644 index 00000000000..3cf723ff0c7 --- /dev/null +++ b/guidebook/features/advanced-tools/configuration/biometric-card.md @@ -0,0 +1,18 @@ +--- +categories: + - Advanced Tools/Configuration Tools +item_ids: + - ae2:biometric_card +navigation: + title: Biometric Card +--- + +Encodes a players identity on the card, right click another player, or shift +slick to set yourself. If you encode the same player twice, it will clear the +card. A Cleared card represents other users as the "default". + +When the has +been encoded it will show a Identicon for the user so you can tell cards +apart, each card for the user will have the same Identicon. + + diff --git a/guidebook/features/advanced-tools/configuration/memory-card.md b/guidebook/features/advanced-tools/configuration/memory-card.md new file mode 100644 index 00000000000..75286e30545 --- /dev/null +++ b/guidebook/features/advanced-tools/configuration/memory-card.md @@ -0,0 +1,36 @@ +--- +categories: + - Advanced Tools/Configuration Tools +item_ids: + - ae2:memory_card + - ae2:memory_card_white + - ae2:memory_card_orange + - ae2:memory_card_magenta + - ae2:memory_card_light_blue + - ae2:memory_card_yellow + - ae2:memory_card_lime + - ae2:memory_card_pink + - ae2:memory_card_gray + - ae2:memory_card_light_gray + - ae2:memory_card_cyan + - ae2:memory_card_purple + - ae2:memory_card_blue + - ae2:memory_card_brown + - ae2:memory_card_green + - ae2:memory_card_red + - ae2:memory_card_black +navigation: + title: Memory Card +--- + +A small item, that can be used to store, copy, and paste settings. Shift + +Right Click on a configurable object to save the settings onto the memory +card, then right click on any other block of the same type to paste the +settings. + +They are also used to link the input to the correct output + +. + + diff --git a/guidebook/features/advanced-tools/network/network-tool.md b/guidebook/features/advanced-tools/network/network-tool.md new file mode 100644 index 00000000000..92daa7d325a --- /dev/null +++ b/guidebook/features/advanced-tools/network/network-tool.md @@ -0,0 +1,19 @@ +--- +categories: + - Advanced Tools/Network Tools +item_ids: + - ae2:network_tool +navigation: + title: Network Tool +--- + +Tool that can be used to remove parts from cables ( like any other BC +compatible Wrench ), and has a 9 slot inventory that can store AE Upgrade +Cards. When in your inventory the inventory of the appears in machine GUIs +which accept upgrades. + +When right clicked on any network component it will give you break down of all +the attached parts, and power storage / usage details for the network. + + diff --git a/guidebook/features/advanced-tools/utilities/cell-workbench.md b/guidebook/features/advanced-tools/utilities/cell-workbench.md new file mode 100644 index 00000000000..d1f7075b93d --- /dev/null +++ b/guidebook/features/advanced-tools/utilities/cell-workbench.md @@ -0,0 +1,20 @@ +--- +categories: + - Advanced Tools/Utilities +item_ids: + - ae2:cell_workbench +navigation: + title: Cell Workbench +--- + +### ![A picture of a cell work bench.](../../../assets/large/cell_workbench.png) + +The lets you +configure how and + + as well as other similar items and Storage +Cells store their items, they allow you to insert upgrade cards such as and into +the devices, and select what items are accepted or rejected based on the settings +from the Upgrades. + + diff --git a/guidebook/features/advanced-tools/utilities/color-applicator.md b/guidebook/features/advanced-tools/utilities/color-applicator.md new file mode 100644 index 00000000000..38bd174bc00 --- /dev/null +++ b/guidebook/features/advanced-tools/utilities/color-applicator.md @@ -0,0 +1,31 @@ +--- +categories: + - Advanced Tools/Utilities +item_ids: + - ae2:color_applicator +navigation: + title: Color Applicator +--- + +Tool which allows you to paint in world objects with or any dye items; which +are less efficient then paint balls. Supports all four types of ME Cables and +other forge coloring compatible blocks such as IC2 power cables as well as all +vanilla wools, glass blocks, glass panes and hardened clay. + +You can shift click in the air, or hold shift and scroll the mouse wheel to +change the selected color of the applicator. + +Functions like a storage cell which can hold of all colors, so i can +be loaded with an . And +like other tools requires power and can be charged in the . + +In addition to coloring with paint balls it can be used to remove colors from +cables, and clean paint balls off of walls when you use snow balls inside it. + +Also functions in the dispenser as a means to color blocks that are in front +of it. + + diff --git a/guidebook/features/advanced-tools/utilities/entropy-manipulator.md b/guidebook/features/advanced-tools/utilities/entropy-manipulator.md new file mode 100644 index 00000000000..88b4121d36e --- /dev/null +++ b/guidebook/features/advanced-tools/utilities/entropy-manipulator.md @@ -0,0 +1,20 @@ +--- +categories: + - Advanced Tools/Utilities +item_ids: + - ae2:entropy_manipulator +navigation: + title: Entropy Manipulator +--- + +A Powered Multi-purpose tool which can alter the quantity of energy in the +block you target, you can decrease the energy of the block by holding shift ( +cool / aging ), or increase the energy in the block ( heating it ) by using it +normally, if adding heat doesn't do anything it generally will start a fire on +the block instead. You can also hit mobs or players with it to set them on +fire. + +Its battery can store 200k ae and consumes 1600 ae per usage, it can be to be +recharged in a . + + diff --git a/guidebook/features/advanced-tools/utilities/matter-condenser.md b/guidebook/features/advanced-tools/utilities/matter-condenser.md new file mode 100644 index 00000000000..960e54f7b8a --- /dev/null +++ b/guidebook/features/advanced-tools/utilities/matter-condenser.md @@ -0,0 +1,27 @@ +--- +categories: + - Advanced Tools/Utilities +item_ids: + - ae2:condenser +navigation: + title: Matter Condenser +--- + +![A picture of a Matter Condenser.](../../../assets/large/matter_condenser.png)Has +two key functions, ellimination of excess materials in a cheap manor, and the +production of and + +. + +Can accept both fluids and items to be destroyed. Each item or fluid will +count as one additional energy. This energy can be stored on , , or . 256 Energy is +required to generate a , 256,000 respecitvely for the + +. + + diff --git a/guidebook/features/advanced-tools/utilities/portable-cell.md b/guidebook/features/advanced-tools/utilities/portable-cell.md new file mode 100644 index 00000000000..e83d38ec707 --- /dev/null +++ b/guidebook/features/advanced-tools/utilities/portable-cell.md @@ -0,0 +1,40 @@ +--- +categories: + - Advanced Tools/Utilities +item_ids: + - ae2:portable_item_cell_1k + - ae2:portable_item_cell_4k + - ae2:portable_item_cell_16k + - ae2:portable_item_cell_64k + - ae2:portable_fluid_cell_1k + - ae2:portable_fluid_cell_4k + - ae2:portable_fluid_cell_16k + - ae2:portable_fluid_cell_64k +related: + - Possible Upgrades +navigation: + title: Portable Cell +--- + +The is a portable +inventory. It requires power to function but can be charged in the . It acts like any other storage +cell except that you can access its content without external hardware. It +functions with the and +other features in the same way as or other storage cells. + + are smaller then most cells, only carrying +512 bytes, and 27 types at 8 bytes per type. Its battery can hold 20k ae and it drains +0.5 ae/t when in a or however drains +1 ae/t when opened it directly. + + + + + + + + + + diff --git a/guidebook/features/advanced-tools/weapons/charged-staff.md b/guidebook/features/advanced-tools/weapons/charged-staff.md new file mode 100644 index 00000000000..655992afd84 --- /dev/null +++ b/guidebook/features/advanced-tools/weapons/charged-staff.md @@ -0,0 +1,19 @@ +--- +categories: + - Advanced Tools/Weapons +item_ids: + - ae2:charged_staff +navigation: + title: Charged Staff +--- + +An powered melee weapon based on the power of , it only +has a few uses but it can be effective rechargeable weapon, it must be be re- +charged in the when its +power is depleted. + +The Holds 8k ae +in it battery using 300 ae per attack. + + diff --git a/guidebook/features/advanced-tools/weapons/matter-cannon.md b/guidebook/features/advanced-tools/weapons/matter-cannon.md new file mode 100644 index 00000000000..9258a967a0c --- /dev/null +++ b/guidebook/features/advanced-tools/weapons/matter-cannon.md @@ -0,0 +1,27 @@ +--- +categories: + - Advanced Tools/Weapons +item_ids: + - ae2:matter_cannon +related: + - Possible Projectiles + - Possible Upgrades +navigation: + title: Matter Cannon +--- + +The is a portable +railgun, which can shoot small projectiles; It causes damage is based on its +ammo. Since it is an powered weapon, it requires charging in the . Ammunition can be refilled in a + +, or as it functions as a +single item Storage cell, similar to + +Its battery can hold up to 200k ae and consumes 1600 ae per item fired. ( +1 +item fired per ) + +A a general rule heavier metals inflict more damage, and lighter non metal +iems are less damaging. + + diff --git a/guidebook/features/annihilation-core.md b/guidebook/features/annihilation-core.md new file mode 100644 index 00000000000..52d7a897a3d --- /dev/null +++ b/guidebook/features/annihilation-core.md @@ -0,0 +1,10 @@ +--- +item_ids: + - ae2:annihilation_core +navigation: + title: Annihilation Core +--- + +A component which can convert matter into energy. + + diff --git a/guidebook/features/auto-crafting.md b/guidebook/features/auto-crafting.md new file mode 100644 index 00000000000..e10813991a8 --- /dev/null +++ b/guidebook/features/auto-crafting.md @@ -0,0 +1,150 @@ +--- +navigation: + title: Auto-Crafting + icon: molecular_assembler +item_ids: + - ae2:blank_pattern + - ae2:crafting_pattern + - ae2:processing_pattern + - ae2:1k_crafting_storage + - ae2:4k_crafting_storage + - ae2:16k_crafting_storage + - ae2:64k_crafting_storage + - ae2:crafting_accelerator + - ae2:crafting_monitor + - ae2:crafting_unit + - ae2:pattern_provider + - ae2:cable_pattern_provider +--- + +## Crafting CPU + +Manages a single auto crafting task from start to finish, built of various crafting units. + +To be a valid crafting CPU, two rules must be met: + +1. The CPU must be a cuboid, completely composed of the parts listed above; air or other blocks are not valid. +2. The CPU must contain at least 1 storage component. + +The crafting CPU as a multi-block only requires a single channel for the +entire structure. Crafting co-processors increase the number of tasks the +crafting CPU can perform at once; with no co-processors, the crafting CPU can +perform a single task at a time. Storage requirements are moderately +complicated, and do not follow the usual ME storage math, but for a first +approximation, you will need a little over one byte per input item, output +item, or operation. + +You can name your Crafting CPUs by naming any of the crafting units it is made up of with +an or an Anvil. + +To provide patterns to the autocrafting cpus you can use +or . + +### Components + +#### Crafting Unit + +![A picture of several crafting units in a crafting CPU.](../assets/large/craftingunit.png) + +This particular block provides the CPU with no additional features, but can be used as a "filler" block. +It is the base for crafting the other functional components of a crafting CPU. + +#### Crafting Storage + +![A picture of a 1k crafting storage unit.](../assets/large/crafting1k.png) + +Provides 1024 bytes of storage for crafting. + + + +![A picture of a 4k crafting storage unit.](../assets/large/crafting4k.png) + +Provides 4,096 bytes of storage for crafting. + + + +![A 16k Crafting Storage Unit.](../assets/large/crafting16k.png) + +Provides 16,384 bytes of storage for crafting. + + + +![A picture of a 64k Crafting Storage Unit](../assets/large/crafting64k.png) + +Provides 65,536 bytes of storage for crafting. + + + +#### Co-Processor + + +Provides additional item delivery from the CPU to the for +crafting. + +This can be used to make more assemblers active in parallel for the job, and +thus increase overall crafting speed. These only help if your setup has steps +properly separated so the system can run multiple tasks in parallel, or even +split the same pattern across multiple interfaces. + + + +
+ +#### Crafting Monitor + +![A picture of a crafting monitor inside a Crafting CPU.](../assets/large/craftingmonitor.png) + +Displays the top level job and its current progress so you can see what a particular Crafting CPU is currently +working on. + + + +## Pattern Provider + +Recipes need to be encoded into patterns to be usable by crafting CPUs. The encoded patterns need to be put +into pattern providers on the same network as the Crafting CPU itself. When the crafting CPU then +needs to craft the primary result of that pattern, it'll delegate this to the +pattern provider. Normally, the pattern provider will then push out the +ingredients to an adjacent block (a for crafting recipes, for example), +and crafting continues once the result enters the network again. +This can be achieved by pushing the crafting result back into the pattern provider, +an or any other means that would import the crafting result into the network. Molecular +assemblers are smart enough to automatically return the crafting result to the same pattern provider that provided +the ingredients. + + + + +### Blank Pattern + +A blank pattern, once encoded as an +or , is used to control +crafting by inserting them into and . + +Patterns can be encoded in the . + + + +### Crafting Patterns + +Encoded version of created by using +the in "Crafting Mode". + +Crafting Recipes are very specific, and automatically have an output +associated with them, these are required to work with a . + +The description of a crafting pattern starts with "Crafts". + +### Processing Recipes + +Encoded version of created by using +the in "Processing Mode". + +Processing recipes are not crafting recipes, they have no rules to their +inputs, or outputs, and are used for things like machines or furances, they +can support up to 9 different inputs, and up to 3 diffrent outputs ( these +outputs cannot be random chance, each output must still be a 100% chance. ) + +The description of a processing pattern starts with "Creates". diff --git a/guidebook/features/crystals.md b/guidebook/features/crystals.md new file mode 100644 index 00000000000..3ed111e7189 --- /dev/null +++ b/guidebook/features/crystals.md @@ -0,0 +1,37 @@ +--- +navigation: + title: Crystals + icon: certus_quartz_crystal +item_ids: + - ae2:quartz_ore + - ae2:certus_quartz_crystal + - ae2:charged_certus_quartz_crystal + - ae2:certus_quartz_dust + - ae2:fluix_crystal + - ae2:fluix_dust +--- + +## Certus Quartz Crystals + +Certus quartz crystals possess the unique trait of storing large quantities of energy. +When charged in the it will convert +into . + +### Fluix Crystals + +This crystal possesses the unique ability to absorb and convert energy from one +form to another, and is the foundation of all ME technology. + + is crafted in world by placing +, +and in water. It will quickly start to react, +and result in a . . + +
+ +
diff --git a/guidebook/features/decorative-blocks/cable-anchor.md b/guidebook/features/decorative-blocks/cable-anchor.md new file mode 100644 index 00000000000..d53d24a13d2 --- /dev/null +++ b/guidebook/features/decorative-blocks/cable-anchor.md @@ -0,0 +1,17 @@ +--- +categories: + - Decorative Blocks/Other Features +item_ids: + - ae2:cable_anchor +navigation: + title: Cable Anchor +--- + +![A picture of cable anchors on glass cable.](../../assets/large/cable_anchor.png) + +Small decorative cable-mounted spikes that you can use to create ladders with cables, or make the cable appear +connected to the walls around it. Also used to craft . + +Cable anchors prevent connections from forming on the side they're mounted on. + + diff --git a/guidebook/features/decorative-blocks/cable-facade.md b/guidebook/features/decorative-blocks/cable-facade.md new file mode 100644 index 00000000000..a69500dc8e7 --- /dev/null +++ b/guidebook/features/decorative-blocks/cable-facade.md @@ -0,0 +1,14 @@ +--- +navigation: + title: Cable Facade +item_ids: + - ae2:facade +--- + +![A picture of some stone brick facades.](../../assets/large/facade.png) + +Facades are crafted with any facadeable block in the middle of the crafting table, and +4 around it. They can be mounted on any variant of cables, and +around buses. They can be used to create a solid surface for things like levers or buttons, +but are for decorative in nature. + diff --git a/guidebook/features/decorative-blocks/certus-quartz.md b/guidebook/features/decorative-blocks/certus-quartz.md new file mode 100644 index 00000000000..69f6b88a20f --- /dev/null +++ b/guidebook/features/decorative-blocks/certus-quartz.md @@ -0,0 +1,20 @@ +--- +navigation: + title: Certus Quartz +item_ids: + - ae2:quartz_block + - ae2:quartz_pillar + - ae2:chiseled_quartz_block +--- + +![A picture of Certus Quartz Block](../../assets/large/certus_quartz_block.png) + + + +![A picture of certus quartz pillar](../../assets/large/certus_quartz_pillar.png) + + + +![A picture of chisled certus quartz.](../../assets/large/chisled_certus_quartz.png) + + diff --git a/guidebook/features/decorative-blocks/fluix.md b/guidebook/features/decorative-blocks/fluix.md new file mode 100644 index 00000000000..1822092d244 --- /dev/null +++ b/guidebook/features/decorative-blocks/fluix.md @@ -0,0 +1,12 @@ +--- +navigation: + title: Fluix +item_ids: + - ae2:fluix_block +--- + +![A picture of a fluix block.](../../assets/large/fluix_block.png) + +A Block of . + + diff --git a/guidebook/features/decorative-blocks/illuminated-panel.md b/guidebook/features/decorative-blocks/illuminated-panel.md new file mode 100644 index 00000000000..05038199c6e --- /dev/null +++ b/guidebook/features/decorative-blocks/illuminated-panel.md @@ -0,0 +1,29 @@ +--- +navigation: + title: Illuminated Panel + icon: monitor +item_ids: + - ae2:monitor + - ae2:dark_monitor + - ae2:semi_dark_monitor +--- + +![A picture of Illuminated Panels.](../../assets/large/illuminated_panel.png) + +Mostly decorative powered light source that can be attached to or other non dense +cables. Also used for crafting , +and . + + + +![A picture of Bright Illuminated Panels.](../../assets/large/bright_illuminated_panel.png) + +An alternate variation of + + + +![A picture of Dark Illuminated Panels.](../../assets/large/dark_illuminated_panel.png) + +An alternate variation of + + diff --git a/guidebook/features/decorative-blocks/quartz-fixture.md b/guidebook/features/decorative-blocks/quartz-fixture.md new file mode 100644 index 00000000000..95987124d99 --- /dev/null +++ b/guidebook/features/decorative-blocks/quartz-fixture.md @@ -0,0 +1,17 @@ +--- +navigation: + title: Quartz Fixtures +item_ids: + - ae2:quartz_fixture +--- + +### Quartz Fixtures + +![A picture of a Charged Quartz Fixture](../../assets/large/charged_quartz_fixture3.png) + +![A picture of a charged quartz fixture](../../assets/large/charged_quartz_fixture1.png) + +Decorative light source that can be mounted on any solid surface. Even the ceiling. Unlike torches, this block is not +affected by water. + + diff --git a/guidebook/features/decorative-blocks/quartz-glass.md b/guidebook/features/decorative-blocks/quartz-glass.md new file mode 100644 index 00000000000..cc8e5f1030e --- /dev/null +++ b/guidebook/features/decorative-blocks/quartz-glass.md @@ -0,0 +1,26 @@ +--- +categories: + - Decorative Blocks +item_ids: + - ae2:quartz_glass + - ae2:quartz_vibrant_glass +navigation: + title: Quartz Glass +--- + +### Quartz Glass + +![A picture of Quartz Glass](../../assets/large/quartz_glass.png) + +Mostly clear glass made with . +Can be used to make vibrant quartz glass and other items. + + + +### Vibrant Quartz Glass + +![A picture of Vibrant Quartz Glass](../../assets/large/VibrantQuartzGlassAni.gif) + +A variant of quartz glass that glows like glowstone. + + diff --git a/guidebook/features/decorative-blocks/sky-stone.md b/guidebook/features/decorative-blocks/sky-stone.md new file mode 100644 index 00000000000..e0d16bf93f1 --- /dev/null +++ b/guidebook/features/decorative-blocks/sky-stone.md @@ -0,0 +1,34 @@ +--- +categories: + - Decorative Blocks +item_ids: + - ae2:smooth_sky_stone_block + - ae2:sky_stone_brick + - ae2:sky_stone_small_brick +navigation: + title: Sky Stone +--- + +### Sky Stone Block + +![A Picture of a Sky Stone Block.](../../../assets/large/skystone_block.png) + +Block form of . + + + +### Sky Stone Brick + +![A picture of Sky Stone Brick.](../../../assets/large/sky_stone_brick.png) + +Brick form of . + + + +### Sky Stone Small Brick + +![A picture of Sky Stone Small Brick](../../../assets/large/sky_stone_small_brick.png) + +Small brick form of . + + diff --git a/guidebook/features/formation-core.md b/guidebook/features/formation-core.md new file mode 100644 index 00000000000..ee9d14c9ef4 --- /dev/null +++ b/guidebook/features/formation-core.md @@ -0,0 +1,10 @@ +--- +item_ids: + - ae2:formation_core +navigation: + title: Formation Core +--- + +A component which can convert energy back into matter. + + diff --git a/guidebook/features/matter-ball.md b/guidebook/features/matter-ball.md new file mode 100644 index 00000000000..d08b693ae50 --- /dev/null +++ b/guidebook/features/matter-ball.md @@ -0,0 +1,12 @@ +--- +item_ids: + - ae2:matter_ball +navigation: + title: Matter Ball +--- + +A cheap ammunition for the produced in the . Can also be used to craft + +. diff --git a/guidebook/features/me-network.md b/guidebook/features/me-network.md new file mode 100644 index 00000000000..e545803b23d --- /dev/null +++ b/guidebook/features/me-network.md @@ -0,0 +1,17 @@ +--- +navigation: + title: ME Network +--- + +# ME Network + +The main feature of Applied Energistics 2 is the ME Network (pronounced Emm- +Eee and stands for Matter Energy), which is a set of connected blocks, and +cables grouped into a system, where storage, power and functions cooperate +between multiple components. + +A Network requires power, which can be provided by various blocks, and may +require (s) if you +require more than 8 [channels](me-network/channels.md) on a single network. + + diff --git a/guidebook/features/me-network/ad-hoc-networks.md b/guidebook/features/me-network/ad-hoc-networks.md new file mode 100644 index 00000000000..0662e33481f --- /dev/null +++ b/guidebook/features/me-network/ad-hoc-networks.md @@ -0,0 +1,24 @@ +--- +navigation: + title: Ad Hoc Networks +--- + +# Ad Hoc Networks + +Ad-Hoc networks are small [ME Networks](../me-network.md) that do not have an . +They can have up to 8 [channels](channels.md) using devices. + +You can use them as small stand alone systems, or as systems designed to +enhance a larger [ME Network](../me-network.md), generally they are +powered via +however they can also be powered via a or even an energy cell if +you don't want to keep it running for extend periods of time. + +Smart Cables on Ad-Hoc networks will show the channel usage for every device on +the network at all points on the network, this is different from how they will +show usage if you are using a . + +Once an ad-hoc network exceeds 8 devices, the network will be unable to +allocate channels and everything will shutdown, you will either need to remove +devices, or install a and to convert it to a +standard network, instead of an ad-hoc network. diff --git a/guidebook/features/me-network/cables.md b/guidebook/features/me-network/cables.md new file mode 100644 index 00000000000..240d730c9fa --- /dev/null +++ b/guidebook/features/me-network/cables.md @@ -0,0 +1,184 @@ +--- +navigation: + title: Cables + icon: fluix_glass_cable +item_ids: + - ae2:white_glass_cable + - ae2:orange_glass_cable + - ae2:magenta_glass_cable + - ae2:light_blue_glass_cable + - ae2:yellow_glass_cable + - ae2:lime_glass_cable + - ae2:pink_glass_cable + - ae2:gray_glass_cable + - ae2:light_gray_glass_cable + - ae2:cyan_glass_cable + - ae2:purple_glass_cable + - ae2:blue_glass_cable + - ae2:brown_glass_cable + - ae2:green_glass_cable + - ae2:red_glass_cable + - ae2:black_glass_cable + - ae2:fluix_glass_cable + - ae2:white_covered_cable + - ae2:orange_covered_cable + - ae2:magenta_covered_cable + - ae2:light_blue_covered_cable + - ae2:yellow_covered_cable + - ae2:lime_covered_cable + - ae2:pink_covered_cable + - ae2:gray_covered_cable + - ae2:light_gray_covered_cable + - ae2:cyan_covered_cable + - ae2:purple_covered_cable + - ae2:blue_covered_cable + - ae2:brown_covered_cable + - ae2:green_covered_cable + - ae2:red_covered_cable + - ae2:black_covered_cable + - ae2:fluix_covered_cable + - ae2:white_covered_dense_cable + - ae2:orange_covered_dense_cable + - ae2:magenta_covered_dense_cable + - ae2:light_blue_covered_dense_cable + - ae2:yellow_covered_dense_cable + - ae2:lime_covered_dense_cable + - ae2:pink_covered_dense_cable + - ae2:gray_covered_dense_cable + - ae2:light_gray_covered_dense_cable + - ae2:cyan_covered_dense_cable + - ae2:purple_covered_dense_cable + - ae2:blue_covered_dense_cable + - ae2:brown_covered_dense_cable + - ae2:green_covered_dense_cable + - ae2:red_covered_dense_cable + - ae2:black_covered_dense_cable + - ae2:fluix_covered_dense_cable + - ae2:white_smart_cable + - ae2:orange_smart_cable + - ae2:magenta_smart_cable + - ae2:light_blue_smart_cable + - ae2:yellow_smart_cable + - ae2:lime_smart_cable + - ae2:pink_smart_cable + - ae2:gray_smart_cable + - ae2:light_gray_smart_cable + - ae2:cyan_smart_cable + - ae2:purple_smart_cable + - ae2:blue_smart_cable + - ae2:brown_smart_cable + - ae2:green_smart_cable + - ae2:red_smart_cable + - ae2:black_smart_cable + - ae2:fluix_smart_cable + - ae2:white_smart_dense_cable + - ae2:orange_smart_dense_cable + - ae2:magenta_smart_dense_cable + - ae2:light_blue_smart_dense_cable + - ae2:yellow_smart_dense_cable + - ae2:lime_smart_dense_cable + - ae2:pink_smart_dense_cable + - ae2:gray_smart_dense_cable + - ae2:light_gray_smart_dense_cable + - ae2:cyan_smart_dense_cable + - ae2:purple_smart_dense_cable + - ae2:blue_smart_dense_cable + - ae2:brown_smart_dense_cable + - ae2:green_smart_dense_cable + - ae2:red_smart_dense_cable + - ae2:black_smart_dense_cable + - ae2:fluix_smart_dense_cable + - ae2:toggle_bus + - ae2:inverted_toggle_bus +--- + +While ME networks are also created by adjacent ME-capable machines, cables are the primary way of +extending an ME network over larger areas. + +Differently colored cables can be used to ensure adjacent cables do not connected to each other, +allowing [channels](channels.md) to be distributed more efficiently. + +## Glass Cable + +![A Picture of Glass Cable](../../assets/large/glass_cable.png) + + is the simplest cable to make, transfers power +and up to 8 [Channels](channels.md). It comes in 17 diffrent colors, the default +being Fluix, and can be dyed any color using any of the 16 dyes. + +To craft colored cables surround a dye of any type with 8 cables of the same +type ( color of the cables dosn't matter, but they must be the same type, +glass, smart, etc ). You can also paint cables with any forge compatible paint +brush in world. + +You can craft any colored cable with a water bucket to remove the dye. + +You can cover the cable with wool to create , and craft to get a better idea of what is going on with +your [Channels](channels.md). + + + +## Covered Cable + +![A picture of covered cables.](../../assets/large/covered_cable.png) + +The covered cable variant offers no gameplay benefits over its counterpart. It can however be used +as an alternate aesthetic choice if you prefer the covered look. + +Can be colored in the same manner as . Four can be crafted with +redstone and glowstone to make . + + + +## Dense Cable + +![A picture of dense cable.](../../assets/large/dense_cable.png)Higher Capacity +cable, can carry 32 channels unlike standard cable which can only carry 8, +however it doesn't support buses so you must first step down from dense to a +smaller cable (such as or ) before using buses or +panels. Shows load similarly to , with each line lit +representing four channels in use. + + + +## Smart Cable + +![A picture of smart cable.](../../assets/large/smart_cable.png) + +While bearing some similarity to in appearance, they +provide diagnostic function by visualizing the channel usage on the cables, +the channels appear as lit colored lines that run along the black stripe on +the cables giving you an understanding of how your channels are being used on +your network. The first four channels show as lines matching the color of the +cable, the next four show as white lines. + +These can also be colored in the same manner as . + + + +## Toggle Bus + +A bus which functions similarly to or other cables, but it +allows its connection state to be toggled via redstone. This allows you to cut +off a section of a [ME Network](../me-network.md). + +When redstone signal supplied the part enables the connection, provides the reverse +behavior by disabling the connection instead. + + + +There is also an inverted version of the toggle bus that disables the connection +when a redstone signal is supplied. + + diff --git a/guidebook/features/me-network/channels.md b/guidebook/features/me-network/channels.md new file mode 100644 index 00000000000..3ec8fc063c7 --- /dev/null +++ b/guidebook/features/me-network/channels.md @@ -0,0 +1,123 @@ +--- +navigation: + title: Channels + icon: controller +--- + +Applied Energistics 2's [ME Networks](../me-network.md) require +Channels to support devices which use networked storage, or other network +services. Most devices such as standard cables, and machines can only support +up to 8 channels. However can support up +to 32 channels, the only other devices capable of transmitting 32 are +and the [Quantum Network Bridge](quantum-bridge.md). + +A Network without a +is considered to be Ad-Hoc, and can support up to 8 channel using devices. +Once you exceed 8 devices the networks channel using devices will shutdown, +you can either remove devices, or add a . + +While using [Ad-Hoc](ad-hoc-networks.md) networks each device will +use 1 channel network wide, this is very different from how allocate channels based on +shortest route. + +Channels will consume 1⁄128 ae/t per node they transverse, this means that by +adding a for a +network with 8 devices and over 96 nodes your power usage might actually +decrease power consumption because it changes how channels are allocated. + +When using a +Channels must route via the shortest path from the to the device. If the path is +already maxed out, some devices may not get their required channels, use +colored cables, cable anchors and tunnels to your advantage to make sure your +channels go in the path you desire. + +## Channel Modes + +AE2 10.0.0 for Minecraft 1.18 introduces new options to change how AE2 channels behave in your world. +There's a new configuration option in the general section (`channels`) which controls this option, and a new in-game +command for operators to change the mode and the config from inside the game. The command is `/ae2 channelmode ` +to change it and `/ae2 channelmode` to show the current mode. When the mode is changed in-game, all existing grids will +reboot and use the new mode immediately. + +This resurrects and improves upon the option that was available in Minecraft 1.12 and introduces better options for +players that just want a little more laid back gameplay but don't want the mechanic to be removed entirely. + +The following table lists the available modes in both the configuration file and command. + +| Setting | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `default` | The standard mode with the channel capacities of cable and ad-hoc networks as described throughout this website | +| `x2` | All channel capacities are doubled (16 on normal cable, 64 on dense cable, ad-hoc networks support 16 channels) | +| `x3` | All channel capacities are tripled (24 on normal cable, 92 on dense cable, ad-hoc networks support 24 channels) | +| `x4` | All channel capacities are quadrupled (32 on normal cable, 128 on dense cable, ad-hoc networks support 32 channels) | +| `infinite` | All channel restrictions are removed. Controllers still reduce the power consumption of grids _significantly_. Smart cables will only toggle between completely off (no channels carried) and completely on (1 or more channels carried). | + +## Design + +Designing your layouts with channels can be tricky because of their shortest +route nature, if any specific spot in your system has two possible routes, you +may find yourself returning home from a mining trip to see half your devices +offline. Take a look at the following example: + +![Diagram showing that two equal length paths are bad.](../../assets/channels/badLength.png) + +--- + +Equal Length Route + +In the above image the controller is represented by the Green Block, Cables or +machines by green lines. The blue square indicate which Locations only have 1 +route; this is good, but there is a red block, which indicates that there is +two possible routes, this can be bad, especially if your exceeding 8 channels +on cable, or machines for a specific block of machines. Now that you can +understand that basic issue and diagram look at these other diagrams. + +
+ +![An example of a good layout](../../assets/channels/good_split.png) + +
+ +![An example of a bad layout](../../assets/channels/bad_split3.png) + +
+ +![An example of a bad layout](../../assets/channels/bad_split.png) + +
+ +![An example of a bad layout](../../assets/channels/bad_split2.png) + +
+ +You can see that depending on how you run your cable, you might end up with +different possible outcomes in a block of machines, you can also see that +using a controller you can ensure that the channels equilibrium is kept from a +straight line. + +In the second setup you can see that the middle line is red, however its +important to remember that it only matters if that line of machines uses +channels, if that line was for instance molecular assemblers, it wouldn't +matter, so that could be a valid setup for building. + +In the Last two you can see that you might run an extra cable into a block of +machines, and it might appear to work, but you can see that it can break quite +easily. + +Now that you understand how this works, I'll leave you with one final piece of +helpful information, if you run into a situation where you can't use a +controller, and your design is imbalanced, consider using p2p tunnels, since a +tunnel connection is considered a single "hop" you can get the system to have +a different outcome. + +![Diagram showing how to fix a previous setup with a p2p tunnel.](../../assets/channels/p2psplit.png) + +## Using P2P-Tunnels to adjust route lengths + +One last important note about this, you can see that the p2p tunnel is +directly on the controller, and directly on the block of machines at the +bottom of the setup, this is done because the in and out tunnel are both +considered "a node", so the two cable from the controller and in and out +tunnel balance to create the final balanced setup. diff --git a/guidebook/features/me-network/me-controller.md b/guidebook/features/me-network/me-controller.md new file mode 100644 index 00000000000..03c4330b151 --- /dev/null +++ b/guidebook/features/me-network/me-controller.md @@ -0,0 +1,31 @@ +--- +item_ids: + - ae2:controller +navigation: + title: ME Controller +--- + +![A picture of a controller.](../../assets/large/controller.png) + +The is the routing hub of a [ME Network](../me-network.md). +Without it, only up to 8 devices can interact, any more and everything stops working. + +It is not possible to have 2 in one [ME Network](../me-network.md). + +Unlike most [ME Network](../me-network.md) devices, the does not require +[channels](channels.md), rather it emits them 8 or 32 per side, depending how many [channels](channels.md) the device supports. + +The requires 6 AE/t per controller block to +function. Each block can store 8000 AE, so larger networks might require additional +energy storage. See [network energy](network-energy.md) for details. + +Multiblock Controllers can be build in a fairly free form, however there are a few rules that must be followed: + +1. All blocks on a [ME Network](../me-network.md) must be connected; else the blocks will turn red. +2. The size of the must be within 7x7x7; else it will turn red. +3. A can have 2 adjacent blocks in at most 1 axis; if a block violates this rule, it will disable and turn white. + +As long as all rules are followed and powered, the controller should glow and +cycle colors. + + diff --git a/guidebook/features/me-network/misc/me-io-port.md b/guidebook/features/me-network/misc/me-io-port.md new file mode 100644 index 00000000000..358a483aecc --- /dev/null +++ b/guidebook/features/me-network/misc/me-io-port.md @@ -0,0 +1,26 @@ +--- +categories: + - ME Network/Misc +item_ids: + - ae2:io_port +related: + - Possible Upgrades +navigation: + title: ME IO Port +--- + +![A picture of a IO port.](../../../assets/large/io_port.png) + +This block lets transfer items between your [ME Network](../../me-network.md) and your or other storage cells, +unlike other forms of automation the can send items without any configuration of which items to send. + +The top of the UI indicates which direction you wish to move the data, into the cell, or into the networks. + +The requires a [channel](../channels.md) to function. When used with automation, the +input is the top or bottom, and the output is any of the sides. + +If you're storing data onto cells it's usually a good idea to pre-format them, +that way you can get just the items you're interested in, and not get tons of +extra items. + + diff --git a/guidebook/features/me-network/misc/me-security-terminal.md b/guidebook/features/me-network/misc/me-security-terminal.md new file mode 100644 index 00000000000..121a2e05e9b --- /dev/null +++ b/guidebook/features/me-network/misc/me-security-terminal.md @@ -0,0 +1,59 @@ +--- +categories: + - ME Network/Misc +item_ids: + - ae2:security_station +navigation: + title: ME Security Terminal +--- + +![A picture of a security terminal.](../../../assets/large/security_terminal.png) + +Allows you to configure which users, and what permissions the users have with +the ME System. By existing it enforces permissions on the usage of the system. + +The security system does not prevent destructive tampering, removing cables / +machines or breaking of drives is not directly provided by the security +Terminal. If you need to protect your system from physical vandalism you will +need another form of physical security. This block provides Network level +security. + +The player who places the has full control over +the network and cannot exclude himself any rights. By adding a blank you define a default +behavior for every player who has no own registered. + +Other than adding security on software layer, you can link up your with the network and +access it wirelessly. + +### The GUI + +![Security Terminal GUI](../../../assets/content/securityTerminalGUI.png) | + +A. **Sort Order**: Toggle sorting direction + +B. **Search Box Mode**: Auto Search + +C. **** + +D. **Deposit**: User is allowed to store new items into storage + +E. **Withdraw**: User is allowed to remove items from storage + +F. **Craft**: User can inititate new crafting jobs + +G. **Build**: User can modify the physical structure of the network and make +configuration changes. + +H. **Security**: User can access and modify the security terminal of the network + +I. **Wireless Access Terminal**: Links up the WAT to the network + +J. **Linked up WAT** + +---|--- + + diff --git a/guidebook/features/me-network/misc/p2p-tunnel.md b/guidebook/features/me-network/misc/p2p-tunnel.md new file mode 100644 index 00000000000..b2fdd2a30ac --- /dev/null +++ b/guidebook/features/me-network/misc/p2p-tunnel.md @@ -0,0 +1,40 @@ +--- +categories: + - ME Network/Misc +item_ids: + - ae2:me_p2p_tunnel +navigation: + title: P2P Tunnel +--- + +The or "Point to Point Tunnel" is +a versatile configurable system to move items / redstone / power / and fluids from +one location to another though an existing [ME Network](../../me-network.md) without +storage. + + + +Tunnels are 1 input to N outputs. This means you can output to as many points +as you want, but only input at a single point per tunnel. + +Networks can support any number of tunnels, of any different types, and they +all function independently. + +ME Tunnels can be used to carry channels from one location to another, and can +carry up to 32 [channels](../channels.md), same as a , while only +requiring a single channel per point, making tunnels a very powerful tool to +expand [me networks](../../me-network.md), especially over a distance. + +The channel required by a P2P-Tunnel cannot be carried through another P2P-Tunnel. + +To configure a +you must first attune the tunnel to carry what you want it to (see below), then you need +to configure the outputs to their input. You configure the connections by +using the ; First +Shift+Right Click the input to save it on your memory card, then simply right-click the different outputs to +store the input onto the outputs. this also sets the type of the output to match the type of the input. + +## Tunnel Types + + diff --git a/guidebook/features/me-network/monitors.md b/guidebook/features/me-network/monitors.md new file mode 100644 index 00000000000..6fe25813a78 --- /dev/null +++ b/guidebook/features/me-network/monitors.md @@ -0,0 +1,39 @@ +--- +navigation: + title: Storage Monitors +item_ids: + - ae2:conversion_monitor + - ae2:storage_monitor +--- + +## Storage Monitor + +The is a simple +way to see the current level of a specified item. There are several +interactions to modify it. + +| Action | Effect | +| --------------------------------- | --------------------------------------------------------------------- | +| Right-click with item | Will display the current stored amount of that item if not locked. | +| Right-click with empty hand | Will reset the display if not locked. | +| Shift+Right-click with empty hand | Will toggle the lock. | +| Right-click with wrench | Will rotate the monitor if it is locked and on the ground or ceiling. | + + + +## Conversion Monitor + +The is the +upgraded version of the . It adds the ability to +directly withdraw from or store items into the [ME Network](../me-network.md). + +In addition to the storage monitor's interactions, conversion monitors support the following actions: + +| Action | Effect | +| --------------------------- | ----------------------------------------------------------------------- | +| Left-click | Extracts a stack of the shown item into your inventory. | +| Right-click with item | Inserts the held item into the network. | +| Right-click with empty hand | Will insert all of the shown item from your inventory into the network. | + + diff --git a/guidebook/features/me-network/network-energy.md b/guidebook/features/me-network/network-energy.md new file mode 100644 index 00000000000..3724c3cd207 --- /dev/null +++ b/guidebook/features/me-network/network-energy.md @@ -0,0 +1,82 @@ +--- +navigation: + title: Network Energy + icon: energy_cell +item_ids: + - ae2:energy_acceptor + - ae2:cable_energy_acceptor + - ae2:creative_energy_cell + - ae2:energy_cell + - ae2:dense_energy_cell + - ae2:vibration_chamber + - ae2:quartz_fiber +--- + +The ME Network needs energy to function. This energy is measured in AE per tick. + +To power your network, you can either connect a directly, +or use an to connect energy sources from compatible mods. + +Your network will have some inherent energy storage, which can be increased by connecting +energy cells. + +To see the current energy statistics for your network, right-click any part of it with a . + +## Energy Acceptor + +![Picture of a Energy Accepter.](../../assets/large/energy_accepter.png) + +The converts energy from external +systems into AE and stores it in the network. + +The following energy systems are supported: + +| Energy System | Conversion Rate | +| ---------------------------- | --------------- | +| Forge Energy / Redstone Flux | 2 FE = 1 AE | + + + + +## Energy Storage + +![A picture of a uncharged, and charged energy cell.](../../assets/large/energy_cell.png) + +Stores up to 200,000 AE. They do not accept power directly, but are used to add +additional power storage to an already existing [ME Network](../me-network.md). + + + +![A picture of a uncharged, and charged energy cell.](../../assets/large/dense_energy_cell.png) + +store AE energy up to 1.6 million units. They do not accept power directly but +are used to add additional power storage to an already existing [ME Network](../me-network.md). + + + + contain infinite AE energy and can be used +to provide power without needing to generate it. + +They can only be spawned in **Creative Mode**. + +### Vibration Chamber + +![A picture of a Vibration Chamber.](../../assets/large/vibration_chamber.png) + +A modified furnace capable of generating AE Power instead of smelting ores. When +placed on an [ME Network](../me-network.md) it will charge or +power other Network Devices. + +The will burn +almost any solid burnable fuel for power. It will slow, or accelerate the burn +depending on how much power it is able to store vs what is wasted. Generates +between 1 and 10 AE/t depending on its burn speed. + + + +### Sharing Power Between Networks + +A part designed to share energy between two [ME Network](../me-network.md)s without sharing anything else, also +used to craft . + + diff --git a/guidebook/features/me-network/network-functions/me-annihilation-plane.md b/guidebook/features/me-network/network-functions/me-annihilation-plane.md new file mode 100644 index 00000000000..5912f284360 --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-annihilation-plane.md @@ -0,0 +1,17 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:annihilation_plane +navigation: + title: ME Annihilation Plane +--- + +The is a +part designed to destroy any block put in front of it. It can buffer a single +operation. Buffered items will automatically be stored in [ME Network](../../me-network.md)'s +Storage as space is made available. + +Requires a [channel](../channels.md) to function. + + diff --git a/guidebook/features/me-network/network-functions/me-export-bus.md b/guidebook/features/me-network/network-functions/me-export-bus.md new file mode 100644 index 00000000000..3cef46c7039 --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-export-bus.md @@ -0,0 +1,25 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:export_bus +related: + - Possible Upgrades +navigation: + title: ME Export Bus +--- + +![A Image of an Export Bus](../../../assets/large/export_bus.png) + +The extracts items from the +[ME Network](../../me-network.md)'s Networked Storage and places them into the inventory it faces. +You must configure which items it will insert, leaving the configuration blank will result in nothing. + +The will try to export any of the items on its list +skipping over those it cannot fit into the destination. + +The requires a [channel](../channels.md) to function. + +This is the functional opposite of the . + + diff --git a/guidebook/features/me-network/network-functions/me-formation-plane.md b/guidebook/features/me-network/network-functions/me-formation-plane.md new file mode 100644 index 00000000000..16d851e883f --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-formation-plane.md @@ -0,0 +1,21 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:formation_plane +related: + - Possible Upgrades +navigation: + title: ME Formation Plane +--- + +The is a part +designed to place or drop any item put into the [ME Network](../../me-network.md). +It functions as storage similar to how a , so items +that are added to the network are dropped or placed passively. You can configure +the to indicate specific +items it should place. Items are placed instantly when they enter the network. + +Requires a [channel](../channels.md) to function. + + diff --git a/guidebook/features/me-network/network-functions/me-import-bus.md b/guidebook/features/me-network/network-functions/me-import-bus.md new file mode 100644 index 00000000000..4c91ca7aff3 --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-import-bus.md @@ -0,0 +1,24 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:import_bus +related: + - Possible Upgrades +navigation: + title: ME Import Bus +--- + +![A picture of an Import Bus.](../../../assets/large/import_bus.png)Pulls items from +the inventory it is pointed at and places them into the [ME Network](../../me-network.md)'s Networked Storage. +You can specify which items it will pull out via the UI, else it tries to pull out any item in the adjacent +inventory. The will +attempt to import any possible options, even if 1 or more of the configured +items cannot be stored. The requires a +[channel](../channels.md) to function. + +This is the functional opposite of the . + + diff --git a/guidebook/features/me-network/network-functions/me-interface.md b/guidebook/features/me-network/network-functions/me-interface.md new file mode 100644 index 00000000000..f019c3f9aae --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-interface.md @@ -0,0 +1,48 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:interface + - ae2:cable_interface +related: + - Possible Upgrades +navigation: + title: ME Interface +--- + +![A picture of a Interface Block.](../../../assets/large/interface.png)![A picture +of a Interface Part.](../../../assets/large/interface_module.png)The is the only component which can +be used as a part, or as a Block. Crafting an ME interface in either form by +itself produces the other form. The thin form is useful if you want to provide +several different interfaces in a single block of physical space, but each +will need its own channel. The block form lets multiple other blocks connect +to a single ME interface, using only one channel for the interface. + + + + + + +The acts as an in +between when working with pipes, tubes, networks, or machines from other mods. + +You can configure certain items to be exported from the [ME Network](../../me-network.md) into the for use with other mods. Or use +other mods to insert into any . as long as it isn't full of +exported materials it will add any added items into the [ME Network](../../me-network.md). + +The interface normally functions like a chest, however with one exception, if +you place a storage bus on an interface, you essentially include the entire +network instead, this allows networks to share huge sets of contents and to be +chained together in a very effective manner. In addition to this mode, if you +you configure your interface to explicilty provide specific materials, the +storage bus will behave as if the interface was a standard chest, disabling +this advanced feature. (As of this writing, autocrafting in another network +won't reliably use the items in a configured interface.) + +The require a +[channel](../channels.md) to function. + + diff --git a/guidebook/features/me-network/network-functions/me-level-emitter.md b/guidebook/features/me-network/network-functions/me-level-emitter.md new file mode 100644 index 00000000000..589bed751d0 --- /dev/null +++ b/guidebook/features/me-network/network-functions/me-level-emitter.md @@ -0,0 +1,28 @@ +--- +categories: + - ME Network/Network Functions +item_ids: + - ae2:level_emitter +related: + - Possible Upgrades +navigation: + title: ME Level Emitter +--- + +![An active Level Emitter](../../../assets/large/emitter2.png)The can indicate either the +level of a specified item or the [ME Network](../../me-network.md) energy +level. You can set the threshold and if the will emit a redstone signal +or turn it off. + +Requires a [channel](../channels.md) to function, if its power or +channel is lost the emitter will switch to an off state. + +These can tap into the crafting system when a crafting card is installed, +allowing you to output redstone while an item is being crafted, or even +configure the system to output redstone to preform a crafting task, its +important to note that you cannot mix level emitter craft via redstone with +interface patterns. + + diff --git a/guidebook/features/me-network/network-storage/me-chest.md b/guidebook/features/me-network/network-storage/me-chest.md new file mode 100644 index 00000000000..23a3676dd56 --- /dev/null +++ b/guidebook/features/me-network/network-storage/me-chest.md @@ -0,0 +1,50 @@ +--- +categories: + - ME Network/Network Storage +item_ids: + - ae2:chest +related: + - Other Networked Storage + - Storage Cells +navigation: + title: ME Chest +--- + +![A picture of an ME Chest](../../../assets/large/me_chest.png)The is the simplest way to use a +Storage cell, it will give you direct access to the contents of the cell +placed inside. + + + +show their contents and the storage cell status on the front face. + +- Red - Indicates the Storage Cell is Full. +- Orange - Indicates the Storage Cell cannot hold any more types, but it can store more items. +- Green - Indicates the cell can hold more types, or more items. +- Black - Indicates there is no channel, or power is offline. + + + +can be powered stand alone with external power, or as part of a [ME +Network](../../me-network.md). When on a network, the storage of the +chest will be available to any other devices in the same network using an +assigned Channel. When used without a network, the chest will not transfer +full stacks at once, capping transfers at 38 items instead. + +The consumes 1 AE/t, and +consumes a small amount based on which storage cell is installed. If powered +on its own it has a very small internal storage which only lasts a few +moments, using a or +using a battery from another mod is suggested for reliable operation. + +The has two UI's, one is +used to place the storage cell inside the device from the sides or bottom, the +other is accessed by using the top surface to access the contents. + +Items can be injected into the like any other inventory, however +items cannot be extracted with automation except via networked functions like +the . + + diff --git a/guidebook/features/me-network/network-storage/me-drive.md b/guidebook/features/me-network/network-storage/me-drive.md new file mode 100644 index 00000000000..4a7591eb996 --- /dev/null +++ b/guidebook/features/me-network/network-storage/me-drive.md @@ -0,0 +1,35 @@ +--- +categories: + - ME Network/Network Storage +item_ids: + - ae2:drive +related: + - Other Networked Storage + - Storage Cells +navigation: + title: ME Drive +--- + +![A picture of an ME Drive.](../../../assets/large/me_drive.png)A block designed to +do one thing, store Storage Cells. This block holds 10 storage cells so you +can tightly pack your storage into a very small space. + + + +show their contents and the storage cell status on the front of the drive +face. + +- Red - Indicates the Storage Cell is Full. +- Orange - Indicates the Storage Cell cannot hold any more types, but it can store more items. +- Green - Indicates the cell can hold more types, or more items. +- Black - Indicates there is no channel, or power is offline. + +Its important to note, that without a [ME Network](../../me-network.md) +this block does nothing. Its only useful when combined with a way to input, +and output items, and requires 2 AE/t power to function, and additional power +for each Storage Cell stored inside it. + +The requires a +[channel](../channels.md) to function. + + diff --git a/guidebook/features/me-network/network-storage/me-storage-bus.md b/guidebook/features/me-network/network-storage/me-storage-bus.md new file mode 100644 index 00000000000..68ddb0b15e2 --- /dev/null +++ b/guidebook/features/me-network/network-storage/me-storage-bus.md @@ -0,0 +1,44 @@ +--- +categories: + - ME Network/Network Storage +item_ids: + - ae2:storage_bus +related: + - Other Networked Storage + - Storage Cells + - Possible Upgrades +navigation: + title: ME Storage Bus +--- + +The , when attached +to another inventory block in the world lets you access that inventory via +networked functions. This allows you to use chests, barrels, or other types of +item storage in your networks. + +The storage via the +is bi-directional, it can both insert, or extract items from the inventory +block it is attached to as long as the has its required +[channel](../channels.md). + +The UI allows you to control which items are selected as storable items, this +selection has no effect on what items can be extracted once they are in the +storage. + +The Storage Bus will function with nearly any inventory block, including + +, Minefactory Reloaded DSUs, Factorization Barrels, +JABBA Barrels, and Better Storage Crates. They can also be used to route items +passivly into Buildcraft Pipes. + +If you place a storage bus on an the storage bus will be able to +interact with the full conents of the target network, unless that interface is +configured to store items inside itself, in which case it will see those +stored items. + +_ **\* Storage Buses ignore input/output sides for DSUs, Barrels, and Digital +Chests.**_ + + diff --git a/guidebook/features/me-network/powered-machines/charger.md b/guidebook/features/me-network/powered-machines/charger.md new file mode 100644 index 00000000000..35135916346 --- /dev/null +++ b/guidebook/features/me-network/powered-machines/charger.md @@ -0,0 +1,27 @@ +--- +categories: + - ME Network/Powered Machines +item_ids: + - ae2:charger + - ae2:crank +related: + - Supported Tools +navigation: + title: Charger +--- + +![A picture of a charger.](../../../assets/large/charger_with_crank.jpg) + +The provides a way to charge +supported tools through the [ME Network](../../me-network.md) or manually using a crank. + +Power can be provided via the top or bottom, via either or other Cables, or +other mod power cables. Items can be inserted or removed from any side. + +Can also be used to create +from , and from . + +To power it manually, place a crank on the top or bottom and right-click it until the item is charged. + + diff --git a/guidebook/features/me-network/powered-machines/crystal-growth-accelerator.md b/guidebook/features/me-network/powered-machines/crystal-growth-accelerator.md new file mode 100644 index 00000000000..dc5b144c14a --- /dev/null +++ b/guidebook/features/me-network/powered-machines/crystal-growth-accelerator.md @@ -0,0 +1,23 @@ +--- +categories: + - ME Network/Powered Machines +item_ids: + - ae2:quartz_growth_accelerator +navigation: + title: Crystal Growth Accelerator +--- + +Used to accelerate [crystal growth](../../crystals.md), which allows you to create and + + from crystal dust. + +Must be powered by an ME Network via the top or bottom, and consumes a steady +8 ae/t while plugged in. Crystal Seeds must be in an adjacent water block to +be effected, the seeds will shimmer more rapidly when in the presence of a +powered . + +Can only connect to cables, or other networked machines on the top and bottom +of the machine. + + diff --git a/guidebook/features/me-network/powered-machines/inscriber.md b/guidebook/features/me-network/powered-machines/inscriber.md new file mode 100644 index 00000000000..929d8e87a51 --- /dev/null +++ b/guidebook/features/me-network/powered-machines/inscriber.md @@ -0,0 +1,49 @@ +--- +categories: + - ME Network/Powered Machines +item_ids: + - ae2:inscriber +related: + - Presses +navigation: + title: Inscriber +--- + +![A Picture of an Inscriber with a press pattern in it.](../../../large/inscriber.png) + +The inscriber is used to press items using various Inscriber Plates. Each operation requires 1k AE charged up. + + + +### Recipe + + + +### The GUI + +![Inscriber GUI](../../../assets/content/inscriberGUI.png) + +A. **Top Input** automated from side with top press + +B. **Center Input** automated from any side without press + +C. **Bottom Input** automated from side with bottom press + +D. **Output** automated from any side without press + +### Upgrades + +The inscriber supports the following upgrades: + +- + +### Automation + +The inscriber can be fully automated using storage buses and various other means. Use the fact that +specific sides of the inscriber insert into specific slots to your advantage. + +An early alternative to fully automating inscribers is using hoppers for semi-automation. +Note in the following picture, the inscribers have been rotated 90° clock-wise by using +a . + +![Hopper Automation](../../../large/inscriber_hoppers.png) diff --git a/guidebook/features/me-network/powered-machines/molecular-assembler.md b/guidebook/features/me-network/powered-machines/molecular-assembler.md new file mode 100644 index 00000000000..7fbaa6842ca --- /dev/null +++ b/guidebook/features/me-network/powered-machines/molecular-assembler.md @@ -0,0 +1,34 @@ +--- +categories: + - ME Network/Powered Machines +item_ids: + - ae2:molecular_assembler +navigation: + title: Molecular Assembler +--- + +The assembler is a powered machine which crafts items, it can be upgraded by +inserting into it; +once upgraded it is very fast. + +Functions in one of two modes, single pattern mode or automatic crafting mode. + +### Single Pattern Mode + +Uses a single inserted into the assembler to craft an item +without a crafting network. + +This makes it useful in stand alone setups or in configurations where it can +be fed by other mods and even chained together to to craft a final output. +Items will be accepted from any side and exported into any available +inventories. + +### Automatic Crafting Mode + +Uses the +from attached to craft items when they are +requested by the [Crafting CPU](../../auto-crafting.md). + +**NOTE:** This mode requires that the assembler's pattern slot is empty. + + diff --git a/guidebook/features/me-network/quantum-bridge.md b/guidebook/features/me-network/quantum-bridge.md new file mode 100644 index 00000000000..aceae29a551 --- /dev/null +++ b/guidebook/features/me-network/quantum-bridge.md @@ -0,0 +1,58 @@ +--- +navigation: + title: Quantum Bridge + icon: singularity +item_ids: + - ae2:quantum_link + - ae2:quantum_ring + - ae2:quantum_entangled_singularity +--- + +![A formed Quantum Network Bridge](../../large/qnb.png) + +_Quantum Network Bridges_ can connect two networks over infinite distances and even between dimensions. +They can carry 32 channels in total (regardless of how cables are connected to each face). + +## Quantum Ring + +Eight of these blocks placed around a will create a +_Quantum Network Bridge_. Only the 4 blocks adjacent to +the will accept network connections, +the 4 corner blocks cannot connect to cables. + + + +## Quantum Link Chamber + +One of these blocks surrounded by a +will create a _Quantum Network Bridge_. This block doesn't connect to any cables and only registers +as part of the network with the full bridge is made. + +This blocks inventory can only hold a single and is +automation accessible. + + + +## Quantum Entangled Singularity + +Required to create a connection between to _Quantum Network Bridges_, they are always produced in matching +pairs, to create a connection place 1 of the pair of into the of +the bridge on each side. + +They are crafted by causing a reaction between or +and a . Any explosive force should be enough to trigger the reaction. + +**_Nearly any explosion - even creepers - will work._** + +Always produced in pairs, but only require a single . + +It might be a good idea to label these with names when you create them using the vanilla anvil. + +### Note for Anti Griefing Servers + +AE also includes a block called , this is a small craftable TNT +which can have its block damage disabled, but can still hurt a little, and +can be used as an alternative to vanilla tnt / other explosions even when +block damage is disabled. diff --git a/guidebook/features/me-network/spatial/spatial-containment-structure.md b/guidebook/features/me-network/spatial/spatial-containment-structure.md new file mode 100644 index 00000000000..5d848b8884d --- /dev/null +++ b/guidebook/features/me-network/spatial/spatial-containment-structure.md @@ -0,0 +1,36 @@ +--- +navigation: + title: Spatial Containment Structure +--- + +# Spatial Containment Structure + +A Spatial Containment Structure or SCS, is a Multiblock networked structure +that dictates a region of space as the target of the . + +The structure must be an [ME network](../../me-network.md) with a number +of which define +the target region. + +The rules for a valid SCS are, + +1. A minium size of 3x3x3 ( this will capture a single block. ) +2. All must be in the outside bounding box. +3. All must be either connected with cable or via a QNB, and on the same network. + +This also means you can only create 1 SCS per Controller. + +The Formed Status of the SCS is displayed as the color of the if it is a red color, that +means the configuration is invalid, if its a light purple color, it indicates +it is valid. The status is only available if the pylons are powered, and +connected. + +Most SCS will require to power the , however, these blocks +are not considered part of the SCS. + +**Be aware, that you travel to a dimension without a direct way to get back. +Setup your spatial IO in a way, that you can get back.** diff --git a/guidebook/features/me-network/spatial/spatial-io-port.md b/guidebook/features/me-network/spatial/spatial-io-port.md new file mode 100644 index 00000000000..9e1929b0e3b --- /dev/null +++ b/guidebook/features/me-network/spatial/spatial-io-port.md @@ -0,0 +1,41 @@ +--- +categories: + - ME Network/Spatial +item_ids: + - ae2:spatial_io_port +navigation: + title: Spatial IO Port +--- + +![A picture of a Spatial IO +Port](../../../assets/large/spatial_io_port.png) are used to capture and +deploy regions of space that are defiend by . + +To Capture/Deploy a region of space you must first construct a [Spatial +Containtment Structure](spatial-containment-structure.md), once +constructed and ready your will show your required +power, and current power, the next step would be to adjust your +[SCS](spatial-containment-structure.md) design, or to build and +power your required +or to meet +the demands of the . + +Once power is available and your [SCS](spatial-containment-structure.md) is valid, you need to insert a , , or + + +depending on the required size you may need a larger or small storage cell. + +When everything is ready, and the storage cell is placed inside the applying a redstone +signal to the +will trigger the capture/deployment of the cell into the [SCS](spatial-containment-structure.md). + +Requires a [channel](../channels.md) to function. + + diff --git a/guidebook/features/me-network/spatial/spatial-pylon.md b/guidebook/features/me-network/spatial/spatial-pylon.md new file mode 100644 index 00000000000..dd078c67bd8 --- /dev/null +++ b/guidebook/features/me-network/spatial/spatial-pylon.md @@ -0,0 +1,27 @@ +--- +categories: + - ME Network/Spatial +item_ids: + - ae2:spatial_pylon +navigation: + title: Spatial Pylon +--- + +![A Picture of a Spatial Pylon.](../../../assets/large/spatial_pylon.png)The main +block used to construct a [Spatial Containment Structure](spatial-containment-structure.md), +they must be built in straight lines, with +a minium length of 2. When powered and on a network they will either glow a +light purple, or a light red, if the color is light red, that indicates design +has an issue and needs to be adjusted. They emitt a small amount of light when +powered. + +Only useful when used in conjunction with a , All in an [ME Network](../../me-network.md) are part of the +same [SCS](spatial-containment-structure.md). + +Each Spatial Pylon Multiblock requires a [channel](../channels.md) ( 1 +per strand of blocks, not 1 per ) to function. + + diff --git a/guidebook/features/me-network/storage-cells.md b/guidebook/features/me-network/storage-cells.md new file mode 100644 index 00000000000..f2275ecdc05 --- /dev/null +++ b/guidebook/features/me-network/storage-cells.md @@ -0,0 +1,103 @@ +--- +navigation: + title: Storage Cells + icon: item_storage_cell_64k +item_ids: + - ae2:fluid_storage_cell_1k + - ae2:fluid_storage_cell_4k + - ae2:fluid_storage_cell_16k + - ae2:fluid_storage_cell_64k +--- + +Storage Cells, are one of the core mechanics of storage in Applied Energistics +2, there are three kinds: one for items, one for fluids, and one for regions of +space. + +## Item Storage Cells + +Item storage cells can hold up to 63 distinct types of items; the +number of items they can store depends in part on how many types they're +holding, and their storage capacity. + + + + + + + + +### Portable Item Storage + + + + + + + + +## Fluid Storage Cells + +Fluid storage cells can hold up to 5 distinct types of fluids; the +volume of fluid they can store depends in part on how many types they're +holding, and their storage capacity. + + + + + + + + +### Portable Fluid Storage + + + + + + + + +## Capacity Limits + +Storage cells have limits of size, and limits +of types, plus you need to consider the resource usage of your cells, to +decide what your best options are. Each storage cell can store a fixed amount +of data. Each type consumes a number of bytes (which varies with the cell +size), and each item consumes one bit of storage, so eight items consume one +byte, and a full stack of 64 consumes 8 bytes, regardless of how the item +would stack outside an ME network. For instance, 64 identical saddles don't +take up more space than 64 stone. + +Gunning straight for top tier storage cells, is not generally the best idea, +since you use more resources, but don't get any extra type storage. + +Below is a table comparing the different tiers of storage cells, how much they store, and +a rough estimate of their cost. + +### Storage Cell Contents Vs Cost + +| Cell | Bytes | Types | Byte/Type | C-Quartz | N-Quartz | Gold | Diamonds | +|-----------------------------------------|-------:|------:|----------:|---------:|---------:|-----:|---------:| +| | 1,024 | 63 | 8 | 5 | 5 | 1 | 0 | +| | 4,096 | 63 | 32 | 17 | 5 | 3 | 0 | +| | 16,384 | 63 | 128 | 51 | 10 | 9 | 1 | +| | 65,536 | 63 | 512 | 153 | 20 | 27 | 4 | + +### Storage Capacity with Varying Type Count + +| Cell | Stacks of items With 1 Item In Cell | Stacks of items With 63 Items in Cell | +|-----------------------------------------|------------------------------------:|--------------------------------------:| +| | 127 | 65 | +| | 508 | 260 | +| | 2,032 | 1,040 | +| | 8,128 | 4,160 | + +## Spatial Storage + +Storage cells for spatial I/O come in three sizes. + + + + + + diff --git a/guidebook/features/me-network/storage-cells/me-storage-housing.md b/guidebook/features/me-network/storage-cells/me-storage-housing.md new file mode 100644 index 00000000000..f17a03e1d9f --- /dev/null +++ b/guidebook/features/me-network/storage-cells/me-storage-housing.md @@ -0,0 +1,19 @@ +--- +item_ids: + - ae2:item_cell_housing + - ae2:fluid_cell_housing +related: + - Storage Cell Parts + - Storage Cells + - Spatial Cell Parts + - Spatial Cells +navigation: + title: ME Storage Housing +--- + +An empty storage container, you can insert any of the various storage cell +parts into it to create a usable storage cell matching the type of the housing +and size of the component. + + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cell-parts/128cubed-spatial-component.md b/guidebook/features/me-network/storage-cells/spatial-cell-parts/128cubed-spatial-component.md new file mode 100644 index 00000000000..d691f2fc29d --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cell-parts/128cubed-spatial-component.md @@ -0,0 +1,16 @@ +--- +categories: + - Storage Cells/Spatial Cell Parts +item_ids: + - ae2:spatial_cell_component_128 +related: + - Spatial Cell Parts + - Spatial Cells +navigation: + title: 128Cubed Spatial Component +--- + +Largest spatial storage component, used to make . + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cell-parts/16cubed-spatial-component.md b/guidebook/features/me-network/storage-cells/spatial-cell-parts/16cubed-spatial-component.md new file mode 100644 index 00000000000..69675079593 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cell-parts/16cubed-spatial-component.md @@ -0,0 +1,16 @@ +--- +categories: + - Storage Cells/Spatial Cell Parts +item_ids: + - ae2:spatial_cell_component_16 +related: + - Spatial Cell Parts + - Spatial Cells +navigation: + title: 16Cubed Spatial Component +--- + +Medium spatial storage component, used to make . + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cell-parts/2cubed-spatial-component.md b/guidebook/features/me-network/storage-cells/spatial-cell-parts/2cubed-spatial-component.md new file mode 100644 index 00000000000..a90a1247beb --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cell-parts/2cubed-spatial-component.md @@ -0,0 +1,16 @@ +--- +categories: + - Storage Cells/Spatial Cell Parts +item_ids: + - ae2:spatial_cell_component_2 +related: + - Spatial Cell Parts + - Spatial Cells +navigation: + title: 2Cubed Spatial Component +--- + +Smallets spatial storage component, used to make . + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cells/128cubed-spatial-storage-cell.md b/guidebook/features/me-network/storage-cells/spatial-cells/128cubed-spatial-storage-cell.md new file mode 100644 index 00000000000..06b88ab05a1 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cells/128cubed-spatial-storage-cell.md @@ -0,0 +1,13 @@ +--- +categories: + - Storage Cells/Spatial Cells +item_ids: + - ae2:spatial_storage_cell_128 +navigation: + title: 128Cubed Spatial Storage Cell +--- + +Used with to +store spatial regions. + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cells/16cubed-spatial-storage-cell.md b/guidebook/features/me-network/storage-cells/spatial-cells/16cubed-spatial-storage-cell.md new file mode 100644 index 00000000000..86f22101e10 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cells/16cubed-spatial-storage-cell.md @@ -0,0 +1,13 @@ +--- +categories: + - Storage Cells/Spatial Cells +item_ids: + - ae2:spatial_storage_cell_16 +navigation: + title: 16Cubed Spatial Storage Cell +--- + +Used with to +store spatial regions. + + diff --git a/guidebook/features/me-network/storage-cells/spatial-cells/2cubed-spatial-storage-cell.md b/guidebook/features/me-network/storage-cells/spatial-cells/2cubed-spatial-storage-cell.md new file mode 100644 index 00000000000..7ed0ee23e0c --- /dev/null +++ b/guidebook/features/me-network/storage-cells/spatial-cells/2cubed-spatial-storage-cell.md @@ -0,0 +1,13 @@ +--- +categories: + - Storage Cells/Spatial Cells +item_ids: + - ae2:spatial_storage_cell_2 +navigation: + title: 2Cubed Spatial Storage Cell +--- + +Used with to +store spatial regions. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cell-parts/16k-me-storage-component.md b/guidebook/features/me-network/storage-cells/storage-cell-parts/16k-me-storage-component.md new file mode 100644 index 00000000000..2597b98e5e2 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cell-parts/16k-me-storage-component.md @@ -0,0 +1,20 @@ +--- +categories: + - Storage Cells/Storage Cell Parts +item_ids: + - ae2:cell_component_16k +related: + - Storage Cell Parts + - Storage Cells +navigation: + title: 16k ME Storage Component +--- + +Second largest storage component, used to make , or can be upgraded to +make . + +Can be recovered from a crafted by fully emptying the +[storage cell](../../storage-cells.md), and shift clicking it in your hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cell-parts/1k-me-storage-component.md b/guidebook/features/me-network/storage-cells/storage-cell-parts/1k-me-storage-component.md new file mode 100644 index 00000000000..e9d229348e3 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cell-parts/1k-me-storage-component.md @@ -0,0 +1,22 @@ +--- +categories: + - Storage Cells/Storage Cell Parts +item_ids: + - ae2:cell_component_1k +related: + - Storage Cell Parts + - Storage Cells +navigation: + title: 1k ME Storage Component +--- + +Smallest storage component, used to make , or can be upgraded to +make . + +Can be recovered from a crafted by fully emptying the +[storage cell](../../storage-cells.md), and shift clicking i in your +hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cell-parts/4k-me-storage-component.md b/guidebook/features/me-network/storage-cells/storage-cell-parts/4k-me-storage-component.md new file mode 100644 index 00000000000..76b741420ee --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cell-parts/4k-me-storage-component.md @@ -0,0 +1,22 @@ +--- +categories: + - Storage Cells/Storage Cell Parts +item_ids: + - ae2:cell_component_4k +related: + - Storage Cell Parts + - Storage Cells +navigation: + title: 4k ME Storage Component +--- + +Second smallest storage component, used to make , or can be upgraded to +make . + +Can be recovered from a crafted by fully emptying the +[storage cell](../../storage-cells.md), and shift clicking i in your +hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cell-parts/64k-me-storage-component.md b/guidebook/features/me-network/storage-cells/storage-cell-parts/64k-me-storage-component.md new file mode 100644 index 00000000000..2306c22e92b --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cell-parts/64k-me-storage-component.md @@ -0,0 +1,21 @@ +--- +categories: + - Storage Cells/Storage Cell Parts +item_ids: + - ae2:cell_component_64k +related: + - Storage Cell Parts + - Storage Cells +navigation: + title: 64k ME Storage Component +--- + +Largest storage component, used to make . + +Can be recovered from a crafted by fully emptying the +[storage cell](../../storage-cells.md), and shift clicking i in your +hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cells/16k-item-storage-cell.md b/guidebook/features/me-network/storage-cells/storage-cells/16k-item-storage-cell.md new file mode 100644 index 00000000000..83eb7837b29 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cells/16k-item-storage-cell.md @@ -0,0 +1,31 @@ +--- +categories: + - Storage Cells/Storage Cells +item_ids: + - ae2:item_storage_cell_16k +related: + - Storage Cell Parts + - Storage Cells + - Possible Upgrades +navigation: + title: 16k ME Storage Cell +--- + +Middle Tier Storage Cell, which can contain 16,384 bytes of storage. + +16,384 bytes of storage can hold 2,032 Stacks of a single item. or 1,040 +Stacks,while holding 63 Different items. + +The 16k Storage Cell uses 128 bytes of data to store a single type. [Click +here for details on how storage math works.](../../storage-cells.md) + +When placed inside a drive or chest will consume 1.5 ae/t. + +Must be in a or to be usable. + +You can remove the by fully emptying the +storage cell, and sneak clicking i in your hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cells/1k-item-storage-cell.md b/guidebook/features/me-network/storage-cells/storage-cells/1k-item-storage-cell.md new file mode 100644 index 00000000000..f8db2acfc44 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cells/1k-item-storage-cell.md @@ -0,0 +1,31 @@ +--- +categories: + - Storage Cells/Storage Cells +item_ids: + - ae2:item_storage_cell_1k +related: + - Storage Cell Parts + - Storage Cells + - Possible Upgrades +navigation: + title: 1k ME Item Fluid Storage Cell +--- + +Lowest Tier Storage Cell, which can contain 1,024 bytes of storage. + +1,024 bytes of storage can hold 127 Stacks of a single item. or 65 +Stacks,while holding 63 Different items. + +The 1k Storage Cell uses 8 bytes of data to store a single type. [Click here +for details on how storage math works.](../../storage-cells.md) + +When placed inside a drive or chest will consume 0.5 ae/t. + +Must be in a or to be usable. + +You can remove the by fully emptying the storage cell, and shift-right-clicking +while holding it. + +The settings can be changed in the . + + diff --git a/guidebook/features/me-network/storage-cells/storage-cells/4k-item-storage-cell.md b/guidebook/features/me-network/storage-cells/storage-cells/4k-item-storage-cell.md new file mode 100644 index 00000000000..88d95a5fe21 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cells/4k-item-storage-cell.md @@ -0,0 +1,31 @@ +--- +categories: + - Storage Cells/Storage Cells +item_ids: + - ae2:item_storage_cell_4k +related: + - Storage Cell Parts + - Storage Cells + - Possible Upgrades +navigation: + title: 4k ME Storage Cell +--- + +Low Tier Storage Cell, which can contain 4,096 bytes of storage. + +4,096 bytes of storage can hold 508 Stacks of a single item. or 260 +Stacks,while holding 63 Different items. + +The 4k Storage Cell uses 32 bytes of data to store a single type. [Click here +for details on how storage math works.](../../storage-cells.md) + +When placed inside a drive or chest will consume 1.0 ae/t. + +Must be in a or to be usable. + +You can remove the by fully emptying the +storage cell, and sneak clicking i in your hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cells/64k-item-storage-cell.md b/guidebook/features/me-network/storage-cells/storage-cells/64k-item-storage-cell.md new file mode 100644 index 00000000000..013cfeebcad --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cells/64k-item-storage-cell.md @@ -0,0 +1,31 @@ +--- +categories: + - Storage Cells/Storage Cells +item_ids: + - ae2:item_storage_cell_64k +related: + - Storage Cell Parts + - Storage Cells + - Possible Upgrades +navigation: + title: 64k ME Storage Cell +--- + +Highest Tier Storage Cell, which can contain 65,536 bytes of storage. + +65,536 bytes of storage can hold 8,128 Stacks of a single item. or 4,160 +Stacks,while holding 63 Different items. + +The 64k Storage Cell uses 512 bytes of data to store a single type. [Click +here for details on how storage math works.](../../storage-cells.md) + +When placed inside a drive or chest will consume 2.0 ae/t. + +Must be in a or to be usable. + +You can remove the by fully emptying the +storage cell, and sneak clicking i in your hand. + + diff --git a/guidebook/features/me-network/storage-cells/storage-cells/creative-me-storage-cell.md b/guidebook/features/me-network/storage-cells/storage-cells/creative-me-storage-cell.md new file mode 100644 index 00000000000..e8e2692cff1 --- /dev/null +++ b/guidebook/features/me-network/storage-cells/storage-cells/creative-me-storage-cell.md @@ -0,0 +1,19 @@ +--- +categories: + - Storage Cells/Storage Cells +item_ids: + - ae2:creative_item_cell +navigation: + title: Creative ME Storage Cell +--- + +Creative storage cells store and provide infinite quantities of the items you +configure them to store in the , by configuring them to +store , you extract infinite , +and store infinite + +Only configured items or fluids can be stored/extracted. + +These are not intended as infinite storage, but rather as a way to generate infinite items. + +They can only be spawned in **Creative Mode**. diff --git a/guidebook/features/me-network/terminals.md b/guidebook/features/me-network/terminals.md new file mode 100644 index 00000000000..7a9911bd842 --- /dev/null +++ b/guidebook/features/me-network/terminals.md @@ -0,0 +1,80 @@ +--- +item_ids: + - ae2:crafting_terminal + - ae2:terminal + - ae2:pattern_access_terminal + - ae2:pattern_encoding_terminal +navigation: + title: Terminals +--- + +## Item Terminal + +![A picture of 3 terminals.](../../assets/large/terminal.png) + +The is a HID which gives you access +to items stored in your [ME Network](../me-network.md). This will also include +items accessible through . + +It has the ability to sort and search, as well as filter by using . It requires a [channel](channels.md) to function. + +Can be upgraded into a . + + + +## Item Crafting Terminal + +![A picture of 3 crafting terminals.](../../assets/large/crafting_terminal.png) + +The is the upgraded version of the which has an integrated crafting grid with access to +a [ME Network](../me-network.md)'s Networked Storage. + +Like the it also requires a [channel](channels.md) to function. + + + +## Pattern Access Terminal + +Gives remote access to all pattern slots for the pattern providers on your network separated +by which type of machine they are on. It will show the type of machine in the +terminal, however you can name the in the or an Anvil to alter the name +displayed in the . + +Combined with the fact that you can toggle if the pattern provider shows up at all +this gets you control over your pattern terminal's display. + +You can also choose to pattern providers whose pattern inventory is already full. + + + +## Pattern Encoding Terminal + +A specialized version of the designed to +encode into +or . + +See [auto crafting](../auto-crafting.md) for more details on automated crafting in general. + +Lets you browse the contents of your network like other terminals, but also +contains an area for designing patterns. There are two modes for pattern +encoding. Crafting Patterns, and Processing Patterns. Processing patterns are +designed for use with machines that do not use standard crafting recipes; such +as furnaces, or other machines. To select between modes, click the button to +the right of the interface; when it shows a standard crafting table, it will +create Crafting Patterns, and when it shows a furnace, it will create +Processing Patterns. + +For Crafting Patterns ("Crafts..."), you specify the input crafting materials +on a standard 3x3 crafting grid, and the output materials are determined +automatically. + +For Processing Patterns ("Creates..."), you specify the input materials and +output materials, including quantity, by placing stacks of items in the +interface. If a processing operation is not guaranteed to succeed (such as +secondary products from some machines), it will not work correctly as a +Processing Pattern. + + diff --git a/guidebook/features/me-network/wireless-access.md b/guidebook/features/me-network/wireless-access.md new file mode 100644 index 00000000000..84bba2c5c56 --- /dev/null +++ b/guidebook/features/me-network/wireless-access.md @@ -0,0 +1,47 @@ +--- +navigation: + title: Wireless Access +item_ids: + - ae2:wireless_receiver + - ae2:wireless_booster + - ae2:wireless_access_point + - ae2:wireless_terminal + - ae2:fluix_pearl +--- + +## Wireless Terminal + +After you linked up the in the , it grants portable access to the [ME Network](../me-network.md). +Put it into a to recharge it. + +Holds 1.6m AE in its battery and drains 1 AE/t for each block you are away +from the nearest . + + + +## Wireless Access Point + +![A picture of a wireless access point.](../../assets/large/wireless_access_point.png) + +Allows wireless access via a . +Range and power usage is determined based on the number of installed +into the . + +A network can have any number of with any number +of in each one, allowing you to optimize power usage +and range by altering your setup. + +Requires a [channel](channels.md) to be operational. + + + +Used to increase the range of the . + + + +## Crafting Materials + +The follow crafting materials are used in the creation of wireless network components. + + diff --git a/guidebook/features/me-quantum-network-bridge.md b/guidebook/features/me-quantum-network-bridge.md new file mode 100644 index 00000000000..ee8fb7eb4c3 --- /dev/null +++ b/guidebook/features/me-quantum-network-bridge.md @@ -0,0 +1,43 @@ +--- +navigation: + title: ME Quantum Network Bridge +--- + +# Quantum Network Bridge + +![A Quantum Network Bridge](../assets/large/quantum_network_bridge.png) + +A multiblock structure that connects 2 potentially distant network fragments +together. Created by crafting 8 and a and placing the in the center, and +sourrounding it in the 8 . + +Each Quantum Network Bridge requires power to function. This +power must be provided from the network fragment it is attached to until the +bridge is linked; at which time power from either side will be available. +However if power is lost, connectivity will fail and power will be drained +from the side the bridge is on in attempt to restore connection. + +When the Quantum Network Bridge is powered, the various blue lights on the structure will turn on and glow. +The Quantum Network Bridge requires 200 AE/t ( 100 EU/t, 400 RF/t ) + +To establish a link between 2 Quantum Network Bridges, you must +create a pair of . One of +each will be placed inside of a particular connection. Each Bridge may only +connect to one other bridge. + +You might consider renaming your to better +identify the connection. + +### Checklist + +1. Are both sides of the Quantum Network Bridge powered? The lights turn on if they are. +2. Are the matching pair of in each bridge? +3. Are both sides chunk-loaded? +4. Make sure you only have 1 controller, the other side of the bridge is still the same network. +5. Power and network connectivty must connect to the 4 edges, not the corners. diff --git a/guidebook/features/meteorites.md b/guidebook/features/meteorites.md new file mode 100644 index 00000000000..9b41889a6e9 --- /dev/null +++ b/guidebook/features/meteorites.md @@ -0,0 +1,46 @@ +--- +navigation: + title: Meteorites +item_ids: + - ae2:meteorite_compass + - ae2:sky_stone_block +--- + +### Meteorites + +Meteorites can be found on the surface or underground of the overworld, and vary in size. +They generally contain a which can contain various +ingredients required for advanced technology. You can use a to +locate meteorites near you and as you explore. + +![A picture of a meteorite.](../assets/large/meteorite.png) + +Meteorites are the only natural source of sky stone. + +### Meteorite Compass + +![A picture of a meteorite compass.](../assets/large/meteorite_compass.png) + +A compass which points to the nearest in the current world, it +has a max range of roughly 2,700 blocks. + +If it is spinning rapidly it means the current chunk your standing in contains +skystone. If its spinning slowly, it means that there is no meteorite in range, +this usually means you should do some more exploring, it will find one as new +terrain is generated, or in some cases such as the nether you can use skystone +blocks to control the compass since skystone doesn't spawn naturally. + +To craft a meteorite compass, charge a normal compass in a . +This can be done very early by using a . + +### Sky Stone + +![A Picture of Skystone.](../assets/large/sky_stone.png) + + is a blast resistant dark stone block found in +meteorites that have impacted the surface in the distant or semi recent past. Their +origin is unknown however they appear to contain remnant of technology from another +place. + + is extremely hard and requires at least a diamond +pick to mine it, the processed variants can be removed with any pick, however. diff --git a/guidebook/features/paint-ball.md b/guidebook/features/paint-ball.md new file mode 100644 index 00000000000..5ee0165df62 --- /dev/null +++ b/guidebook/features/paint-ball.md @@ -0,0 +1,12 @@ +--- +item_ids: + - ae2:white_paint_ball +navigation: + title: Paint Ball +--- + +Crafted by putting 8 around a dye in the crafting +table. Used in the , and can also be shot +with the . diff --git a/guidebook/features/presses/inscriber-calculation-press.md b/guidebook/features/presses/inscriber-calculation-press.md new file mode 100644 index 00000000000..f6ae3343999 --- /dev/null +++ b/guidebook/features/presses/inscriber-calculation-press.md @@ -0,0 +1,35 @@ +--- +categories: + - Presses + - Processor Press Plates +item_ids: + - ae2:calculation_processor_press +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Inscriber Calculation Press +--- + +Required to make with the + +. Found in that +spawn in meteorites durring world gen, and can be copied using the + +. + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/presses/inscriber-engineering-press.md b/guidebook/features/presses/inscriber-engineering-press.md new file mode 100644 index 00000000000..98260368971 --- /dev/null +++ b/guidebook/features/presses/inscriber-engineering-press.md @@ -0,0 +1,35 @@ +--- +categories: + - Presses + - Processor Press Plates +item_ids: + - ae2:engineering_processor_press +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Inscriber Engineering Press +--- + +Required to make with the + +. Found in that +spawn in meteorites durring world gen, and can be copied using the + +. + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/presses/inscriber-logic-press.md b/guidebook/features/presses/inscriber-logic-press.md new file mode 100644 index 00000000000..91d6be9fc1f --- /dev/null +++ b/guidebook/features/presses/inscriber-logic-press.md @@ -0,0 +1,30 @@ +--- +categories: + - Presses + - Processor Press Plates +item_ids: + - ae2:logic_processor_press +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Inscriber Logic Press +--- + +Required to make with the . +Found in that spawn in meteorites durring world gen, and can be copied using the . + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/presses/inscriber-name-press.md b/guidebook/features/presses/inscriber-name-press.md new file mode 100644 index 00000000000..061aa6198b5 --- /dev/null +++ b/guidebook/features/presses/inscriber-name-press.md @@ -0,0 +1,19 @@ +--- +categories: + - Presses +item_ids: + - ae2:name_press +navigation: + title: Inscriber Name Press +--- + +To craft right click the or and insert an + +, you will then have to type the name you +wish to write onto the plate then simply extract the finished plate. You can use +the in the to rename +any of your items, you can use one or two plates at a time, if you use two +plates, it will print the name using the combination of both names, top slot, +then bottom slot. diff --git a/guidebook/features/presses/inscriber-silicon-press.md b/guidebook/features/presses/inscriber-silicon-press.md new file mode 100644 index 00000000000..229f6125a54 --- /dev/null +++ b/guidebook/features/presses/inscriber-silicon-press.md @@ -0,0 +1,33 @@ +--- +categories: + - Presses + - Processor Press Plates +item_ids: + - ae2:silicon_press +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Inscriber Silicon Press +--- + +Required to make the various processors with the . Found in that spawn in meteorites +during world generation and can be copied using the . + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/processor-parts/printed-calculation-circuit.md b/guidebook/features/processor-parts/printed-calculation-circuit.md new file mode 100644 index 00000000000..226a2a0ac42 --- /dev/null +++ b/guidebook/features/processor-parts/printed-calculation-circuit.md @@ -0,0 +1,29 @@ +--- +categories: + - Processor Parts +item_ids: + - ae2:printed_calculation_processor +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Printed Calculation Circuit +--- + +Required to make . + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/processor-parts/printed-engineering-circuit.md b/guidebook/features/processor-parts/printed-engineering-circuit.md new file mode 100644 index 00000000000..45fd93e9f1b --- /dev/null +++ b/guidebook/features/processor-parts/printed-engineering-circuit.md @@ -0,0 +1,29 @@ +--- +categories: + - Processor Parts +item_ids: + - ae2:printed_engineering_processor +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Printed Engineering Circuit +--- + +Required to make . + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/processor-parts/printed-logic-circuit.md b/guidebook/features/processor-parts/printed-logic-circuit.md new file mode 100644 index 00000000000..e89d44409d2 --- /dev/null +++ b/guidebook/features/processor-parts/printed-logic-circuit.md @@ -0,0 +1,29 @@ +--- +categories: + - Processor Parts +item_ids: + - ae2:printed_logic_processor +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Printed Logic Circuit +--- + +Required to make . + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/processor-parts/printed-silicon.md b/guidebook/features/processor-parts/printed-silicon.md new file mode 100644 index 00000000000..18cff072022 --- /dev/null +++ b/guidebook/features/processor-parts/printed-silicon.md @@ -0,0 +1,26 @@ +--- +categories: + - Processor Parts +item_ids: + - ae2:printed_silicon +related: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Printed Silicon +--- + + + +### Processor Press Plates + + + +### Processors + + + +### Processor Parts + + diff --git a/guidebook/features/processors/calculation-processor.md b/guidebook/features/processors/calculation-processor.md new file mode 100644 index 00000000000..07207c9bdfb --- /dev/null +++ b/guidebook/features/processors/calculation-processor.md @@ -0,0 +1,26 @@ +--- +item_ids: + - ae2:calculation_processor +categories: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Calculation Processor +--- + +Medium tier processor. + + + +### Processor Press Plates + + +### Other Processors + +TODO EXCEPT SELF + + +### Processor Parts + + diff --git a/guidebook/features/processors/engineering-processor.md b/guidebook/features/processors/engineering-processor.md new file mode 100644 index 00000000000..0987b5be032 --- /dev/null +++ b/guidebook/features/processors/engineering-processor.md @@ -0,0 +1,28 @@ +--- +item_ids: + - ae2:engineering_processor +categories: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Engineering Processor +--- + +Most Advanced Processor. + + + +### Processor Press Plates + + + +### Other Processors + +TODO EXCEPT SELF + + + +### Processor Parts + + diff --git a/guidebook/features/processors/logic-processor.md b/guidebook/features/processors/logic-processor.md new file mode 100644 index 00000000000..3a73044155e --- /dev/null +++ b/guidebook/features/processors/logic-processor.md @@ -0,0 +1,28 @@ +--- +item_ids: + - ae2:logic_processor +categories: + - Processors + - Processor Press Plates + - Processor Parts +navigation: + title: Logic Processor +--- + +Most basic processor. + + + +### Processor Press Plates + + + +### Other Processors + +TODO EXCEPT SELF + + + +### Processor Parts + + diff --git a/guidebook/features/silicon.md b/guidebook/features/silicon.md new file mode 100644 index 00000000000..bc3bd3dceb3 --- /dev/null +++ b/guidebook/features/silicon.md @@ -0,0 +1,9 @@ +--- +item_ids: + - ae2:silicon +navigation: + title: Silicon + icon: silicon +--- + + diff --git a/guidebook/features/simple-tools/cutting-knife.md b/guidebook/features/simple-tools/cutting-knife.md new file mode 100644 index 00000000000..97f7c9de651 --- /dev/null +++ b/guidebook/features/simple-tools/cutting-knife.md @@ -0,0 +1,14 @@ +--- +navigation: + title: Cutting Knife +item_ids: + - ae2:certus_quartz_cutting_knife + - ae2:nether_quartz_cutting_knife +--- + +A cutting knife made out of or . +It can be used to make , , and . + + + diff --git a/guidebook/features/simple-tools/light-detecting-fixture.md b/guidebook/features/simple-tools/light-detecting-fixture.md new file mode 100644 index 00000000000..ceb6e173c31 --- /dev/null +++ b/guidebook/features/simple-tools/light-detecting-fixture.md @@ -0,0 +1,16 @@ +--- +categories: + - Simple Tools/Misc +item_ids: + - ae2:light_detector +navigation: + title: Light Detector +--- + +Just like a daylight sensor uses nether quartz to detect daylight, this nether +quartz fixture outputs redstone level on any nearby light sources. + +Outputs starting at light level 6 at redstone level 1 and goes up to light +level 15 redstone level 9. + + diff --git a/guidebook/features/simple-tools/quartz-tools.md b/guidebook/features/simple-tools/quartz-tools.md new file mode 100644 index 00000000000..ceff76426eb --- /dev/null +++ b/guidebook/features/simple-tools/quartz-tools.md @@ -0,0 +1,35 @@ +--- +navigation: + title: Quartz Tools +item_ids: + - ae2:certus_quartz_pickaxe + - ae2:certus_quartz_axe + - ae2:certus_quartz_shovel + - ae2:certus_quartz_hoe + - ae2:certus_quartz_sword + - ae2:nether_quartz_pickaxe + - ae2:nether_quartz_axe + - ae2:nether_quartz_shovel + - ae2:nether_quartz_hoe + - ae2:nether_quartz_sword +--- + +Basic tools can be made from both and . + +Their durability and mining speed is equivalent to iron tools. + +## Certus Quartz Tools + + + + + + + +## Nether Quartz Tools + + + + + + diff --git a/guidebook/features/simple-tools/sky-stone-chest.md b/guidebook/features/simple-tools/sky-stone-chest.md new file mode 100644 index 00000000000..11f1c1430b7 --- /dev/null +++ b/guidebook/features/simple-tools/sky-stone-chest.md @@ -0,0 +1,33 @@ +--- +categories: + - Simple Tools/Skystone Chests + - World Gen + - Decorative Blocks/Skystone Blocks +item_ids: + - ae2:sky_stone_chest + - ae2:smooth_sky_stone_chest +navigation: + title: Sky Stone Chest +--- + +Chests can be crafted from found in meteorites. +They are blast resistant and hold up to 36 stacks of items. + +
+ + + +
+ +![A picture of a sky stone chest](../../assets/large/sky_stone_chest.png) + +
+
+ + + +
+ +![A picture of a sky stone block chest](../../assets/large/sky_stone_block_chest.png) + +
diff --git a/guidebook/features/simple-tools/tiny-tnt.md b/guidebook/features/simple-tools/tiny-tnt.md new file mode 100644 index 00000000000..afcdae18f8b --- /dev/null +++ b/guidebook/features/simple-tools/tiny-tnt.md @@ -0,0 +1,14 @@ +--- +categories: + - Simple Tools/Misc +item_ids: + - ae2:tiny_tnt +navigation: + title: Tiny TNT +--- + +![A picture of Tiny TNT, next to regular TNT](../../assets/large/tiny_tnt2.png) + +A smaller, less damaging block of TNT. Useful for less destructive crafting of . + + diff --git a/guidebook/features/simple-tools/wrench.md b/guidebook/features/simple-tools/wrench.md new file mode 100644 index 00000000000..2791f7526d4 --- /dev/null +++ b/guidebook/features/simple-tools/wrench.md @@ -0,0 +1,13 @@ +--- +navigation: + title: Wrench +item_ids: + - ae2:certus_quartz_wrench + - ae2:nether_quartz_wrench +--- + +A wrench made of or . +Useful for rotating blocks, and removing individual parts from cables. + + + diff --git a/guidebook/features/singularity.md b/guidebook/features/singularity.md new file mode 100644 index 00000000000..8edf9232fe7 --- /dev/null +++ b/guidebook/features/singularity.md @@ -0,0 +1,10 @@ +--- +item_ids: + - ae2:singularity +navigation: + title: Singularity +--- + +Is used to craft and is +produced inside a . diff --git a/guidebook/features/upgrades/acceleration-card.md b/guidebook/features/upgrades/acceleration-card.md new file mode 100644 index 00000000000..dceec299d8e --- /dev/null +++ b/guidebook/features/upgrades/acceleration-card.md @@ -0,0 +1,21 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:speed_card +navigation: + title: Acceleration Card +--- + +Can be installed into , + + and to increase the speed +of items being transfered, this also increases the idle drain of the machine by 1 +ae/t per upgrade. + +You can also insert +into to increase +the number of projectiles shot per usage, this cuases the device to use +additional power for each bullet however. + + diff --git a/guidebook/features/upgrades/advanced-card.md b/guidebook/features/upgrades/advanced-card.md new file mode 100644 index 00000000000..25a1c91a2d3 --- /dev/null +++ b/guidebook/features/upgrades/advanced-card.md @@ -0,0 +1,14 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:advanced_card +navigation: + title: Advanced Card +--- + +Used to craft , + + and + + diff --git a/guidebook/features/upgrades/basic-card.md b/guidebook/features/upgrades/basic-card.md new file mode 100644 index 00000000000..691ef1fac8d --- /dev/null +++ b/guidebook/features/upgrades/basic-card.md @@ -0,0 +1,14 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:basic_card +navigation: + title: Basic Card +--- + +Used to craft and + + + + diff --git a/guidebook/features/upgrades/capacity-card.md b/guidebook/features/upgrades/capacity-card.md new file mode 100644 index 00000000000..7c1872129e9 --- /dev/null +++ b/guidebook/features/upgrades/capacity-card.md @@ -0,0 +1,16 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:capacity_card +navigation: + title: Capacity Card +--- + +Upgrades the number of configuration slots available in , , and the . + + diff --git a/guidebook/features/upgrades/crafting-card.md b/guidebook/features/upgrades/crafting-card.md new file mode 100644 index 00000000000..0ca7063e88f --- /dev/null +++ b/guidebook/features/upgrades/crafting-card.md @@ -0,0 +1,12 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:crafting_card +navigation: + title: Crafting Card +--- + +Can be used with the +or the to request +autocrafting from the system. diff --git a/guidebook/features/upgrades/fuzzy-card.md b/guidebook/features/upgrades/fuzzy-card.md new file mode 100644 index 00000000000..785d137491f --- /dev/null +++ b/guidebook/features/upgrades/fuzzy-card.md @@ -0,0 +1,50 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:fuzzy_card +navigation: + title: Fuzzy Card +--- + +Used to add fuzzy behavior to , , , , , and as well as other non +spatial [storage cells](../me-network/storage-cells.md). + +### Fuzzy Comparison Details + +Below is an example of how Fuzzy Damage comparison mods work, left side is the +bus config, top is the compared item. + +| 25% | 10% Damaged Pickaxe | 30% Damaged Pickaxe | 80% Damaged Pickaxe | Full Repair Pickaxe | +| ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| Nearly Broken Pickaxe | ✅ | \*\*\*\* | \*\*\*\* | \*\*\*\* | +| Fully Repaired Pickaxe | \*\*\*\* | ✅ | ✅ | ✅ | + +| 50% | 10% Damaged Pickaxe | 30% Damaged Pickaxe | 80% Damaged Pickaxe | Full Repair Pickaxe | +| ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| Nearly Broken Pickaxe | ✅ | ✅ | \*\*\*\* | \*\*\*\* | +| Fully Repaired Pickaxe | \*\*\*\* | \*\*\*\* | ✅ | ✅ | + +| 75% | 10% Damaged Pickaxe | 30% Damaged Pickaxe | 80% Damaged Pickaxe | Full Repair Pickaxe | +| ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| Nearly Broken Pickaxe | ✅ | ✅ | \*\*\*\* | \*\*\*\* | +| Fully Repaired Pickaxe | \*\*\*\* | | ✅ | ✅ | + +| 99% | 10% Damaged Pickaxe | 30% Damaged Pickaxe | 80% Damaged Pickaxe | Full Repair Pickaxe | +| ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| Nearly Broken Pickaxe | ✅ | ✅ | ✅ | \*\*\*\* | +| Fully Repaired Pickaxe | \*\*\*\* | \*\*\*\* | \*\*\*\* | ✅ | + +| Ignore | 10% Damaged Pickaxe | 30% Damaged Pickaxe | 80% Damaged Pickaxe | Full Repair Pickaxe | +| ---------------------- | ------------------- | ------------------- | ------------------- | ------------------- | +| Nearly Broken Pickaxe | ✅ | ✅ | ✅ | **✅** | +| Fully Repaired Pickaxe | **✅** | **✅** | **✅** | ✅ | + + diff --git a/guidebook/features/upgrades/inverter-card.md b/guidebook/features/upgrades/inverter-card.md new file mode 100644 index 00000000000..1a0aefada64 --- /dev/null +++ b/guidebook/features/upgrades/inverter-card.md @@ -0,0 +1,17 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:inverter_card +navigation: + title: Inverter Card +--- + +In non spatial [storage cells](../me-network/storage-cells.md) such as , , and the changes the standard accepted +item list into a rejection list. + + diff --git a/guidebook/features/upgrades/redstone-card.md b/guidebook/features/upgrades/redstone-card.md new file mode 100644 index 00000000000..a7b8b2ee862 --- /dev/null +++ b/guidebook/features/upgrades/redstone-card.md @@ -0,0 +1,16 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:redstone_card +navigation: + title: Redstone Card +--- + +Basic upgrade which adds the ability to be controlled by redstone signal to + +, and + +. + + diff --git a/guidebook/features/upgrades/view-cell.md b/guidebook/features/upgrades/view-cell.md new file mode 100644 index 00000000000..585d95501d7 --- /dev/null +++ b/guidebook/features/upgrades/view-cell.md @@ -0,0 +1,14 @@ +--- +categories: + - Upgrades +item_ids: + - ae2:view_cell +navigation: + title: View Cell +--- + +A item which contains a configuration similar to a Storage Cell, that allows +you to customize the filtering of a particular . + + diff --git a/guidebook/frequently-asked-questions.md b/guidebook/frequently-asked-questions.md new file mode 100644 index 00000000000..479201c2480 --- /dev/null +++ b/guidebook/frequently-asked-questions.md @@ -0,0 +1,20 @@ +--- +navigation: + title: Frequently Asked Questions +--- + +Below are a number of commonly asked questions. + +### How do I install AE2? + +Simply place the jar file in the `mods` folder of your Minecraft game with Fabric or Forge installed. + +### Can I use this in my private/public mod pack? + +Yes, don't ask for permission. For proof send people to this FAQ. + +### My game crashed or I'm having others issues with Applied Energistics 2. + +If you've tried everything you can think of, and you think something is +behaving oddly or crashing your game, the best way to send us bug reports is +the [bug tracker](https://github.com/AppliedEnergistics/Applied-Energistics-2/issues). diff --git a/guidebook/getting-started.md b/guidebook/getting-started.md new file mode 100644 index 00000000000..df8c6bc12b9 --- /dev/null +++ b/guidebook/getting-started.md @@ -0,0 +1,170 @@ +--- +navigation: + title: Getting Started (1.19+) +--- + +
+ The following information only applies to Applied Energistics 2 in Minecraft + 1.19 and newer. +
+ +## Early Game Progression + +- Build a , and power it with a or a generators from others + mods. You can also power it by hand using a . +- Put a Vanilla compass in the charger and charge it to craft a . +- Use the meteorite compass to find meteorites, which have a chance to contain quartz blocks and budding quartz. +- To progress, you need to create . You have two options: + - Use the compass to find a [meteorite](./features/meteorites.md), where you might find + some . +- To craft the processors required for more advanced machines, you'll need the . Each type of + processor has an associated press, which you will find in [meteorites](./features/meteorites.md). + +## My Very First Quartz + +The first step to getting started in AE2 is to acquire Quartz. AE2 itself +adds , and makes use of +vanilla's as well. The first tier of AE2 tech, such as +the +, , +and [Certus Quartz tools](./features/simple-tools/quartz-tools.md), use as the +primary crafting ingredient. + +The primary way of obtaining quartz is from breaking quartz crystal clusters. These grow on budding quartz in a way +that is similar to Vanillas amethyst. Budding quartz decays when growing buds, but can be recharged using +in a puddle of water. New budding quartz can be created the same way by using a . + +Meteorites will sometimes contain a flawless budding quartz. + +## Alright, I have a bunch of Certus and Nether Quartz; how do I move up in the world? + +After some investigation, you've probably noticed that to move up through the tech tree you +need . are made through the following +process: + +Throw , , and into a pool of +water and wait. This will create . + +You can create in a Charger, which can be powered +by a full of coal. + +Since growing crystals without any accelerators takes a long time, you should invest your first fluix crystals into +building as many as you can. + +After this, you're set to start on the next level of tech with an . + +## Unlocking Technology - Hunting For The Last Few Pieces + +### I can't make any of the circuits, and the plates for the Inscriber don't have a crafting recipe. Is AE2 broken? + +No, AE2 isn't broken. The "final" pieces of the AE2 puzzle needed to move up the tech tree into ME Networks are the +Inscriber Presses. There are four presses that you need (Listed in order of "tier"): + + + + + + + + +These presses are used to make the Circuits needed for the Tech 2 machines and beyond. They're found randomly +in , which are located within Meteorites. Meteorites are randomly +spawned throughout the world, normally underground. So, the hunt begins! + +This hunt is a little less aimless than the hunt for your first . You'll have a tool to help you on your way, the . The Compass will point you toward the chunk the meteor has generated in, not the specific block or the +center of the meteor itself. You'll have to do some digging and searching in order to find the meteor, and then you'll +have to take it apart and find the center, which is where the will +be located. + +### I did it! + +After this is where the fun starts. You now have all the tools to start making the complex pieces that Applied +Energistics has to offer! Get out there and start filling up data drives. + +## Matter Energy Tech: ME Networks and Storage + +### What is ME Storage? + +Its pronounced Emm-Eee, and stands for Matter Energy. + +Matter Energy is the main component of Applied Energistics 2, it's like a mad scientist version of a Multi-Block chest, +and it can revolutionize your storage situation. ME is extremely different then other storage systems in Minecraft, and +it might take a little out of the box thinking to get used to; but once you get started vast amounts of storage in tiny +space, and multiple access terminals are just the tip of the iceberg of what becomes possible. + +### What do I need to know to get started? + +First, ME Stores items inside of other items, called Storage Cells; there are 4 tiers with ever increasing amounts of +storage. In order to use a Storage Cell it must be placed inside either an , +or an . + + + +The shows you the contents of the Cell as soon as its placed inside, and you +can add and remove items from it as if it were a , with the exception that the items are +actually stored in the Storage cells, and not the itself. + +While the is a great way to get introduced to the concept of ME, to really +take advantage you need to set up an [ME Network](features/me-network.md). + +### How do I setup my first network? + +An [ME Network](features/me-network.md) is pretty easy to get started you need 2 things, +an / or + +, and an ( or ) you'll also need some kind of cable, such as to attach the too. + +Place all these next to each other, and you have the world's simplest network, storage and access. + +You can add storage cells to the , or use one in a for storage, and access it all from the . + +You might want to add more to other rooms, for this you'll want to make +some , any ME Blocks attached +to will be connected to the [ME Network](features/me-network.md) + +### Expanding your Network + +So you have some basic storage, and access to that storage, its a good start, but you'll likely be looking to maybe +automate some processing. + +A great example of this is to place a on the top of a furnace to +dump in ores, and a +on the bottom of the furance to extract furnaced ores. + +The lets you export items from the network, into the attached +inventory, while the imports items from the attached inventory into +the network. + +### Overcoming Limits + +At this point you probably getting close to 8 or so devices, once you hit 9 devices you'll have to start +managing [channels](features/me-network/channels.md). Many devices but not all, require a [channel](features/me-network/channels.md) to +function. If the device deals solely with power, or connectivity like cables the device will not require +a [channel](features/me-network/channels.md). Anything that uses items, or moves them around, will. + +By default network can support 8 [channels](features/me-network/channels.md), once you break this limit, you'll have to add +an to your network. this allows you to expand your network greatly. + +Each face of the controller will output 32 [channels](features/me-network/channels.md), depending on whats is accepting +these [channels](features/me-network/channels.md) will determine how they get used, for instance, if you place a next to the controller you will be able to carry a full 32 +[channels](features/me-network/channels.md), however if you place a next to it, or +non-dense cable, you will only get 8 [channels](features/me-network/channels.md). + +### Tunneling + +So you're getting things started, but getting [channels](features/me-network/channels.md) +where you want them is kind of a nusance. Its time to start using . +When configured for ME, they allow you to move [channels](features/me-network/channels.md) from point to point. this allows you to +move up to 32 [channels](features/me-network/channels.md) per pair of . + +![A example of using P2P Tunnels to move channels.](assets/large/tunnelchannels.png) diff --git a/guidebook/index.md b/guidebook/index.md new file mode 100644 index 00000000000..6e6ddbe372b --- /dev/null +++ b/guidebook/index.md @@ -0,0 +1,44 @@ +--- +navigation: + title: About Applied Energistics +path: / +--- + +
+ This website is for the most recent versions of Applied Energistics 2, which + usually supports the latest version of Minecraft. For Minecraft 1.16 and + older, you can find more appropriate information in the{" "} + archived wiki. The{" "} + wiki for AE1 is also archived. +
+ +### What is Applied Energistics 2? + +Applied Energistics 2 is a mod for [Minecraft](https://www.minecraft.net/) which contains a large amount of new +content, mostly centered around the concept of using Energy, and the Transformation of Energy in a unique way. +Most features relate, or are part of the core mechanic, the [ME Network](features/me-network.md). + +Applied Energistics 2 is available for both the [Fabric](https://fabricmc.net/) and [Forge](https://www.minecraftforge.net) +modding platforms. Please see the [downloads page](/download) for details. + +#### Applied Energistics 2 - [ME Networks](features/me-network.md) provide: + +- Modular Robust automation tools and great support for working with other automation mods. + - + - + - + - + - Unique storage system using , Storage Cells and es. +- to transmit signals, items, fluids and + other concepts over common cables. +- Less time wasted walking back to your chests when working in your base with the . +- Hide your cables with and full support for Forge Multipart. +- Lots of neat new decorative blocks like , , and . +- and so much more! + +#### Getting Started + +- [Getting Started](getting-started.md) +- [Channels](features/me-network/channels.md) +- [Miscellaneous Tips](miscellaneous-tips.md) +- [Video Spotlights](video-spotlights.md) diff --git a/guidebook/materials/ender_dust.md b/guidebook/materials/ender_dust.md new file mode 100644 index 00000000000..aa6a36ebd95 --- /dev/null +++ b/guidebook/materials/ender_dust.md @@ -0,0 +1,12 @@ +--- +navigation: + title: Ender Dust + parent: getting-started.md + icon: ender_dust +item_ids: + - ae2:ender_dust +--- + + is created by shattering an in +a normal blast furnace. + diff --git a/guidebook/materials/fluix_pearl.md b/guidebook/materials/fluix_pearl.md new file mode 100644 index 00000000000..5fda529a705 --- /dev/null +++ b/guidebook/materials/fluix_pearl.md @@ -0,0 +1,10 @@ +--- +navigation: + title: Fluix Pearl + parent: getting-started.md + icon: ae2:fluix_pearl +item_ids: + - ae2:fluix_pearl +--- + + diff --git a/guidebook/miscellaneous-tips.md b/guidebook/miscellaneous-tips.md new file mode 100644 index 00000000000..e96a4c6b1b1 --- /dev/null +++ b/guidebook/miscellaneous-tips.md @@ -0,0 +1,53 @@ +--- +navigation: + title: Miscellaneous Tips +--- + +### How Items are Placed + +Items entering the network will start at the highest priority storage, as +their first destination, in the case of two storages have the same priority, +if one already contains the item, they will prefer that storage over any +other. Any Whitelisted cells will be treated as already containing the item +when in the same priority group as other storages. + +### Upgrading Storage Cells + +If you have an EMPTY Storage cell any tier you can remove the +Cell/Segment/Block/Cluster from the housing by shift + right clicking with it +in your hand, so you can store it or use it to make bigger cells. it also +gives you an empty storage cell housing to re-insert a cell into. + +### Colored Terminals / Monitors + +When you place a or +other monitors on a cable, they take on the color of that cable, so if the cable is +blue, so will the screen of the placed part. + +### One Way Network Connections + +You can hook up a Storage Bus to a interface on a seperate network, to provide +a one way connection, allowing you to create public / private networks. This +requires that the Interface be unconfigured, if the interface is configured to +store items, it will instead see the items in the inventory. + +### Rotating Blocks + +You can rotate most blocks by using a Buildcraft Compatible Wrench, such as +the . + +### Setting Priority + +You can set Storage Priorities on , or in the Priority Tab on the +right top side. Higher Priorities are more imporant then lower ones and by +default all storages are set to 0. + +### Removing Blocks / Parts + +You can Shift + Rightclick with a Buildcraft Compatible Wrench and it will +dismantle the AE Block or Part and dropping it for you, this is most useful +with Parts as if you use a pick it will drop any cable, and parts in the +block, using a wrench lets you only take off a single part. diff --git a/guidebook/sandbox.md b/guidebook/sandbox.md new file mode 100644 index 00000000000..37c9529c1c3 --- /dev/null +++ b/guidebook/sandbox.md @@ -0,0 +1,29 @@ +--- +navigation: + title: "!Sandbox" +--- + + + + + + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi varius egestas augue, non lacinia ligula tristique eu. Integer tempus orci in nulla aliquet porttitor. In justo purus, ullamcorper vitae elit vitae, condimentum gravida erat. Duis eget eleifend leo. Aenean tincidunt mi risus, eu varius ipsum pretium sit amet. Nullam tincidunt leo ligula, quis blandit sem pellentesque vel. Vestibulum non auctor diam, eget ullamcorper velit. Maecenas venenatis neque at odio cursus sollicitudin. Fusce non ipsum nisi. Nam bibendum purus dolor, vitae viverra neque fermentum vitae. + +
+ +# Headline + +Integer mattis turpis enim, vel semper massa aliquet ac. Vivamus porta, ante vitae ullamcorper rutrum, metus magna sagittis risus, id dignissim odio leo et lectus. Nam sit amet nunc sit amet ligula finibus euismod sed dignissim nisi. Curabitur at euismod velit. Mauris sollicitudin egestas massa, ac mollis turpis rutrum porta. Curabitur in diam nec elit feugiat accumsan. Donec sed enim rhoncus orci blandit pretium non a urna. Duis viverra mauris nec dui faucibus finibus. Donec commodo cursus risus, eget lobortis purus rutrum vitae. Nam quis ultrices turpis. + +Nunc auctor sed eros suscipit porta. Vivamus eleifend convallis mauris dapibus pretium. Fusce consequat, velit eu volutpat rutrum, tortor velit pretium sapien, et auctor sem lorem ac augue. Nam nulla felis, pellentesque quis convallis eget, euismod quis orci. Praesent convallis, lectus et gravida dapibus, velit diam feugiat mauris, in mollis est neque at erat. Aenean vestibulum pretium tortor ut bibendum. Mauris rutrum sollicitudin bibendum. Donec erat eros, volutpat ac faucibus eu, hendrerit a ipsum. + +Sed eu arcu in nisl efficitur maximus sit amet eget est. Morbi ac purus imperdiet, euismod odio vitae, viverra elit. Nullam neque urna, sollicitudin a gravida ut, posuere porttitor neque. Cras cursus vulputate ultricies. Pellentesque est ipsum, hendrerit in varius a, efficitur eget augue. In fringilla ultrices blandit. Morbi a arcu a urna gravida suscipit congue et mauris. + +Morbi a ullamcorper enim. Sed feugiat augue magna, sollicitudin volutpat est hendrerit at. Sed turpis arcu, placerat in condimentum in, sollicitudin ultrices est. Donec non velit id tellus sodales imperdiet. Ut blandit magna nibh, id tincidunt orci convallis sit amet. Praesent consectetur felis nec sapien feugiat, in congue ex volutpat. Nam sollicitudin venenatis porta. Aenean at fringilla diam, quis auctor erat. + +Sed et lorem commodo, rutrum eros sed, pharetra dolor. Morbi ultricies lectus sem, nec commodo eros consectetur et. Aenean efficitur arcu leo, vitae placerat erat convallis sed. Nulla sit amet purus lacus. Nulla hendrerit risus eget mauris accumsan, mattis congue sapien eleifend. Maecenas eu efficitur erat, a consequat sapien. Sed ac massa in odio mattis aliquam. Donec ultricies sem at augue imperdiet, a feugiat enim porta. Etiam rhoncus ullamcorper felis, eget molestie tortor maximus eget. Aliquam erat volutpat. Sed fringilla, diam non euismod pretium, turpis nisl mollis diam, quis sagittis urna ligula et ipsum. Curabitur sagittis massa urna, ac malesuada nunc egestas a. + +Duis faucibus justo ligula, at tristique magna sagittis ut. Suspendisse a odio cursus, posuere nisi et, congue erat. In fermentum fringilla libero venenatis dapibus. Morbi eu augue convallis purus pulvinar hendrerit eget ac ligula. Duis erat nisl, elementum ut mattis vitae, pellentesque eu nunc. Sed accumsan ipsum quis dolor dictum, molestie egestas sem ultrices. Ut laoreet elementum dolor, at vulputate turpis convallis ac. Aliquam condimentum lacinia dui ut commodo. Aliquam at fringilla nulla. Pellentesque et consectetur tellus, at ultrices arcu. Nullam fringilla, lacus vel tempus viverra, enim lectus feugiat erat, id feugiat augue libero vitae nunc. Suspendisse porttitor augue vel magna convallis ornare. Nulla eget mollis arcu. Curabitur maximus pretium tortor. Nullam leo nulla, bibendum vel eros sed, pulvinar faucibus sem. Aenean viverra lacinia bibendum. + +Suspendisse enim mi, pharetra eget vestibulum non, sagittis sed leo. Praesent in blandit metus. Praesent mollis tempus dolor cursus condimentum. Morbi ac porta augue, in scelerisque turpis. Proin facilisis, sapien ac fringilla scelerisque, nibh dolor gravida ante, at lacinia leo nisi ac mauris. Morbi consequat, dolor nec mattis pharetra, sapien nisl ultrices mi, quis suscipit diam enim consequat sapien. In eget ornare lacus. Etiam neque lectus, iaculis a arcu sit amet, vehicula rhoncus ipsum. Nam aliquet leo non dui ornare sollicitudin. Phasellus pretium at ligula ut commodo. \ No newline at end of file diff --git a/guidebook/video-spotlights.md b/guidebook/video-spotlights.md new file mode 100644 index 00000000000..0a1924b2667 --- /dev/null +++ b/guidebook/video-spotlights.md @@ -0,0 +1,89 @@ +--- +navigation: + title: Video Spotlights +--- + +Below are a set of various spotlights about the mod. + +## rv0 : Direwolf 20 - Part 1 / 3 + +## English + +**Version** | **Youtuber** | **Video +** +---|---|--- +31-03-15 **rv2** | Nonsanity | [Getting +Started](https://www.youtube.com/watch?v=-sEiNbm1DdU) +23-01-15 **rv2** | iskal85 | [Channels, Tunnels and Auto- +Crafting](https://www.youtube.com/watch?v=0ZtRR4a5P7Q) +16-09-14 **rv1** | therealtkh | [Wireless Terminals / Access +Points](http://youtu.be/sXqBi2MS6eQ) +09-09-14 **rv1** | therealtkh | [Paint Balls](http://youtu.be/jNQ_9j8m090) +22-08-14 **rv0** | therealtkh | [Matter +Cannon](https://www.youtube.com/watch?v=xcTjMVdod9g) +16-08-14 **rv1** | AlgorithmX2 | [Inscriber +Autocrafting](https://www.youtube.com/watch?v=mz3zJrf3Y8s) +05-08-14 **rv0** | therealtkh | [ME Buses And Upgrade +Cards](https://www.youtube.com/watch?v=G5IGTWJ_j0U) +15-07-14 **rv0** | therealtkh | [ME Controller +Networks](https://www.youtube.com/watch?v=QZ9OCT2S2YI) +14-07-14 **rv1** | BevoLJ | [Intro to +Crafting](https://www.youtube.com/watch?v=QfyU9O_nKCo) +07-07-14 **rv0** | therealtkh | [Decorative And Useful +Things](https://www.youtube.com/watch?v=QJElQokcvbY&list) +28-06-14 **rv0** | therealtkh | [ME Networks and +Storage](https://www.youtube.com/watch?v=uiIDJVKGjqA) +19-06-14 **rv0** | therealtkh | [Getting Started - +Resources](https://www.youtube.com/watch?v=F43MLiuEtWs) +13-04-14 **rv0** | Direwolf20 | [Spotlight +3/3](https://www.youtube.com/watch?v=cy1_vlgPfII) +12-04-14 **rv0** | Direwolf20 | [Spotlight +2/3](https://www.youtube.com/watch?v=7xPDdoQP6yc) +11-04-14 **rv0** | AlgorithmX2 | [Channels explanation and +demonstration](https://www.youtube.com/watch?v=sLI1mGna3Vc) +11-04-14 **rv0** | Direwolf20 | [Spotlight +1/3](https://www.youtube.com/watch?v=IzstD3eV2FI) +09-04-14 **rv0** | Danilus | [First +look](https://www.youtube.com/watch?v=xoeGrQyVfCc) +02-04-14 **rv0** | Mwizard10 | +[Spotlight](https://www.youtube.com/watch?v=7pnLGZVZ9iY) +03-03-14 **rv0** | BevoLJ | +[Meteors](https://www.youtube.com/watch?v=GPxOiMm6c30) +15-02-14 **rv0** | BevoLJ | [Looking at new +features](https://www.youtube.com/watch?v=6ktv7iKN5pI) +05-02-14 **rv0** | BevoLJ | [AE2 +preview](https://www.youtube.com/watch?v=XEIHvG_4EsA) + +## Non-English + +**Version** | **Youtuber** | **Video +** +---|---|--- +08-15-14 **rv1** | Galvas Play (Portuguese) | [Spotlight +2/x](https://www.youtube.com/watch?v=_CfHb9Y6RRY) +06-08-14 **rv1** | Galvas Play (Portuguese) | [Spotlight +1/x](https://www.youtube.com/watch?v=9dtcy17dciE) +27-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +6/6](https://www.youtube.com/watch?v=XqSpqvbu5vk) +23-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +5/6](https://www.youtube.com/watch?v=E37kkdX6_Vw) +19-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +4/6](https://www.youtube.com/watch?v=wF61RPWrX_c) +17-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +3/6](https://www.youtube.com/watch?v=Ko8Td-PzRO8) +14-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +2/6](https://www.youtube.com/watch?v=V-y-rzs-cEA) +12-06-14 **rv0** | Nedrek (Spanish) | [Spotlight +1/6](https://www.youtube.com/watch?v=lFWRzpDX64w) +10-06-14 **rv0** | Tuberizing (Portuguese) | [Spotlight +3/3](https://www.youtube.com/watch?v=cI039ZiTCA0) +10-06-14 **rv0** | Tuberizing (Portuguese) | [Spotlight +2/3](https://www.youtube.com/watch?v=sO27NyhY3Jc) +10-06-14 **rv0** | Tuberizing (Portuguese) | [Spotlight +1/3](https://www.youtube.com/watch?v=TxmM09zm4A8) +10-04-14 **rv0** | iFebag (Italian) | +[Showcase](https://www.youtube.com/watch?v=Y5xHenMcRmc) +06-04-14 **rv0** | GorohCraft (Russian) | [Mod overview +1/x](https://www.youtube.com/watch?v=PKemuHMxwNQ) +05-04-14 **rv0** | AlFox (Russian) | +[Overview](https://www.youtube.com/watch?v=9GyZJDOjwvE) diff --git a/libs/markdown/LICENSE b/libs/markdown/LICENSE new file mode 100644 index 00000000000..539c428019a --- /dev/null +++ b/libs/markdown/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2020 Titus Wormer +Copyright (c) 2022 Sebastian Hartte + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/libs/markdown/README.md b/libs/markdown/README.md new file mode 100644 index 00000000000..4d69c1066ff --- /dev/null +++ b/libs/markdown/README.md @@ -0,0 +1,12 @@ +# AE2 Markdown Libraries + +This is a Java Port of the following projects from the JavaScript ecosystem: + +- https://github.com/micromark/micromark +- https://github.com/micromark/micromark-extension-gfm +- https://github.com/micromark/micromark-extension-mdx +- https://github.com/syntax-tree/mdast (specification) +- https://github.com/syntax-tree/unist (specification) +- https://github.com/syntax-tree/mdast-util-mdx (and related) + +All of these projects are licensed under MIT, as is this port library. diff --git a/libs/markdown/build.gradle b/libs/markdown/build.gradle new file mode 100644 index 00000000000..99c0f4825b8 --- /dev/null +++ b/libs/markdown/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java-library' +} + +dependencies { + implementation 'org.slf4j:slf4j-api:1.7.36' + + compileOnly 'org.jetbrains:annotations:23.0.0' + implementation 'com.google.code.gson:gson:2.10' + + testCompileOnly 'org.jetbrains:annotations:23.0.0' + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.36' +} + +test { + useJUnitPlatform() +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdAst.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdAst.java new file mode 100644 index 00000000000..ae7a4eee7c3 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdAst.java @@ -0,0 +1,14 @@ +package appeng.libs.mdast; + +import appeng.libs.mdast.model.MdAstRoot; +import appeng.libs.micromark.Micromark; + +public final class MdAst { + private MdAst() { + } + + public static MdAstRoot fromMarkdown(String markdown, MdastOptions options) { + var evts = Micromark.parseAndPostprocess(markdown, options); + return new MdastCompiler(options).compile(evts); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdAstYamlFrontmatter.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdAstYamlFrontmatter.java new file mode 100644 index 00000000000..7db3eccd488 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdAstYamlFrontmatter.java @@ -0,0 +1,24 @@ +package appeng.libs.mdast; + +import appeng.libs.mdast.model.MdAstAnyContent; +import appeng.libs.mdast.model.MdAstNode; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class MdAstYamlFrontmatter extends MdAstNode implements MdAstAnyContent { + public String value = ""; + + public MdAstYamlFrontmatter() { + super("yamlFrontmatter"); + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("value").value(value); + } + + @Override + public void toText(StringBuilder buffer) { + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdastCompiler.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdastCompiler.java new file mode 100644 index 00000000000..863eb010cde --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdastCompiler.java @@ -0,0 +1,1057 @@ +package appeng.libs.mdast; + +import appeng.libs.mdast.model.MdAstBlockquote; +import appeng.libs.mdast.model.MdAstBreak; +import appeng.libs.mdast.model.MdAstCode; +import appeng.libs.mdast.model.MdAstDefinition; +import appeng.libs.mdast.model.MdAstEmphasis; +import appeng.libs.mdast.model.MdAstHTML; +import appeng.libs.mdast.model.MdAstHeading; +import appeng.libs.mdast.model.MdAstImage; +import appeng.libs.mdast.model.MdAstImageReference; +import appeng.libs.mdast.model.MdAstInlineCode; +import appeng.libs.mdast.model.MdAstLink; +import appeng.libs.mdast.model.MdAstLinkReference; +import appeng.libs.mdast.model.MdAstList; +import appeng.libs.mdast.model.MdAstListItem; +import appeng.libs.mdast.model.MdAstLiteral; +import appeng.libs.mdast.model.MdAstNode; +import appeng.libs.mdast.model.MdAstParagraph; +import appeng.libs.mdast.model.MdAstParent; +import appeng.libs.mdast.model.MdAstPhrasingContent; +import appeng.libs.mdast.model.MdAstPosition; +import appeng.libs.mdast.model.MdAstReferenceType; +import appeng.libs.mdast.model.MdAstRoot; +import appeng.libs.mdast.model.MdAstStrong; +import appeng.libs.mdast.model.MdAstText; +import appeng.libs.mdast.model.MdAstThematicBreak; +import appeng.libs.micromark.Assert; +import appeng.libs.micromark.DecodeString; +import appeng.libs.micromark.ListUtils; +import appeng.libs.micromark.NamedCharacterEntities; +import appeng.libs.micromark.NormalizeIdentifier; +import appeng.libs.micromark.Point; +import appeng.libs.micromark.Token; +import appeng.libs.micromark.TokenProperty; +import appeng.libs.micromark.TokenizeContext; +import appeng.libs.micromark.Tokenizer; +import appeng.libs.micromark.Types; +import appeng.libs.micromark.html.HtmlContextProperty; +import appeng.libs.micromark.html.NumericCharacterReference; +import appeng.libs.micromark.symbol.Codes; +import appeng.libs.micromark.symbol.Constants; +import appeng.libs.unist.UnistPoint; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +final class MdastCompiler implements MdastContext { + + private static final TokenProperty SPREAD = new TokenProperty<>(); + + private final MdastExtension extension; + + boolean expectingFirstListItemValue; + boolean flowCodeInside; + boolean setextHeadingSlurpLineEnding; + boolean atHardBreak; + MdAstReferenceType referenceType; + boolean inReference; + CharacterReferenceType characterReferenceType; + + private final Map, Object> extensionData = new IdentityHashMap<>(); + + private List stack; + private List tokenStack; + @Nullable + private TokenizeContext currentTokenContext; + private final StringBuilder stringBuffer = new StringBuilder(); + + MdastCompiler(MdastOptions options) { + var extensionBuilder = MdastExtension.builder() + .canContainEol( + "emphasis", + "fragment", + "heading", + "paragraph", + "strong" + ) + .enter("autolink", opener(this::link)) + .enter("autolinkProtocol", this::onenterdata) + .enter("autolinkEmail", this::onenterdata) + .enter("atxHeading", opener(this::heading)) + .enter("blockQuote", opener(this::blockQuote)) + .enter("characterEscape", this::onenterdata) + .enter("characterReference", this::onenterdata) + .enter("codeFenced", opener(this::codeFlow)) + .enter("codeFencedFenceInfo", this::buffer) + .enter("codeFencedFenceMeta", this::buffer) + .enter("codeIndented", opener(this::codeFlow, this::buffer)) + .enter("codeText", opener(this::codeText, this::buffer)) + .enter("codeTextData", this::onenterdata) + .enter("data", this::onenterdata) + .enter("codeFlowValue", this::onenterdata) + .enter("definition", opener(this::definition)) + .enter("definitionDestinationString", this::buffer) + .enter("definitionLabelString", this::buffer) + .enter("definitionTitleString", this::buffer) + .enter("emphasis", opener(this::emphasis)) + .enter("hardBreakEscape", opener(this::hardBreak)) + .enter("hardBreakTrailing", opener(this::hardBreak)) + .enter("htmlFlow", opener(this::html, this::buffer)) + .enter("htmlFlowData", this::onenterdata) + .enter("htmlText", opener(this::html, this::buffer)) + .enter("htmlTextData", this::onenterdata) + .enter("image", opener(this::image)) + .enter("label", this::buffer) + .enter("link", opener(this::link)) + .enter("listItem", opener(this::listItem)) + .enter("listItemValue", this::onenterlistitemvalue) + .enter("listOrdered", opener(this::list, this::onenterlistordered)) + .enter("listUnordered", opener(this::list)) + .enter("paragraph", opener(this::paragraph)) + .enter("reference", this::onenterreference) + .enter("referenceString", this::buffer) + .enter("resourceDestinationString", this::buffer) + .enter("resourceTitleString", this::buffer) + .enter("setextHeading", opener(this::heading)) + .enter("strong", opener(this::strong)) + .enter("thematicBreak", opener(this::thematicBreak)) + .exit("atxHeading", closer()) + .exit("atxHeadingSequence", this::onexitatxheadingsequence) + .exit("autolink", closer()) + .exit("autolinkEmail", this::onexitautolinkemail) + .exit("autolinkProtocol", this::onexitautolinkprotocol) + .exit("blockQuote", closer()) + .exit("characterEscapeValue", this::onexitdata) + .exit("characterReferenceMarkerHexadecimal", this::onexitcharacterreferencemarker) + .exit("characterReferenceMarkerNumeric", this::onexitcharacterreferencemarker) + .exit("characterReferenceValue", this::onexitcharacterreferencevalue) + .exit("codeFenced", closer(this::onexitcodefenced)) + .exit("codeFencedFence", this::onexitcodefencedfence) + .exit("codeFencedFenceInfo", this::onexitcodefencedfenceinfo) + .exit("codeFencedFenceMeta", this::onexitcodefencedfencemeta) + .exit("codeFlowValue", this::onexitdata) + .exit("codeIndented", closer(this::onexitcodeindented)) + .exit("codeText", closer(this::onexitcodetext)) + .exit("codeTextData", this::onexitdata) + .exit("data", this::onexitdata) + .exit("definition", closer()) + .exit("definitionDestinationString", this::onexitdefinitiondestinationstring) + .exit("definitionLabelString", this::onexitdefinitionlabelstring) + .exit("definitionTitleString", this::onexitdefinitiontitlestring) + .exit("emphasis", closer()) + .exit("hardBreakEscape", closer(this::onexithardbreak)) + .exit("hardBreakTrailing", closer(this::onexithardbreak)) + .exit("htmlFlow", closer(this::onexithtmlflow)) + .exit("htmlFlowData", this::onexitdata) + .exit("htmlText", closer(this::onexithtmltext)) + .exit("htmlTextData", this::onexitdata) + .exit("image", closer(this::onexitimage)) + .exit("label", this::onexitlabel) + .exit("labelText", this::onexitlabeltext) + .exit("lineEnding", this::onexitlineending) + .exit("link", closer(this::onexitlink)) + .exit("listItem", closer()) + .exit("listOrdered", closer()) + .exit("listUnordered", closer()) + .exit("paragraph", closer()) + .exit("referenceString", this::onexitreferencestring) + .exit("resourceDestinationString", this::onexitresourcedestinationstring) + .exit("resourceTitleString", this::onexitresourcetitlestring) + .exit("resource", this::onexitresource) + .exit("setextHeading", closer(this::onexitsetextheading)) + .exit("setextHeadingLineSequence", this::onexitsetextheadinglinesequence) + .exit("setextHeadingText", this::onexitsetextheadingtext) + .exit("strong", closer()) + .exit("thematicBreak", closer()); + + for (var mdastExtension : options.mdastExtensions) { + extensionBuilder.addAll(mdastExtension); + } + + extension = extensionBuilder.build(); + } + + enum CharacterReferenceType { + characterReferenceMarkerHexadecimal, + characterReferenceMarkerNumeric + } + + MdAstRoot compile(List events) { + MdAstRoot tree = new MdAstRoot(); + stack = new ArrayList<>(); + stack.add(tree); + tokenStack = new ArrayList<>(); + List listStack = new ArrayList<>(); + int index = -1; + + while (++index < events.size()) { + var event = events.get(index); + + // We preprocess lists to add `listItem` tokens, and to infer whether + // items the list itself are spread out. + if ( + event.token().type.equals(Types.listOrdered) || + event.token().type.equals(Types.listUnordered) + ) { + if (event.isEnter()) { + listStack.add(index); + } else { + var tail = ListUtils.pop(listStack); + Assert.check(tail != null, "expected list ot be open"); + index = prepareList(events, tail, index); + } + } + } + + index = -1; + + while (++index < events.size()) { + var event = events.get(index); + var handlerMap = event.isEnter() ? extension.enter : extension.exit; + var handler = handlerMap.get(event.token().type); + + if (handler != null) { + currentTokenContext = event.context(); + try { + handler.handle(this, event.token()); + } finally { + Assert.check(currentTokenContext == event.context(), "currentTokenContext changed while calling handler!"); + currentTokenContext = null; + } + } + } + + if (!tokenStack.isEmpty()) { + var tail = tokenStack.get(tokenStack.size() - 1); + var handler = Objects.requireNonNullElse(tail.onError(), this::defaultOnError); + handler.error(this, null, tail.token()); + } + + // Figure out `root` position. + tree.position = new MdAstPosition() + .withStart(point( + !events.isEmpty() ? events.get(0).token().start : + makePoint(1, 1, 0) + )) + .withEnd(point( + !events.isEmpty() + ? events.get(events.size() - 2).token().end + : makePoint(1, 1, 0) + )); + + for (var transform : extension.transforms) { + tree = transform.transform(tree); + } + + return tree; + } + + private static UnistPoint makePoint(int line, int column, int offset) { + return new Point(line, column, offset, -1, -1); + } + + private static int prepareList(List events, int start, int length) { + var index = start - 1; + var containerBalance = -1; + var listSpread = false; + Token listItem = null; + Integer lineIndex = null; + Integer firstBlankLineIndex = null; + boolean atMarker = false; + + while (++index <= length) { + var event = events.get(index); + var tokenType = event.token().type; + + if ( + tokenType.equals(Types.listUnordered) || + tokenType.equals(Types.listOrdered) || + tokenType.equals(Types.blockQuote) + ) { + if (event.isEnter()) { + containerBalance++; + } else { + containerBalance--; + } + + atMarker = false; + } else if (tokenType.equals(Types.lineEndingBlank)) { + if (event.isEnter()) { + if ( + listItem != null && + !atMarker && + containerBalance == 0 && + (firstBlankLineIndex == null || firstBlankLineIndex == 0) + ) { + firstBlankLineIndex = index; + } + + atMarker = false; + } + } else if ( + tokenType.equals(Types.linePrefix) || + tokenType.equals(Types.listItemValue) || + tokenType.equals(Types.listItemMarker) || + tokenType.equals(Types.listItemPrefix) || + tokenType.equals(Types.listItemPrefixWhitespace) + ) { + // Empty. + } else { + atMarker = false; + } + + if ( + (containerBalance == 0 && + event.isEnter() && + tokenType.equals(Types.listItemPrefix)) || + (containerBalance == -1 && + event.isExit() && + (tokenType.equals(Types.listUnordered) || + tokenType.equals(Types.listOrdered))) + ) { + if (listItem != null) { + var tailIndex = index; + lineIndex = null; + + while (tailIndex-- != 0) { + var tailEvent = events.get(tailIndex); + var tailEventTokenType = tailEvent.token().type; + + if ( + tailEventTokenType.equals(Types.lineEnding) || + tailEventTokenType.equals(Types.lineEndingBlank) + ) { + if (tailEvent.isExit()) continue; + + if (lineIndex != null && lineIndex != 0) { + events.get(lineIndex).token().type = Types.lineEndingBlank; + listSpread = true; + } + + tailEvent.token().type = Types.lineEnding; + lineIndex = tailIndex; + } else if ( + tailEventTokenType.equals(Types.linePrefix) || + tailEventTokenType.equals(Types.blockQuotePrefix) || + tailEventTokenType.equals(Types.blockQuotePrefixWhitespace) || + tailEventTokenType.equals(Types.blockQuoteMarker) || + tailEventTokenType.equals(Types.listItemIndent) + ) { + // Empty + } else { + break; + } + } + + if ( + (firstBlankLineIndex != null && firstBlankLineIndex != 0) && + (lineIndex == null || lineIndex == 0 || firstBlankLineIndex < lineIndex) + ) { + listItem.set(SPREAD, true); + } + + // Fix position. + listItem.end = (lineIndex != null && lineIndex != 0) ? events.get(lineIndex).token().start : event.token().end; + + ListUtils.splice(events, Objects.requireNonNullElse(lineIndex, index), 0, List.of(Tokenizer.Event.exit(listItem, event.context()))); + index++; + length++; + } + + // Create a new list item. + if (tokenType.equals(Types.listItemPrefix)) { + listItem = new Token(); + listItem.type = "listItem"; + listItem.set(SPREAD, false); + listItem.start = event.token().start; + + // @ts-expect-error: `listItem` is most definitely defined, TS... + ListUtils.splice(events, index, 0, List.of(Tokenizer.Event.enter(listItem, event.context()))); + index++; + length++; + firstBlankLineIndex = null; + atMarker = true; + } + } + } + + events.get(start).token().set(SPREAD, listSpread); + return length; + } + + UnistPoint point(UnistPoint d) { + return d; + } + + MdastExtension.Handler opener(Supplier create) { + return (ctx, token) -> { + enter(create.get(), token); + }; + } + + MdastExtension.Handler opener(Function create) { + return (ctx, token) -> { + enter(create.apply(token), token); + }; + } + + MdastExtension.Handler opener(Supplier create, MdastExtension.Handler and) { + return opener(t -> create.get(), and); + } + + MdastExtension.Handler opener(Supplier create, Runnable and) { + return opener(t -> create.get(), (context, token) -> and.run()); + } + + MdastExtension.Handler opener(Function create, Runnable and) { + return opener(create, (context, token) -> and.run()); + } + + MdastExtension.Handler opener(Function create, MdastExtension.Handler and) { + return (ctx, token) -> { + enter(create.apply(token), token); + if (and != null) { + and.handle(this, token); + } + }; + } + + @Override + public List getStack() { + return stack; + } + + @Override + public List getTokenStack() { + return tokenStack; + } + + public void buffer() { + this.stack.add(new Fragment()); + } + + @SuppressWarnings("unchecked") + @Override + public @Nullable T get(MdastContextProperty property) { + return (T) extensionData.get(property); + } + + @Override + public void set(MdastContextProperty property, T value) { + extensionData.put(property, value); + } + + @Override + public void remove(MdastContextProperty property) { + extensionData.remove(property); + } + + @Override + public N enter(N node, Token token, OnEnterError errorHandler) { + var parent = (MdAstParent) this.stack.get(this.stack.size() - 1); + Assert.check(parent != null, "expected `parent`"); + parent.addChild(node); + this.stack.add(node); + this.tokenStack.add(new TokenStackEntry(token, errorHandler)); + node.position = new MdAstPosition(); + node.position.start = token.start; + return node; + } + + private MdastExtension.Handler closer() { + return (context, token) -> { + exit(token); + }; + } + + private MdastExtension.Handler closer(Runnable and) { + return (context, token) -> { + and.run(); + exit(token); + }; + } + + private MdastExtension.Handler closer(@Nullable MdastExtension.Handler and) { + return (context, token) -> { + if (and != null) { + and.handle(this, token); + } + exit(token); + }; + } + + @Override + public MdAstNode exit(Token token, OnExitError onExitError) { + var node = ListUtils.pop(this.stack); + Assert.check(node != null, "expected `node`"); + var open = ListUtils.pop(this.tokenStack); + + if (open == null) { + throw new RuntimeException( + "Cannot close `" + + token.type + + "` (" + + MdAstPosition.stringify(token.start, token.end) + + "): it’s not open" + ); + } else if (!open.token().type.equals(token.type)) { + if (onExitError != null) { + onExitError.error(this, token, open.token()); + } else { + var handler = Objects.requireNonNullElse(open.onError(), this::defaultOnError); + handler.error(this, token, open.token()); + } + } + + Assert.check(!node.type().equals("fragment"), "unexpected fragment `exit`ed"); + Assert.check(node.position != null, "expected `position` to be defined"); + node.position.end = token.end; + return node; + } + + @Override + public String sliceSerialize(Token token) { + Assert.check(currentTokenContext != null, "missing current token context"); + return currentTokenContext.sliceSerialize(token); + } + + @Override + public MdastExtension getExtension() { + return extension; + } + + public String resume() { + stringBuffer.setLength(0); + ListUtils.pop(this.stack).toText(stringBuffer); + return stringBuffer.toString(); + } + + // + // Handlers. + // + + private void onenterlistordered() { + expectingFirstListItemValue = true; + } + + private void onenterlistitemvalue(MdastContext context, Token token) { + if (expectingFirstListItemValue) { + var ancestor = (MdAstList) (stack.get(stack.size() - 2)); + ancestor.start = Integer.parseInt( + this.sliceSerialize(token), + Constants.numericBaseDecimal + ); + expectingFirstListItemValue = false; + } + } + + private void onexitcodefencedfenceinfo() { + var data = this.resume(); + var node = (MdAstCode) (stack.get(stack.size() - 1)); + node.lang = data; + } + + private void onexitcodefencedfencemeta() { + var data = this.resume(); + var node = (MdAstCode) (stack.get(stack.size() - 1)); + node.meta = data; + } + + private void onexitcodefencedfence() { + // Exit if this is the closing fence. + if (flowCodeInside) return; + this.buffer(); + flowCodeInside = true; + } + + private static final Pattern START_END_NEWLINE = Pattern.compile("^(\r?\n|\r)|(\r?\n|\r)\\z"); + + private void onexitcodefenced() { + var data = this.resume(); + var node = (MdAstCode) (stack.get(stack.size() - 1)); + + // Removes the first and last newline in the string + node.value = START_END_NEWLINE.matcher(data).replaceAll(""); + + flowCodeInside = false; + } + + private void onexitcodeindented() { + var data = this.resume(); + var node = (MdAstCode) (stack.get(stack.size() - 1)); + + node.value = data.replaceAll("(\\r?\\n|\\r)$", ""); + } + + private void onexitdefinitionlabelstring(MdastContext context, Token token) { + // Discard label, use the source content instead. + var label = this.resume(); + var node = (MdAstDefinition) (stack.get(stack.size() - 1)); + node.label = label; + node.identifier = NormalizeIdentifier.normalizeIdentifier( + this.sliceSerialize(token) + ).toLowerCase(); + } + + private void onexitdefinitiontitlestring() { + var data = this.resume(); + var node = (MdAstDefinition) (stack.get(stack.size() - 1)); + node.title = data; + } + + private void onexitdefinitiondestinationstring() { + var data = this.resume(); + var node = (MdAstDefinition) (stack.get(stack.size() - 1)); + node.url = data; + } + + private void onexitatxheadingsequence(MdastContext context, Token token) { + var node = (MdAstHeading) (stack.get(stack.size() - 1)); + if (node.depth == 0) { + var depth = this.sliceSerialize(token).length(); + + Assert.check( + depth == 1 || + depth == 2 || + depth == 3 || + depth == 4 || + depth == 5 || + depth == 6, + "expected `depth` between `1` and `6`" + ); + + node.depth = depth; + } + } + + private void onexitsetextheadingtext() { + setextHeadingSlurpLineEnding = true; + } + + private void onexitsetextheadinglinesequence(MdastContext context, Token token) { + var node = (MdAstHeading) (stack.get(stack.size() - 1)); + + node.depth = + this.sliceSerialize(token).charAt(0) == Codes.equalsTo ? 1 : 2; + } + + private void onexitsetextheading() { + setextHeadingSlurpLineEnding = false; + } + + private void onenterdata(MdastContext context, Token token) { + + var parent = (MdAstParent) this.stack.get(stack.size() - 1); + + MdAstNode tail = null; + if (!parent.children().isEmpty()) { + tail = (MdAstNode) parent.children().get(parent.children().size() - 1); + } + + if (tail == null || !tail.type().equals("text")) { + // Add a new text node. + tail = text(); + // @ts-expect-error: we’ll add `end` later. + tail.position = new MdAstPosition().withStart(token.start); + // @ts-expect-error: Assume `parent` accepts `text`. + parent.addChild(tail); + } + + this.stack.add(tail); + } + + private void onexitdata(MdastContext context, Token token) { + var tail = ListUtils.pop(stack); + Assert.check(tail != null, "expected a `node` to be on the stack"); + Assert.check(tail.position != null, "expected `node` to have an open position"); + if (!(tail instanceof MdAstLiteral literal)) { + throw new IllegalStateException("expected a `literal` to be on the stack"); + } + literal.value += this.sliceSerialize(token); + literal.position.end = point(token.end); + } + + private void onexitlineending(MdastContext ignored, Token token) { + var context = stack.get(stack.size() - 1); + Assert.check(context != null, "expected `node`"); + + // If we’re at a hard break, include the line ending in there. + if (atHardBreak) { + if (!(context instanceof MdAstParent parent)) { + throw new IllegalStateException("expected `parent`"); + } + var tail = (MdAstNode) parent.children().get(parent.children().size() - 1); + Assert.check(tail.position != null, "expected tail to have a starting position"); + tail.position.end = point(token.end); + atHardBreak = false; + return; + } + + if ( + !setextHeadingSlurpLineEnding && + extension.canContainEols.contains(context.type()) + ) { + onenterdata(this, token); + onexitdata(this, token); + } + } + + private void onexithardbreak() { + atHardBreak = true; + } + + private void onexithtmlflow() { + var data = this.resume(); + var node = (MdAstHTML) (stack.get(stack.size() - 1)); + node.value = data; + } + + private void onexithtmltext() { + var data = this.resume(); + var node = (MdAstHTML) (stack.get(stack.size() - 1)); + node.value = data; + } + + private void onexitcodetext() { + var data = this.resume(); + var node = (MdAstInlineCode) (stack.get(stack.size() - 1)); + node.value = data; + } + + private void onexitlink() { + if (!(stack.get(stack.size() - 1) instanceof LinkOrLinkReference context)) { + // This indicates unbalanced tags and will crash later + return; + } + + MdAstParent replacement; + if (inReference) { + var ref = new MdAstLinkReference(); + ref.referenceType = Objects.requireNonNullElse(referenceType, MdAstReferenceType.SHORTCUT); + ref.identifier = context.identifier; + ref.label = context.label; + replacement = ref; + + } else { + var link = new MdAstLink(); + link.url = context.url; + link.title = context.title; + replacement = link; + } + replacement.position = context.position; + replacement.data = context.data; + for (var child : context.children()) { + replacement.addChild((MdAstNode) child); + } + ((MdAstParent) stack.get(stack.size() - 2)).replaceChild(context, replacement); + stack.set(stack.size() - 1, replacement); + + referenceType = null; + } + + + private void onexitimage() { + var context = stack.get(stack.size() - 1); + if (!(context instanceof ImageOrImageReference closedImageOrRef)) { + return; + } + + MdAstNode replacement; + if (inReference) { + var imgRef = new MdAstImageReference(); + imgRef.referenceType = Objects.requireNonNullElse(referenceType, MdAstReferenceType.SHORTCUT); + imgRef.identifier = closedImageOrRef.identifier; + imgRef.label = closedImageOrRef.label; + imgRef.alt = closedImageOrRef.alt; + replacement = imgRef; + } else { + var img = new MdAstImage(); + img.url = closedImageOrRef.url; + img.title = closedImageOrRef.title; + img.alt = closedImageOrRef.alt; + replacement = img; + } + replacement.position = context.position; + replacement.data = context.data; + + ((MdAstParent) stack.get(stack.size() - 2)).replaceChild(context, replacement); + // TODO: Needs replacement in parent too + stack.set(stack.size() - 1, replacement); + + referenceType = null; + } + + private void onexitlabeltext(MdastContext context, Token token) { + // Search up through the ancestors to find the reference + // Fixes issues where unclosed tags/constructs are reported as an error here + // instead of where the tag is then really closed. + var string = this.sliceSerialize(token); + for (int i = stack.size() - 2; i >= 0; i--) { + var ancestor = stack.get(i); + + if (ancestor instanceof LinkOrLinkReference link) { + link.label = DecodeString.decodeString(string); + link.identifier = NormalizeIdentifier.normalizeIdentifier(string).toLowerCase(); + return; + } else if (ancestor instanceof ImageOrImageReference image) { + image.label = DecodeString.decodeString(string); + image.identifier = NormalizeIdentifier.normalizeIdentifier(string).toLowerCase(); + return; + } + } + + throw new IllegalStateException("Couldn't find reference on the stack to close"); + } + + // While it's undecided whether an image ends up being a reference or not + public static class ImageOrImageReference extends MdAstImage { + public String label; + public String identifier; + } + + // While it's undecided whether a link ends up being a reference or not + public static class LinkOrLinkReference extends MdAstLink { + public String label; + public String identifier; + } + + private void onexitlabel() { + var fragment = stack.get(stack.size() - 1); + var value = this.resume(); + var node = stack.get(stack.size() - 1); + + // Assume a reference. + inReference = true; + + if (node instanceof MdAstLink link && fragment instanceof MdAstParent container) { + for (var child : container.children()) { + link.addChild((MdAstNode) child); + } + } else if (node instanceof MdAstImage image) { + image.alt = value; + } + // The else case will crash later + } + + private void onexitresourcedestinationstring() { + var data = this.resume(); + var node = stack.get(stack.size() - 1); + if (node instanceof MdAstLink link) { + link.url = data; + } else if (node instanceof MdAstImage image) { + image.url = data; + } + + } + + private void onexitresourcetitlestring() { + var data = this.resume(); + var node = (stack.get(stack.size() - 1)); + + if (node instanceof MdAstLink link) { + link.title = data; + } else if (node instanceof MdAstImage image) { + image.title = data; + } else { + throw new IllegalArgumentException(); + } + } + + private void onexitresource() { + inReference = false; + } + + private void onenterreference() { + referenceType = MdAstReferenceType.COLLAPSED; + } + + private void onexitreferencestring(MdastContext context, Token token) { + var label = this.resume(); + var node = stack.get(stack.size() - 1); + + if (node instanceof LinkOrLinkReference ref) { + ref.label = label; + ref.identifier = NormalizeIdentifier.normalizeIdentifier( + this.sliceSerialize(token) + ).toLowerCase(); + } else if (node instanceof ImageOrImageReference ref) { + ref.label = label; + ref.identifier = NormalizeIdentifier.normalizeIdentifier( + this.sliceSerialize(token) + ).toLowerCase(); + } else { + throw new IllegalStateException("Expected a link or image reference, but found: " + node); + } + referenceType = MdAstReferenceType.FULL; + } + + private void onexitcharacterreferencemarker(MdastContext context, Token token) { + characterReferenceType = switch (token.type) { + case "characterReferenceMarkerHexadecimal" -> CharacterReferenceType.characterReferenceMarkerHexadecimal; + case "characterReferenceMarkerNumeric" -> CharacterReferenceType.characterReferenceMarkerNumeric; + default -> throw new IllegalStateException(); + }; + } + + private void onexitcharacterreferencevalue(MdastContext context, Token token) { + var data = this.sliceSerialize(token); + var type = characterReferenceType; + String value; + + if (type != null) { + value = NumericCharacterReference.decodeNumericCharacterReference( + data, + type == CharacterReferenceType.characterReferenceMarkerNumeric + ? Constants.numericBaseDecimal + : Constants.numericBaseHexadecimal + ); + characterReferenceType = null; + } else { + // @ts-expect-error `decodeNamedCharacterReference` can return false for + // invalid named character references, but everything we’ve tokenized is + // valid. + value = NamedCharacterEntities.decodeNamedCharacterReference(data); + } + + var tail = ListUtils.pop(stack); + Assert.check(tail != null, "expected `node`"); + Assert.check(tail.position != null, "expected `node.position`"); + if (tail instanceof MdAstLiteral literal) { + literal.value += value; + literal.position.end = point(token.end); + } else { + throw new IllegalStateException("expected `node.value`"); + } + + } + + private void onexitautolinkprotocol(MdastContext context, Token token) { + onexitdata(this, token); + var node = (MdAstLink) (stack.get(stack.size() - 1)); + node.url = this.sliceSerialize(token); + } + + private void onexitautolinkemail(MdastContext context, Token token) { + onexitdata(this, token); + var node = (MdAstLink) (stack.get(stack.size() - 1)); + node.url = "mailto:" + this.sliceSerialize(token); + } + + // + // Creaters. + // + + MdAstBlockquote blockQuote() { + return new MdAstBlockquote(); + } + + MdAstCode codeFlow() { + return new MdAstCode(); + } + + MdAstInlineCode codeText() { + return new MdAstInlineCode(); + } + + MdAstDefinition definition() { + return new MdAstDefinition(); + } + + MdAstEmphasis emphasis() { + return new MdAstEmphasis(); + } + + MdAstHeading heading() { + return new MdAstHeading(); + } + + MdAstBreak hardBreak() { + return new MdAstBreak(); + } + + MdAstHTML html() { + return new MdAstHTML(); + } + + MdAstImage image() { + return new ImageOrImageReference(); + } + + MdAstLink link() { + return new LinkOrLinkReference(); + } + + MdAstList list(Token token) { + var list = new MdAstList(); + list.ordered = token.type.equals("listOrdered"); + list.spread = Boolean.TRUE.equals(token.get(SPREAD)); + return list; + } + + MdAstListItem listItem(Token token) { + var item = new MdAstListItem(); + item.spread = Boolean.TRUE.equals(token.get(SPREAD)); + return item; + } + + MdAstParagraph paragraph() { + return new MdAstParagraph(); + } + + MdAstStrong strong() { + return new MdAstStrong(); + } + + MdAstText text() { + return new MdAstText(); + } + + MdAstThematicBreak thematicBreak() { + return new MdAstThematicBreak(); + } + + private void defaultOnError(MdastContext context, @Nullable Token left, Token right) { + if (left != null) { + throw new RuntimeException( + "Cannot close `" + + left.type + + "` (" + + MdAstPosition.stringify(left.start, left.end) + + "): a different token (`" + + right.type + + "`, " + + MdAstPosition.stringify(right.start, right.end) + + ") is open" + ); + } else { + throw new RuntimeException( + "Cannot close document, a token (`" + + right.type + + "`, " + + MdAstPosition.stringify(right.start, right.end) + + ") is still open" + ); + } + } + + static class Fragment extends MdAstParent { + public Fragment() { + super("fragment"); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdastContext.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdastContext.java new file mode 100644 index 00000000000..91a028cae98 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdastContext.java @@ -0,0 +1,87 @@ +package appeng.libs.mdast; + +import appeng.libs.mdast.model.MdAstNode; +import appeng.libs.micromark.Token; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * mdast compiler context + */ +public interface MdastContext { + record TokenStackEntry(Token token, @Nullable OnEnterError onError) { + } + + @FunctionalInterface + interface OnEnterError { + void error(MdastContext context, @Nullable Token left, Token right); + } + + @FunctionalInterface + interface OnExitError { + void error(MdastContext context, Token left, Token right); + } + + List getStack(); + + List getTokenStack(); + + void buffer(); + + /** + * Stop capturing and access the output data. + */ + String resume(); + + /** + * Enter a token. + */ + default N enter(N node, Token token) { + return enter(node, token, null); + } + + /** + * Enter a token. + */ + N enter(N node, Token token, OnEnterError onError); + + /** + * Exit a token. + */ + default MdAstNode exit(Token token) { + return exit(token, null); + } + + /** + * Exit a token. + */ + MdAstNode exit(Token token, @Nullable OnExitError onError); + + /** + * Get the string value of a token + */ + String sliceSerialize(Token token); + + + default boolean has(MdastContextProperty property) { + return get(property) != null; + } + + /** + * Get data from the key-value store. + */ + @Nullable T get(MdastContextProperty property); + + /** + * Set data in the extension data. + */ + void set(MdastContextProperty property, T value); + + /** + * Remove data from the extension data. + */ + void remove(MdastContextProperty property); + + MdastExtension getExtension(); +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdastContextProperty.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdastContextProperty.java new file mode 100644 index 00000000000..45f51aad7c0 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdastContextProperty.java @@ -0,0 +1,10 @@ +package appeng.libs.mdast; + +/** + * Attach arbitrary data to {@link MdastContext}. For use by {@linkplain MdastExtension extensions}. + * + * @param The type of data associated with this property. + */ +@SuppressWarnings("unused") +public class MdastContextProperty { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdastExtension.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdastExtension.java new file mode 100644 index 00000000000..d87135f4891 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdastExtension.java @@ -0,0 +1,97 @@ +package appeng.libs.mdast; + +import appeng.libs.mdast.model.MdAstRoot; +import appeng.libs.micromark.Token; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An mdast extension changes how markdown tokens are turned into mdast. + */ +public class MdastExtension { + @FunctionalInterface + public interface Transform { + MdAstRoot transform(MdAstRoot tree); + } + + @FunctionalInterface + public interface Handler { + void handle(MdastContext context, Token token); + } + + public final List canContainEols; + public final List transforms; + public final Map enter; + public final Map exit; + + public MdastExtension(List canContainEols, + List transforms, + Map enter, + Map exit) { + this.canContainEols = List.copyOf(canContainEols); + this.transforms = List.copyOf(transforms); + this.enter = Map.copyOf(enter); + this.exit = Map.copyOf(exit); + } + + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final List canContainEols = new ArrayList<>(); + private final List transforms = new ArrayList<>(); + private final Map enter = new HashMap<>(); + private final Map exit = new HashMap<>(); + + private Builder() { + } + + public Builder enter(String type, Handler handler) { + enter.put(type, handler); + return this; + } + + public Builder enter(String type, Runnable handler) { + enter.put(type, (context, token) -> handler.run()); + return this; + } + + public Builder exit(String type, Handler handler) { + exit.put(type, handler); + return this; + } + + public Builder exit(String type, Runnable handler) { + exit.put(type, (context, token) -> handler.run()); + return this; + } + + public Builder canContainEol(String... types) { + Collections.addAll(canContainEols, types); + return this; + } + + public Builder transform(Transform transform) { + transforms.add(transform); + return this; + } + + public Builder addAll(MdastExtension extension) { + canContainEols.addAll(extension.canContainEols); + transforms.addAll(extension.transforms); + enter.putAll(extension.enter); + exit.putAll(extension.exit); + return this; + } + + public MdastExtension build() { + return new MdastExtension(canContainEols, transforms, enter, exit); + } + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/MdastOptions.java b/libs/markdown/src/main/java/appeng/libs/mdast/MdastOptions.java new file mode 100644 index 00000000000..ce06a77554a --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/MdastOptions.java @@ -0,0 +1,29 @@ +package appeng.libs.mdast; + +import appeng.libs.micromark.Extension; +import appeng.libs.micromark.html.ParseOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class MdastOptions extends ParseOptions { + public final List mdastExtensions = new ArrayList<>(); + + @Override + public MdastOptions withSyntaxExtension(Extension extension) { + super.withSyntaxExtension(extension); + return this; + } + + @Override + public MdastOptions withSyntaxExtension(Consumer customizer) { + super.withSyntaxExtension(customizer); + return this; + } + + public MdastOptions withMdastExtension(MdastExtension extension) { + mdastExtensions.add(extension); + return this; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/YamlFrontmatterExtension.java b/libs/markdown/src/main/java/appeng/libs/mdast/YamlFrontmatterExtension.java new file mode 100644 index 00000000000..38847bada5b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/YamlFrontmatterExtension.java @@ -0,0 +1,31 @@ +package appeng.libs.mdast; + +import appeng.libs.micromark.Token; +import appeng.libs.micromark.extensions.YamlFrontmatterSyntax; + +public class YamlFrontmatterExtension { + + public static final MdastExtension INSTANCE = MdastExtension.builder() + .enter(YamlFrontmatterSyntax.TYPE, YamlFrontmatterExtension::open) + .exit(YamlFrontmatterSyntax.TYPE, YamlFrontmatterExtension::close) + .exit(YamlFrontmatterSyntax.VALUE_TYPE, YamlFrontmatterExtension::value) + .build(); + + private static void open(MdastContext context, Token token) { + context.enter(new MdAstYamlFrontmatter(), token); + context.buffer(); + } + + private static void close(MdastContext context, Token token) { + var data = context.resume(); + var node = (MdAstYamlFrontmatter) context.exit(token); + // Remove the initial and final eol. + node.value = data.replaceAll("^(\\r?\\n|\\r)|(\\r?\\n|\\r)\\z", ""); + } + + private static void value(MdastContext context, Token token) { + context.getExtension().enter.get("data").handle(context, token); + context.getExtension().exit.get("data").handle(context, token); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/gfm/GfmTableMdastExtension.java b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/GfmTableMdastExtension.java new file mode 100644 index 00000000000..eb4675543f6 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/GfmTableMdastExtension.java @@ -0,0 +1,84 @@ +package appeng.libs.mdast.gfm; + +import appeng.libs.mdast.MdastContext; +import appeng.libs.mdast.MdastContextProperty; +import appeng.libs.mdast.MdastExtension; +import appeng.libs.mdast.gfm.model.GfmTable; +import appeng.libs.mdast.gfm.model.GfmTableCell; +import appeng.libs.mdast.gfm.model.GfmTableRow; +import appeng.libs.mdast.model.MdAstInlineCode; +import appeng.libs.micromark.Token; +import appeng.libs.micromark.extensions.gfm.GfmTableSyntax; + +import java.util.regex.MatchResult; +import java.util.regex.Pattern; + +public final class GfmTableMdastExtension { + + private static final MdastContextProperty IN_TABLE = new MdastContextProperty<>(); + + public static final MdastExtension INSTANCE = MdastExtension.builder() + .enter("table", GfmTableMdastExtension::enterTable) + .enter("tableData", GfmTableMdastExtension::enterCell) + .enter("tableHeader", GfmTableMdastExtension::enterCell) + .enter("tableRow", GfmTableMdastExtension::enterRow) + .exit("codeText", GfmTableMdastExtension::exitCodeText) + .exit("table", GfmTableMdastExtension::exitTable) + .exit("tableData", GfmTableMdastExtension::exit) + .exit("tableHeader", GfmTableMdastExtension::exit) + .exit("tableRow", GfmTableMdastExtension::exit) + .build(); + + private GfmTableMdastExtension() { + } + + private static void enterTable(MdastContext context, Token token) { + var align = token.get(GfmTableSyntax.ALIGN); + + var table = new GfmTable(); + table.align = align; + + context.enter(table, token); + context.set(IN_TABLE, true); + } + + private static void exitTable(MdastContext context, Token token) { + context.exit(token); + context.remove(IN_TABLE); + } + + private static void enterRow(MdastContext context, Token token) { + context.enter(new GfmTableRow(), token); + } + + private static void exit(MdastContext context, Token token) { + context.exit(token); + } + + private static void enterCell(MdastContext context, Token token) { + context.enter(new GfmTableCell(), token); + } + + private static final Pattern ESCAPED_PIPE_PATERN = Pattern.compile("\\\\([\\\\|])"); + + // Overwrite the default code text data handler to unescape escaped pipes when + // they are in tables. + private static void exitCodeText(MdastContext context, Token token) { + var value = context.resume(); + + if (Boolean.TRUE.equals(context.get(IN_TABLE))) { + value = ESCAPED_PIPE_PATERN.matcher(value).replaceAll(GfmTableMdastExtension::replace); + } + + var stack = context.getStack(); + var node = (MdAstInlineCode) stack.get(stack.size() - 1); + node.value = value; + context.exit(token); + } + + private static String replace(MatchResult result) { + // Pipes work, backslashes don’t (but can’t escape pipes). + return result.group(1).equals("|") ? "|" : result.group(); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTable.java b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTable.java new file mode 100644 index 00000000000..cfa2f41b25a --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTable.java @@ -0,0 +1,42 @@ +package appeng.libs.mdast.gfm.model; + +import appeng.libs.mdast.model.MdAstFlowContent; +import appeng.libs.mdast.model.MdAstParent; +import appeng.libs.micromark.extensions.gfm.Align; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; + +public class GfmTable extends MdAstParent implements MdAstFlowContent { + @Nullable + public List align = null; + + public GfmTable() { + super("table"); + } + + @Override + protected Class childClass() { + return GfmTableRow.class; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + if (align != null) { + writer.name("align").beginArray(); + for (var value : align) { + switch (value) { + case LEFT -> writer.value("left"); + case CENTER -> writer.value("center"); + case RIGHT -> writer.value("right"); + case NONE -> writer.nullValue(); + } + } + writer.endArray(); + } + + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableCell.java b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableCell.java new file mode 100644 index 00000000000..47ac9eef18f --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableCell.java @@ -0,0 +1,16 @@ +package appeng.libs.mdast.gfm.model; + +import appeng.libs.mdast.model.MdAstAnyContent; +import appeng.libs.mdast.model.MdAstParent; +import appeng.libs.mdast.model.MdAstPhrasingContent; + +public class GfmTableCell extends MdAstParent implements MdAstAnyContent { + public GfmTableCell() { + super("tableCell"); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableRow.java b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableRow.java new file mode 100644 index 00000000000..c7607db8bab --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/gfm/model/GfmTableRow.java @@ -0,0 +1,16 @@ +package appeng.libs.mdast.gfm.model; + + +import appeng.libs.mdast.model.MdAstAnyContent; +import appeng.libs.mdast.model.MdAstParent; + +public class GfmTableRow extends MdAstParent implements MdAstAnyContent { + public GfmTableRow() { + super("tableRow"); + } + + @Override + protected Class childClass() { + return GfmTableCell.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/MdxMdastExtension.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/MdxMdastExtension.java new file mode 100644 index 00000000000..f2619d03363 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/MdxMdastExtension.java @@ -0,0 +1,327 @@ +package appeng.libs.mdast.mdx; + +import appeng.libs.mdast.MdastContext; +import appeng.libs.mdast.MdastContextProperty; +import appeng.libs.mdast.MdastExtension; +import appeng.libs.mdast.mdx.model.MdxJsxAttribute; +import appeng.libs.mdast.mdx.model.MdxJsxAttributeNode; +import appeng.libs.mdast.mdx.model.MdxJsxAttributeValueExpression; +import appeng.libs.mdast.mdx.model.MdxJsxExpressionAttribute; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; +import appeng.libs.mdast.model.MdAstNode; +import appeng.libs.mdast.model.MdAstPosition; +import appeng.libs.micromark.ListUtils; +import appeng.libs.micromark.ParseException; +import appeng.libs.micromark.Point; +import appeng.libs.micromark.Token; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class MdxMdastExtension { + private static final MdastContextProperty> TAG_STACK = new MdastContextProperty<>(); + private static final MdastContextProperty TAG = new MdastContextProperty<>(); + + public static final MdastExtension INSTANCE = MdastExtension.builder() + .canContainEol("mdxJsxTextElement") + .enter("mdxJsxFlowTag", MdxMdastExtension::enterMdxJsxTag) + .enter("mdxJsxFlowTagClosingMarker", MdxMdastExtension::enterMdxJsxTagClosingMarker) + .enter("mdxJsxFlowTagAttribute", MdxMdastExtension::enterMdxJsxTagAttribute) + .enter("mdxJsxFlowTagExpressionAttribute", MdxMdastExtension::enterMdxJsxTagExpressionAttribute) + .enter("mdxJsxFlowTagAttributeValueLiteral", MdxMdastExtension::buffer) + .enter("mdxJsxFlowTagAttributeValueExpression", MdxMdastExtension::buffer) + .enter("mdxJsxFlowTagSelfClosingMarker", MdxMdastExtension::enterMdxJsxTagSelfClosingMarker) + .enter("mdxJsxTextTag", MdxMdastExtension::enterMdxJsxTag) + .enter("mdxJsxTextTagClosingMarker", MdxMdastExtension::enterMdxJsxTagClosingMarker) + .enter("mdxJsxTextTagAttribute", MdxMdastExtension::enterMdxJsxTagAttribute) + .enter("mdxJsxTextTagExpressionAttribute", MdxMdastExtension::enterMdxJsxTagExpressionAttribute) + .enter("mdxJsxTextTagAttributeValueLiteral", MdxMdastExtension::buffer) + .enter("mdxJsxTextTagAttributeValueExpression", MdxMdastExtension::buffer) + .enter("mdxJsxTextTagSelfClosingMarker", MdxMdastExtension::enterMdxJsxTagSelfClosingMarker) + .exit("mdxJsxFlowTagClosingMarker", MdxMdastExtension::exitMdxJsxTagClosingMarker) + .exit("mdxJsxFlowTagNamePrimary", MdxMdastExtension::exitMdxJsxTagNamePrimary) + .exit("mdxJsxFlowTagNameMember", MdxMdastExtension::exitMdxJsxTagNameMember) + .exit("mdxJsxFlowTagNameLocal", MdxMdastExtension::exitMdxJsxTagNameLocal) + .exit("mdxJsxFlowTagExpressionAttribute", MdxMdastExtension::exitMdxJsxTagExpressionAttribute) + .exit("mdxJsxFlowTagExpressionAttributeValue", MdxMdastExtension::data) + .exit("mdxJsxFlowTagAttributeNamePrimary", MdxMdastExtension::exitMdxJsxTagAttributeNamePrimary) + .exit("mdxJsxFlowTagAttributeNameLocal", MdxMdastExtension::exitMdxJsxTagAttributeNameLocal) + .exit("mdxJsxFlowTagAttributeValueLiteral", MdxMdastExtension::exitMdxJsxTagAttributeValueLiteral) + .exit("mdxJsxFlowTagAttributeValueLiteralValue", MdxMdastExtension::data) + .exit("mdxJsxFlowTagAttributeValueExpression", MdxMdastExtension::exitMdxJsxTagAttributeValueExpression) + .exit("mdxJsxFlowTagAttributeValueExpressionValue", MdxMdastExtension::data) + .exit("mdxJsxFlowTagSelfClosingMarker", MdxMdastExtension::exitMdxJsxTagSelfClosingMarker) + .exit("mdxJsxFlowTag", MdxMdastExtension::exitMdxJsxTag) + .exit("mdxJsxTextTagClosingMarker", MdxMdastExtension::exitMdxJsxTagClosingMarker) + .exit("mdxJsxTextTagNamePrimary", MdxMdastExtension::exitMdxJsxTagNamePrimary) + .exit("mdxJsxTextTagNameMember", MdxMdastExtension::exitMdxJsxTagNameMember) + .exit("mdxJsxTextTagNameLocal", MdxMdastExtension::exitMdxJsxTagNameLocal) + .exit("mdxJsxTextTagExpressionAttribute", MdxMdastExtension::exitMdxJsxTagExpressionAttribute) + .exit("mdxJsxTextTagExpressionAttributeValue", MdxMdastExtension::data) + .exit("mdxJsxTextTagAttributeNamePrimary", MdxMdastExtension::exitMdxJsxTagAttributeNamePrimary) + .exit("mdxJsxTextTagAttributeNameLocal", MdxMdastExtension::exitMdxJsxTagAttributeNameLocal) + .exit("mdxJsxTextTagAttributeValueLiteral", MdxMdastExtension::exitMdxJsxTagAttributeValueLiteral) + .exit("mdxJsxTextTagAttributeValueLiteralValue", MdxMdastExtension::data) + .exit("mdxJsxTextTagAttributeValueExpression", MdxMdastExtension::exitMdxJsxTagAttributeValueExpression) + .exit("mdxJsxTextTagAttributeValueExpressionValue", MdxMdastExtension::data) + .exit("mdxJsxTextTagSelfClosingMarker", MdxMdastExtension::exitMdxJsxTagSelfClosingMarker) + .exit("mdxJsxTextTag", MdxMdastExtension::exitMdxJsxTag) + .build(); + + private MdxMdastExtension() { + } + + private static void buffer(MdastContext context, Token token) { + context.buffer(); + } + + private static void data(MdastContext context, Token token) { + context.getExtension().enter.get("data").handle(context, token); + context.getExtension().exit.get("data").handle(context, token); + } + + private static void enterMdxJsxTag(MdastContext context, Token token) { + var tag = new Tag(token); + if (!context.has(TAG_STACK)) { + context.set(TAG_STACK, new ArrayList<>()); + } + context.set(TAG, tag); + context.buffer(); + } + + private static void enterMdxJsxTagClosingMarker(MdastContext context, Token token) { + var stack = getStack(context); + + if (stack.isEmpty()) { + throw new ParseException( + "Unexpected closing slash `/` in tag, expected an open tag first", + token.start, token.end, + "mdast-util-mdx-jsx:unexpected-closing-slash" + ); + } + } + + private static void enterMdxJsxTagAnyAttribute(MdastContext context, Token token) { + var tag = getTag(context); + + if (tag.close) { + throw new ParseException( + "Unexpected attribute in closing tag, expected the end of the tag", + token.start, token.end, + "mdast-util-mdx-jsx:unexpected-attribute" + ); + } + } + + private static void enterMdxJsxTagSelfClosingMarker(MdastContext context, Token token) { + var tag = getTag(context); + + if (tag.close) { + throw new ParseException( + "Unexpected self-closing slash `/` in closing tag, expected the end of the tag", + token.start, token.end, + "mdast-util-mdx-jsx:unexpected-self-closing-slash" + ); + } + } + + private static void exitMdxJsxTagClosingMarker(MdastContext context, Token token) { + var tag = getTag(context); + tag.close = true; + } + + private static void exitMdxJsxTagNamePrimary(MdastContext context, Token token) { + var tag = getTag(context); + tag.name = context.sliceSerialize(token); + } + + private static void exitMdxJsxTagNameMember(MdastContext context, Token token) { + var tag = getTag(context); + tag.name += '.' + context.sliceSerialize(token); + } + + private static void exitMdxJsxTagNameLocal(MdastContext context, Token token) { + var tag = getTag(context); + tag.name += ':' + context.sliceSerialize(token); + } + + private static void enterMdxJsxTagAttribute(MdastContext context, Token token) { + var tag = getTag(context); + enterMdxJsxTagAnyAttribute(context, token); + tag.attributes.add(new MdxJsxAttribute()); + } + + private static void enterMdxJsxTagExpressionAttribute(MdastContext context, Token token) { + var tag = getTag(context); + enterMdxJsxTagAnyAttribute(context, token); + tag.attributes.add(new MdxJsxExpressionAttribute()); + context.buffer(); + } + + private static void exitMdxJsxTagExpressionAttribute(MdastContext context, Token token) { + var tag = getTag(context); + var tail = (MdxJsxExpressionAttribute) tag.attributes.get(tag.attributes.size() - 1); + tail.value = context.resume(); + } + + private static void exitMdxJsxTagAttributeNamePrimary(MdastContext context, Token token) { + var tag = getTag(context); + var node = (MdxJsxAttribute) tag.attributes.get(tag.attributes.size() - 1); + node.name = context.sliceSerialize(token); + } + + private static void exitMdxJsxTagAttributeNameLocal(MdastContext context, Token token) { + var tag = getTag(context); + var node = (MdxJsxAttribute) tag.attributes.get(tag.attributes.size() - 1); + node.name += ':' + context.sliceSerialize(token); + } + + private static void exitMdxJsxTagAttributeValueLiteral(MdastContext context, Token token) { + var tag = getTag(context); + var value = ParseEntities.parseEntities(context.resume()); + + var lastAttr = tag.attributes.get(tag.attributes.size() - 1); + if (lastAttr instanceof MdxJsxAttribute attribute) { + attribute.setValue(value); + } else if (lastAttr instanceof MdxJsxExpressionAttribute attribute) { + attribute.value = value; + } else { + throw new IllegalStateException(); + } + } + + private static void exitMdxJsxTagAttributeValueExpression(MdastContext context, Token token) { + var tag = getTag(context); + var tail = (MdxJsxAttribute) tag.attributes.get(tag.attributes.size() - 1); + tail.setExpression(context.resume()); + } + + private static void exitMdxJsxTagSelfClosingMarker(MdastContext context, Token token) { + var tag = getTag(context); + + tag.selfClosing = true; + } + + private static void exitMdxJsxTag(MdastContext context, Token token) { + var tag = getTag(context); + var stack = getStack(context); + var tail = stack.isEmpty() ? null : stack.get(stack.size() - 1); + + if (tag.close && !Objects.equals(tail.name, tag.name)) { + throw new ParseException( + "Unexpected closing tag `" + + serializeAbbreviatedTag(tag) + + "`, expected corresponding closing tag for `" + + serializeAbbreviatedTag(tail) + + "` (" + + MdAstPosition.stringify(tail.position()) + + ')', + token.start, token.end, + "mdast-util-mdx-jsx:end-tag-mismatch" + ); + } + + // End of a tag, so drop the buffer. + context.resume(); + + if (tag.close) { + ListUtils.pop(stack); + } else { + MdAstNode node; + if (Objects.equals(token.type, "mdxJsxTextTag")) { + node = new MdxJsxTextElement(tag.name, tag.attributes); + } else { + node = new MdxJsxFlowElement(tag.name, tag.attributes); + } + + context.enter( + node, + token, + MdxMdastExtension::onErrorRightIsTag + ); + } + + if (tag.selfClosing || tag.close) { + context.exit(token, MdxMdastExtension::onErrorLeftIsTag); + } else { + stack.add(tag); + } + } + + private static void onErrorRightIsTag(MdastContext context, @Nullable Token closing, Token open) { + var tag = getTag(context); + var place = closing != null ? " before the end of `" + closing.type + '`' : ""; + MdAstPosition position = null; + if (closing != null) { + position = new MdAstPosition(closing.start, closing.end); + } + + throw new ParseException( + "Expected a closing tag for `" + + serializeAbbreviatedTag(tag) + + "` (" + + MdAstPosition.stringify(open.start, open.end) + + ')' + + place, + position, + "mdast-util-mdx-jsx:end-tag-mismatch" + ); + } + + private static void onErrorLeftIsTag(MdastContext context, @Nullable Token a, Token b) { + var tag = getTag(context); + throw new ParseException( + "Expected the closing tag `" + + serializeAbbreviatedTag(tag) + + "` either after the end of `" + + b.type + + "` (" + + MdAstPosition.stringify(b.end) + + ") or another opening tag after the start of `" + + b.type + + "` (" + + MdAstPosition.stringify(b.start) + + ')', + a != null ? a.start : null, a != null ? a.end : null, + "mdast-util-mdx-jsx:end-tag-mismatch" + ); + } + + /** + * Serialize a tag, excluding attributes. + * `self-closing` is not supported, because we don’t need it yet. + */ + private static String serializeAbbreviatedTag(Tag tag) { + return "<" + (tag.close ? '/' : "") + (Objects.requireNonNullElse(tag.name, "")) + ">"; + } + + private static class Tag { + @Nullable + String name; + List attributes = new ArrayList<>(); + boolean close; + boolean selfClosing; + Point start; + Point end; + + public Tag(Token token) { + start = token.start; + end = token.end; + } + + public MdAstPosition position() { + return new MdAstPosition(start, end); + } + } + + private static List getStack(MdastContext context) { + return Objects.requireNonNull(context.get(TAG_STACK), "stack is missing from context"); + } + + private static Tag getTag(MdastContext context) { + return Objects.requireNonNull(context.get(TAG), "tag is missing from context"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/ParseEntities.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/ParseEntities.java new file mode 100644 index 00000000000..e7056cb2631 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/ParseEntities.java @@ -0,0 +1,212 @@ +package appeng.libs.mdast.mdx; + +import appeng.libs.micromark.CharUtil; +import appeng.libs.micromark.NamedCharacterEntities; + +import java.util.HashMap; +import java.util.Map; + +/** + * Reduced functionality port of https://github.com/wooorm/parse-entities/ + */ +final class ParseEntities { + private ParseEntities() { + } + + /** + * Parse HTML character references. + */ + public static String parseEntities(String value) { + var result = new StringBuilder(); + + // Ensure the algorithm walks over the first character (inclusive). + for (var index = 0; index < value.length(); ++index) { + var character = value.charAt(index); + + if (character != '&' || index + 1 >= value.length()) { + result.append(character); + continue; + } + + int following = value.charAt(index + 1); + + // The behavior depends on the identity of the next character. + if ( + following == '\t' || + following == '\n' || + following == '\f' || + following == ' ' || + following == '&' || + following == '<' + ) { + // Not a character reference. + // No characters are consumed, and nothing is returned. + // This is not an error, either. + result.append(character); + continue; + } + + var start = index + 1; + var begin = start; + var end = start; + CharRefType type; + + if (following == '#') { + // Numerical reference. + end = ++begin; + + // The behavior further depends on the next character. + following = value.charAt(end); + + if (following == 'X' || following == 'x') { + // ASCII hexadecimal digits. + type = CharRefType.hexadecimal; + end = ++begin; + } else { + // ASCII decimal digits. + type = CharRefType.decimal; + } + } else { + // Named reference. + type = CharRefType.named; + } + + end--; + + // Each type of character reference accepts different characters. + // This test is used to detect whether a reference has ended (as the semicolon + // is not strictly needed). + var charBuffer = new StringBuilder(); + while (++end < value.length()) { + following = value.charAt(end); + + if (!type.test((char) following)) { + break; + } + + charBuffer.append((char) following); + } + + var terminated = end < value.length() && value.charAt(end) == ';'; + + boolean consumeRef = terminated; + + if (terminated) { + end++; + + if (type == CharRefType.named) { + var namedReference = NamedCharacterEntities.decodeNamedCharacterReference(charBuffer.toString()); + if (namedReference != null) { + result.append(namedReference); + } else { + consumeRef = false; // Unknown named references stay untouched + } + } else { + // When terminated and numerical, parse as either hexadecimal or + // decimal. + var referenceCode = Integer.parseInt( + charBuffer, + 0, + charBuffer.length(), + type == CharRefType.hexadecimal ? 16 : 10 + ); + + // Emit a warning when the parsed number is prohibited, and replace with + // replacement character. + if (prohibited(referenceCode)) { + result.append((char) 65533 /* `�` */); + } else if (characterReferenceInvalid.containsKey(referenceCode)) { + // Emit a warning when the parsed number is disallowed, and replace by + // an alternative. + result.append(characterReferenceInvalid.get(referenceCode)); + } else { + result.appendCodePoint(referenceCode); + } + } + } + + // Found it! + // First eat the queued characters as normal text, then eat a reference. + if (!consumeRef) { + // If we could not find a reference, queue the checked characters (as + // normal characters), and move the pointer to their end. + // This is possible because we can be certain neither newlines nor + // ampersands are included. + result.append(value, start - 1, end); + } + index = end - 1; + } + + // Return the reduced nodes. + return result.toString(); + } + + /** + * Check if `character` is outside the permissible unicode range. + */ + private static boolean prohibited(int code) { + return (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff; + } + + enum CharRefType { + named { + @Override + boolean test(char ch) { + return CharUtil.asciiAlphanumeric(ch); + } + }, + decimal { + @Override + boolean test(char ch) { + return CharUtil.asciiDigit(ch); + } + }, + hexadecimal { + @Override + boolean test(char ch) { + return CharUtil.asciiHexDigit(ch); + } + }; + + abstract boolean test(char ch); + } + + /** + * Map of invalid numeric character references to their replacements, according to HTML. + */ + private static final Map characterReferenceInvalid; + static { + var codes = new HashMap(); + codes.put(0, "�"); + codes.put(128, "€"); + codes.put(130, "‚"); + codes.put(131, "ƒ"); + codes.put(132, "„"); + codes.put(133, "…"); + codes.put(134, "†"); + codes.put(135, "‡"); + codes.put(136, "ˆ"); + codes.put(137, "‰"); + codes.put(138, "Š"); + codes.put(139, "‹"); + codes.put(140, "Œ"); + codes.put(142, "Ž"); + codes.put(145, "‘"); + codes.put(146, "’"); + codes.put(147, "“"); + codes.put(148, "”"); + codes.put(149, "•"); + codes.put(150, "–"); + codes.put(151, "—"); + codes.put(152, "˜"); + codes.put(153, "™"); + codes.put(154, "š"); + codes.put(155, "›"); + codes.put(156, "œ"); + codes.put(158, "ž"); + codes.put(159, "Ÿ"); + characterReferenceInvalid = Map.copyOf(codes); + } + + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttribute.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttribute.java new file mode 100644 index 00000000000..4186000bd1b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttribute.java @@ -0,0 +1,51 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.mdast.model.MdAstNode; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class MdxJsxAttribute extends MdAstNode implements MdxJsxAttributeNode { + public String name = ""; + @Nullable + private Object value; + + public MdxJsxAttribute() { + super("mdxJsxAttribute"); + } + + @Override + public void toText(StringBuilder buffer) { + } + + public void setExpression(String expression) { + var node = new MdxJsxAttributeValueExpression(); + node.value = expression; + this.value = node; + } + + public String getStringValue() { + return (String) value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + super.writeJson(writer); + writer.name("name").value(name); + writer.name("value"); + if (value == null) { + writer.nullValue(); + } else if (value instanceof String string) { + writer.value(string); + } else if (value instanceof MdxJsxAttributeValueExpression expression) { + expression.toJson(writer); + } else { + throw new IllegalStateException("Invalid attribute value type: " + value); + } + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeNode.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeNode.java new file mode 100644 index 00000000000..6b87a8d1d5b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeNode.java @@ -0,0 +1,13 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.unist.UnistNode; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * Potential attributes of {@link MdxJsxElementFields} + */ +public interface MdxJsxAttributeNode extends UnistNode { + void toJson(JsonWriter writer) throws IOException; +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeValueExpression.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeValueExpression.java new file mode 100644 index 00000000000..933d8102074 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxAttributeValueExpression.java @@ -0,0 +1,9 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.mdast.model.MdAstLiteral; + +public class MdxJsxAttributeValueExpression extends MdAstLiteral { + public MdxJsxAttributeValueExpression() { + super("mdxJsxAttributeValueExpression"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxElementFields.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxElementFields.java new file mode 100644 index 00000000000..b74bbd110cd --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxElementFields.java @@ -0,0 +1,29 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.unist.UnistNode; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface MdxJsxElementFields extends UnistNode { + @Nullable + String name(); + + List attributes(); + + List children(); + + default String getAttributeString(String name, String defaultValue) { + for (var attributeNode : attributes()) { + if (attributeNode instanceof MdxJsxAttribute jsxAttribute) { + if (name.equals(jsxAttribute.name)) { + return jsxAttribute.getStringValue(); + } + } else if (attributeNode instanceof MdxJsxExpressionAttribute jsxExpressionAttribute) { + throw new IllegalStateException("Attribute spreads unsupported!"); + } + } + + return defaultValue; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxExpressionAttribute.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxExpressionAttribute.java new file mode 100644 index 00000000000..7c67f3df3d5 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxExpressionAttribute.java @@ -0,0 +1,9 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.mdast.model.MdAstLiteral; + +public class MdxJsxExpressionAttribute extends MdAstLiteral implements MdxJsxAttributeNode { + public MdxJsxExpressionAttribute() { + super("mdxJsxExpressionAttribute"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxFlowElement.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxFlowElement.java new file mode 100644 index 00000000000..07742892f36 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxFlowElement.java @@ -0,0 +1,52 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.mdast.model.MdAstFlowContent; +import appeng.libs.mdast.model.MdAstParent; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class MdxJsxFlowElement extends MdAstParent implements MdxJsxElementFields, MdAstFlowContent { + public String name; + public List attributes; + + public MdxJsxFlowElement() { + this("", new ArrayList<>()); + } + + public MdxJsxFlowElement(String name, List attributes) { + super("mdxJsxFlowElement"); + this.name = name; + this.attributes = attributes; + } + + @Override + public @Nullable String name() { + return name; + } + + @Override + public List attributes() { + return attributes; + } + + @Override + protected Class childClass() { + return MdAstFlowContent.class; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + super.writeJson(writer); + writer.name("name").value(name); + writer.name("attributes"); + writer.beginArray(); + for (var attribute : attributes) { + attribute.toJson(writer); + } + writer.endArray(); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxTextElement.java b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxTextElement.java new file mode 100644 index 00000000000..2d66c7009db --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/mdx/model/MdxJsxTextElement.java @@ -0,0 +1,53 @@ +package appeng.libs.mdast.mdx.model; + +import appeng.libs.mdast.model.MdAstParent; +import appeng.libs.mdast.model.MdAstPhrasingContent; +import appeng.libs.mdast.model.MdAstStaticPhrasingContent; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class MdxJsxTextElement extends MdAstParent implements MdxJsxElementFields, MdAstStaticPhrasingContent { + private String name; + private List attributes; + + public MdxJsxTextElement() { + this("", new ArrayList<>()); + } + + public MdxJsxTextElement(String name, List attributes) { + super("mdxJsxTextElement"); + this.name = name; + this.attributes = attributes; + } + + @Override + public @Nullable String name() { + return name; + } + + @Override + public List attributes() { + return attributes; + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + super.writeJson(writer); + writer.name("name").value(name); + writer.name("attributes"); + writer.beginArray(); + for (var attribute : attributes) { + attribute.toJson(writer); + } + writer.endArray(); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAlternative.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAlternative.java new file mode 100644 index 00000000000..f0991e50855 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAlternative.java @@ -0,0 +1,14 @@ +package appeng.libs.mdast.model; + +import org.jetbrains.annotations.Nullable; + +/** + * Represents a node with a fallback. + *

+ * An alt field should be present. It represents equivalent content for environments that cannot represent the node + * as intended. + */ +public interface MdAstAlternative { + @Nullable + String alt(); +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAnyContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAnyContent.java new file mode 100644 index 00000000000..f2c7e7631ea --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAnyContent.java @@ -0,0 +1,9 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistNode; + +/** + * Root for anything that can be part of a {@link MdAstParent}. + */ +public interface MdAstAnyContent extends UnistNode { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAssociation.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAssociation.java new file mode 100644 index 00000000000..bf75bbefb8a --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstAssociation.java @@ -0,0 +1,25 @@ +package appeng.libs.mdast.model; + +import org.jetbrains.annotations.Nullable; + +/** + * Represents an internal relation from one node to another. + */ +public interface MdAstAssociation { + + /** + * An identifier field must be present. It can match another node. identifier is a source value: character escapes and character references are not parsed. Its value must be normalized. + *

+ * To normalize a value, collapse markdown whitespace ([\t\n\r ]+) to a space, trim the optional initial and/or final space, and perform case-folding. + *

+ * Whether the value of identifier (or normalized label if there is no identifier) is expected to be a unique identifier or not depends on the type of node including the Association. + * An example of this is that they should be unique on Definition, whereas multiple LinkReferences can be non-unique to be associated with one definition. + */ + String identifier(); + + /** + * A label field can be present. label is a string value: it works just like title on a link or a lang on code: character escapes and character references are parsed. + */ + @Nullable + String label(); +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBlockquote.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBlockquote.java new file mode 100644 index 00000000000..1107b363cfa --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBlockquote.java @@ -0,0 +1,33 @@ +package appeng.libs.mdast.model; + +import java.util.List; + +/** + * Blockquote (Parent) represents a section quoted from somewhere else. + *

+ * Blockquote can be used where flow content is expected. Its content model is also flow content. + *

+ * For example, the following markdown: + *

+ * > Alpha bravo charlie. + *

+ * Yields: + *

+ * { + * type: 'blockquote', + * children: [{ + * type: 'paragraph', + * children: [{type: 'text', value: 'Alpha bravo charlie.'}] + * }] + * } + */ +public class MdAstBlockquote extends MdAstParent implements MdAstFlowContent { + public MdAstBlockquote() { + super("blockquote"); + } + + @Override + protected Class childClass() { + return MdAstFlowContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBreak.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBreak.java new file mode 100644 index 00000000000..cecc564b7c7 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstBreak.java @@ -0,0 +1,34 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistNode; + +/** + * Break (Node) represents a line break, such as in poems or addresses. + *

+ * Break can be used where phrasing content is expected. It has no content model. + *

+ * For example, the following markdown: + *

+ * foo·· + * bar + *

+ * Yields: + *

+ * { + * type: 'paragraph', + * children: [ + * {type: 'text', value: 'foo'}, + * {type: 'break'}, + * {type: 'text', value: 'bar'} + * ] + * } + */ +public class MdAstBreak extends MdAstNode implements MdAstStaticPhrasingContent { + public MdAstBreak() { + super("break"); + } + + @Override + public void toText(StringBuilder buffer) { + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstCode.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstCode.java new file mode 100644 index 00000000000..3d15e04db44 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstCode.java @@ -0,0 +1,72 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * Code (Literal) represents a block of preformatted text, such as ASCII art or computer code. + *

+ * Code can be used where flow content is expected. Its content is represented by its value field. + *

+ * This node relates to the phrasing content concept InlineCode. + *

+ * For example, the following markdown: + *

+ * foo() + *

+ * Yields: + *

+ * { + * type: 'code', + * lang: null, + * meta: null, + * value: 'foo()' + * } + *

+ * And the following markdown: + *

+ * ```js highlight-line="2" + * foo() + * bar() + * baz() + * ``` + *

+ * Yields: + *

+ * { + * type: 'code', + * lang: 'javascript', + * meta: 'highlight-line="2"', + * value: 'foo()\nbar()\nbaz()' + * } + */ +public class MdAstCode extends MdAstLiteral implements MdAstFlowContent { + public MdAstCode() { + super("code"); + } + + /** + * The language of the code, if not-null. + */ + @Nullable + public String lang; + + /** + * Can be not-null if lang is not-null. It represents custom information relating to the node. + */ + @Nullable + public String meta; + + @Override + public void writeJson(JsonWriter writer) throws IOException { + if (lang != null) { + writer.name("lang").value(lang); + } + if (meta != null) { + writer.name("meta").value(meta); + } + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstContent.java new file mode 100644 index 00000000000..26c33a42ff5 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstContent.java @@ -0,0 +1,7 @@ +package appeng.libs.mdast.model; + +/** + * Content represents runs of text that form definitions and paragraphs. + */ +public interface MdAstContent extends MdAstFlowContent { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstDefinition.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstDefinition.java new file mode 100644 index 00000000000..53640f826d6 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstDefinition.java @@ -0,0 +1,82 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistNode; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * Represents a resource. + *

+ * Definition can be used where content is expected. It has no content model. + *

+ * Definition should be associated with LinkReferences and ImageReferences. + *

+ * For example, the following markdown: + *

+ * [Alpha]: https://example.com + *

+ * Yields: + *

+ * { + * type: 'definition', + * identifier: 'alpha', + * label: 'Alpha', + * url: 'https://example.com', + * title: null + * } + */ +public class MdAstDefinition extends MdAstNode implements MdAstAssociation, MdAstResource, MdAstContent { + public String identifier = ""; + public String label; + public String url = ""; + public String title; + + public MdAstDefinition() { + super("definition"); + } + + @Override + public String identifier() { + return identifier; + } + + @Override + public @Nullable String label() { + return label; + } + + @Override + public String url() { + return url; + } + + @Override + public @Nullable String title() { + return title; + } + + @Override + public void toText(StringBuilder buffer) { + buffer.append(label); + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + if (identifier != null) { + writer.name("identifier").value(identifier); + } + if (label != null) { + writer.name("label").value(label); + } + if (title != null) { + writer.name("title").value(title); + } + if (url != null) { + writer.name("url").value(url); + } + + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstEmphasis.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstEmphasis.java new file mode 100644 index 00000000000..1d234a8a126 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstEmphasis.java @@ -0,0 +1,37 @@ +package appeng.libs.mdast.model; + +import java.util.List; + +/** + * Emphasis (Parent) represents stress emphasis of its contents. + * Emphasis can be used where phrasing content is expected. Its content model is transparent content. + * For example, the following markdown: + * *alpha* _bravo_ + * Yields: + *

+ * {
+ * type: 'paragraph',
+ * children: [
+ * {
+ * type: 'emphasis',
+ * children: [{type: 'text', value: 'alpha'}]
+ * },
+ * {type: 'text', value: ' '},
+ * {
+ * type: 'emphasis',
+ * children: [{type: 'text', value: 'bravo'}]
+ * }
+ * ]
+ * }
+ * 
+ */ +public class MdAstEmphasis extends MdAstParent implements MdAstStaticPhrasingContent { + public MdAstEmphasis() { + super("emphasis"); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstFlowContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstFlowContent.java new file mode 100644 index 00000000000..08b36631824 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstFlowContent.java @@ -0,0 +1,4 @@ +package appeng.libs.mdast.model; + +public interface MdAstFlowContent extends MdAstAnyContent { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHTML.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHTML.java new file mode 100644 index 00000000000..fb0099b0be6 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHTML.java @@ -0,0 +1,22 @@ +package appeng.libs.mdast.model; + +/** + * Represents a fragment of raw HTML. + *

+ * HTML can be used where flow or phrasing content is expected. Its content is represented by its value field. + *

+ * HTML nodes do not have the restriction of being valid or complete HTML ([HTML]) constructs. + *

+ * For example, the following markdown: + *

+ * <div> + *

+ * Yields: + *

+ * {type: 'html', value: '<div>'} + */ +public class MdAstHTML extends MdAstLiteral implements MdAstFlowContent, MdAstStaticPhrasingContent { + public MdAstHTML() { + super("html"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHeading.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHeading.java new file mode 100644 index 00000000000..87d767aa4ad --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstHeading.java @@ -0,0 +1,45 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * Heading (Parent) represents a heading of a section. + *

+ * Heading can be used where flow content is expected. Its content model is phrasing content. + *

+ * For example, the following markdown: + *

+ * # Alpha + *

+ * Yields: + *

+ * { + * type: 'heading', + * depth: 1, + * children: [{type: 'text', value: 'Alpha'}] + * } + */ +public class MdAstHeading extends MdAstParent implements MdAstFlowContent { + /** + * Ranges from 1 to 6. + * 1 is the highest level heading, 6 the lowest. + */ + public int depth; + + public MdAstHeading() { + super("heading"); + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("depth").value(depth); + super.writeJson(writer); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImage.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImage.java new file mode 100644 index 00000000000..1a9cf42fd16 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImage.java @@ -0,0 +1,79 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * Image (Node) represents an image. + *

+ * Image can be used where phrasing content is expected. It has no content model, but is described by its alt field. + *

+ * Image includes the mixins Resource and Alternative. + *

+ * For example, the following markdown: + *

+ * ![alpha](https://example.com/favicon.ico "bravo") + *

+ * Yields: + *

+ * { + * type: 'image', + * url: 'https://example.com/favicon.ico', + * title: 'bravo', + * alt: 'alpha' + * } + */ +public class MdAstImage extends MdAstNode implements MdAstResource, MdAstAlternative, MdAstStaticPhrasingContent { + public String alt; + public String url = ""; + public String title; + + public MdAstImage() { + super("image"); + } + + @Override + public @Nullable String alt() { + return alt; + } + + @Override + public String url() { + return url; + } + + @Override + public @Nullable String title() { + return title; + } + + public void setAlt(String alt) { + this.alt = alt; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public void toText(StringBuilder buffer) { + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + if (title != null) { + writer.name("title").value(title); + } + writer.name("url").value(url); + if (alt != null) { + writer.name("alt").value(alt); + } + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImageReference.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImageReference.java new file mode 100644 index 00000000000..20b8dbc0fc3 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstImageReference.java @@ -0,0 +1,76 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * ImageReference (Node) represents an image through association, or its original source if there is no association. + *

+ * ImageReference can be used where phrasing content is expected. It has no content model, but is described by its alt field. + *

+ * ImageReference should be associated with a Definition. + *

+ * For example, the following markdown: + *

+ * ![alpha][bravo] + *

+ * Yields: + *

+ *

+ * {
+ * type: 'imageReference',
+ * identifier: 'bravo',
+ * label: 'bravo',
+ * referenceType: 'full',
+ * alt: 'alpha'
+ * }
+ * 
+ */ +public class MdAstImageReference extends MdAstNode implements MdAstReference, MdAstAlternative, MdAstStaticPhrasingContent { + public String alt; + public String identifier; + public String label; + public MdAstReferenceType referenceType; + + public MdAstImageReference() { + super("imageReference"); + } + + @Override + public @Nullable String alt() { + return alt; + } + + @Override + public String identifier() { + return identifier; + } + + @Override + public @Nullable String label() { + return label; + } + + @Override + public void toText(StringBuilder buffer) { + } + + @Override + public MdAstReferenceType referenceType() { + return referenceType; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + if (alt != null) { + writer.name("alt").value(alt); + } + writer.name("identifier").value(identifier); + writer.name("label").value(label); + writer.name("referenceType").value(referenceType.getSerializedName()); + + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstInlineCode.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstInlineCode.java new file mode 100644 index 00000000000..fe467575530 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstInlineCode.java @@ -0,0 +1,22 @@ +package appeng.libs.mdast.model; + +/** + * InlineCode (Literal) represents a fragment of computer code, such as a file name, computer program, or anything a computer could parse. + *

+ * InlineCode can be used where phrasing content is expected. Its content is represented by its value field. + *

+ * This node relates to the flow content concept Code. + *

+ * For example, the following markdown: + *

+ * `foo()` + *

+ * Yields: + *

+ * {type: 'inlineCode', value: 'foo()'} + */ +public class MdAstInlineCode extends MdAstLiteral implements MdAstStaticPhrasingContent { + public MdAstInlineCode() { + super("inlineCode"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLink.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLink.java new file mode 100644 index 00000000000..8b0beec0b87 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLink.java @@ -0,0 +1,62 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.List; + +/** + * Link includes Resource + *

+ * Link (Parent) represents a hyperlink. + *

+ * Link can be used where phrasing content is expected. Its content model is static phrasing content. + *

+ * Link includes the mixin Resource. + *

+ * For example, the following markdown: + *

+ * [alpha](https://example.com "bravo") + *

+ * Yields: + *

+ * { + * type: 'link', + * url: 'https://example.com', + * title: 'bravo', + * children: [{type: 'text', value: 'alpha'}] + * } + */ +public class MdAstLink extends MdAstParent implements MdAstPhrasingContent, MdAstResource { + public String url = ""; + public String title; + + public MdAstLink() { + super("link"); + } + + @Override + protected Class childClass() { + return MdAstStaticPhrasingContent.class; + } + + @Override + public String url() { + return url; + } + + @Override + public @Nullable String title() { + return title; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + if (title != null) { + writer.name("title").value(title); + } + writer.name("url").value(url); + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLinkReference.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLinkReference.java new file mode 100644 index 00000000000..b6e2b6da1c0 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLinkReference.java @@ -0,0 +1,68 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +/** + * LinkReference (Parent) represents a hyperlink through association, or its original source if there is no association. + *

+ * LinkReference can be used where phrasing content is expected. Its content model is static phrasing content. + *

+ * LinkReferences should be associated with a Definition. + *

+ * For example, the following markdown: + *

+ * [alpha][Bravo] + *

+ * Yields: + *

+ *

+ * {
+ * type: 'linkReference',
+ * identifier: 'bravo',
+ * label: 'Bravo',
+ * referenceType: 'full',
+ * children: [{type: 'text', value: 'alpha'}]
+ * }
+ * 
+ */ +public class MdAstLinkReference extends MdAstParent implements MdAstReference, MdAstPhrasingContent { + public String identifier; + public String label; + public MdAstReferenceType referenceType; + + public MdAstLinkReference() { + super("linkReference"); + } + + @Override + public String identifier() { + return identifier; + } + + @Override + public @Nullable String label() { + return label; + } + + @Override + public MdAstReferenceType referenceType() { + return referenceType; + } + + @Override + protected Class childClass() { + return MdAstStaticPhrasingContent.class; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("identifier").value(identifier); + writer.name("label").value(label); + writer.name("referenceType").value(referenceType.getSerializedName()); + + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstList.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstList.java new file mode 100644 index 00000000000..037517e203f --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstList.java @@ -0,0 +1,69 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * List (Parent) represents a list of items. + *

+ * List can be used where flow content is expected. Its content model is list content. + *

+ *

+ * For example, the following markdown: + *

+ * 1. foo + *

+ * Yields: + *

+ * { + * type: 'list', + * ordered: true, + * start: 1, + * spread: false, + * children: [{ + * type: 'listItem', + * spread: false, + * children: [{ + * type: 'paragraph', + * children: [{type: 'text', value: 'foo'}] + * }] + * }] + * } + */ +public class MdAstList extends MdAstParent implements MdAstFlowContent { + /** + * Represents that the items have been intentionally ordered (when true), + * or that the order of items is not important (when false). + */ + public boolean ordered; + /** + * Represents, when the ordered field is true, the starting number of the list. + */ + public int start = 1; + /** + * Represents that one or more of its children are separated with a + * blank line from its siblings (when true), or not (when false). + */ + public boolean spread; + + @Override + protected Class childClass() { + return MdAstListContent.class; + } + + public MdAstList() { + super("list"); + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("ordered").value(ordered); + if (ordered) { + writer.name("start").value(start); + } + writer.name("spread").value(spread); + + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListContent.java new file mode 100644 index 00000000000..6e807f802f4 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListContent.java @@ -0,0 +1,7 @@ +package appeng.libs.mdast.model; + +/** + * List content represent the items in a list. + */ +public interface MdAstListContent extends MdAstAnyContent { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListItem.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListItem.java new file mode 100644 index 00000000000..b1127442980 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstListItem.java @@ -0,0 +1,49 @@ +package appeng.libs.mdast.model; + +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * An item in a {@link MdAstList}. + *

+ * It can be used where list content is expected. Its content model is flow content. + *

+ * For example, the following markdown: + *

+ * * bar + *

+ * Yields: + *

+ * { + * type: 'listItem', + * spread: false, + * children: [{ + * type: 'paragraph', + * children: [{type: 'text', value: 'bar'}] + * }] + * } + */ +public class MdAstListItem extends MdAstParent implements MdAstListContent { + public MdAstListItem() { + super("listItem"); + } + + /** + * Represents that the item contains two or more children separated by a blank line (when true), + * or not (when false). + */ + public boolean spread; + + @Override + protected Class childClass() { + return MdAstFlowContent.class; + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("spread").value(spread); + super.writeJson(writer); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLiteral.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLiteral.java new file mode 100644 index 00000000000..8d507bc724f --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstLiteral.java @@ -0,0 +1,38 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistLiteral; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +/** + * Literal (UnistLiteral) represents an abstract public interface in mdast containing a value. + *

+ * Its value field is a string. + */ +public abstract class MdAstLiteral extends MdAstNode implements UnistLiteral { + public String value = ""; + + public MdAstLiteral(String type) { + super(type); + } + + @Override + public void toText(StringBuilder buffer) { + buffer.append(value); + } + + @Override + public String value() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public void writeJson(JsonWriter writer) throws IOException { + writer.name("value").value(value); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstNode.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstNode.java new file mode 100644 index 00000000000..ae3ff962eef --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstNode.java @@ -0,0 +1,53 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistNode; +import appeng.libs.unist.UnistPosition; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public abstract class MdAstNode implements UnistNode { + private final String type; + public Object data; + public MdAstPosition position; + + public MdAstNode(String type) { + this.type = type; + } + + @Override + public final String type() { + return type; + } + + @Override + public @Nullable Object data() { + return data; + } + + @Override + public @Nullable UnistPosition position() { + return position; + } + + public void setData(Object data) { + this.data = data; + } + + public abstract void toText(StringBuilder buffer); + + public final void toJson(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name("type").value(type()); + writeJson(writer); + if (position != null) { + writer.name("position"); + position.writeJson(writer); + } + writer.endObject(); + } + + protected void writeJson(JsonWriter writer) throws IOException { + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParagraph.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParagraph.java new file mode 100644 index 00000000000..819b58f3597 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParagraph.java @@ -0,0 +1,28 @@ +package appeng.libs.mdast.model; + +/** + * Paragraph (Parent) represents a unit of discourse dealing with a particular point or idea. + *

+ * Paragraph can be used where content is expected. Its content model is phrasing content. + *

+ * For example, the following markdown: + *

+ * Alpha bravo charlie. + *

+ * Yields: + *

+ * { + * type: 'paragraph', + * children: [{type: 'text', value: 'Alpha bravo charlie.'}] + * } + */ +public class MdAstParagraph extends MdAstParent implements MdAstContent { + public MdAstParagraph() { + super("paragraph"); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParent.java new file mode 100644 index 00000000000..bf2c45af1f3 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstParent.java @@ -0,0 +1,68 @@ +package appeng.libs.mdast.model; + +import appeng.libs.unist.UnistParent; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Parent (UnistParent) represents an abstract public interface in mdast containing other nodes (said to be children). + *

+ * Its content is limited to only other mdast content. + */ +public abstract class MdAstParent extends MdAstNode implements UnistParent { + private final List children; + + public MdAstParent(String type) { + super(type); + this.children = new ArrayList<>(); + } + + @Override + public List children() { + return children; + } + + protected abstract Class childClass(); + + public void addChild(MdAstNode node) { + if (!childClass().isInstance(node)) { + throw new IllegalArgumentException("Cannot add a node of type " + node.getClass() + " to " + this); + } + children.add(childClass().cast(node)); + } + + @Override + protected void writeJson(JsonWriter writer) throws IOException { + writer.name("children"); + writer.beginArray(); + for (T child : children) { + ((MdAstNode) child).toJson(writer); + } + writer.endArray(); + } + + @Override + public void toText(StringBuilder buffer) { + for (var child : children) { + if (child instanceof MdAstNode childNode) { + childNode.toText(buffer); + } + } + } + + public void replaceChild(MdAstNode child, MdAstNode replacement) { + var replacementChild = childClass().cast(replacement); + + for (int i = 0; i < children.size(); i++) { + if (children.get(i) == child) { + children.set(i, replacementChild); + return; + } + } + + throw new IllegalStateException("Child " + child + " not found"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPhrasingContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPhrasingContent.java new file mode 100644 index 00000000000..dd8bc9b60a5 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPhrasingContent.java @@ -0,0 +1,7 @@ +package appeng.libs.mdast.model; + +/** + * Phrasing content represent the text in a document, and its markup. + */ +public interface MdAstPhrasingContent extends MdAstAnyContent { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPosition.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPosition.java new file mode 100644 index 00000000000..c5e6a050283 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstPosition.java @@ -0,0 +1,76 @@ +package appeng.libs.mdast.model; + +import appeng.libs.micromark.Point; +import appeng.libs.unist.UnistPoint; +import appeng.libs.unist.UnistPosition; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +public class MdAstPosition implements UnistPosition { + public UnistPoint start; + public UnistPoint end; + int @Nullable [] indent; // number >= 1 + + public MdAstPosition() { + } + + public MdAstPosition(UnistPoint start, UnistPoint end) { + this.start = start; + this.end = end; + } + + public static String stringify(UnistPoint point) { + return point.line() + ":" + point.column(); + } + + public static String stringify(UnistPosition position) { + return stringify(position.start(), position.end()); + } + + public static String stringify(UnistPoint start, UnistPoint end) { + var result = new StringBuilder(); + if (start != null ) { + result.append(start.line()).append(":").append(start.column()); + } + if (end != null) { + result.append("-").append(end.line()).append(":").append(end.column()); + } + return result.toString(); + } + + @Override + public UnistPoint start() { + return start; + } + + @Override + public UnistPoint end() { + return end; + } + + @Override + public int @Nullable [] indent() { + return indent; + } + + public MdAstPosition withStart(UnistPoint point) { + this.start = point; + return this; + } + + public MdAstPosition withEnd(UnistPoint point) { + this.end = point; + return this; + } + + public void writeJson(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name("start"); + start.writeJson(writer); + writer.name("end"); + end.writeJson(writer); + writer.endObject(); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReference.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReference.java new file mode 100644 index 00000000000..37992f6a1d8 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReference.java @@ -0,0 +1,11 @@ +package appeng.libs.mdast.model; + +/** + * Reference represents a marker that is associated to another node. + */ +public interface MdAstReference extends MdAstAssociation { + /** + * The explicitness of the reference. + */ + MdAstReferenceType referenceType(); +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReferenceType.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReferenceType.java new file mode 100644 index 00000000000..85403faf34e --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstReferenceType.java @@ -0,0 +1,26 @@ +package appeng.libs.mdast.model; + +public enum MdAstReferenceType { + /** + * The reference is implicit, its identifier inferred from its content. + */ + SHORTCUT("shortcut"), + /** + * The reference is explicit, its identifier inferred from its content. + */ + COLLAPSED("collapsed"), + /** + * The reference is explicit, its identifier explicitly set. + */ + FULL("full"); + + private final String serializedName; + + MdAstReferenceType(String serializedName) { + this.serializedName = serializedName; + } + + public String getSerializedName() { + return serializedName; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstResource.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstResource.java new file mode 100644 index 00000000000..22f79d6f644 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstResource.java @@ -0,0 +1,19 @@ +package appeng.libs.mdast.model; + +import org.jetbrains.annotations.Nullable; + +/** + * A reference to a resource. + */ +public interface MdAstResource { + /** + * The URL of the referenced resource. + */ + String url(); + + /** + * Advisory information for the resource, such as would be appropriate for a tooltip. + */ + @Nullable + String title(); +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstRoot.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstRoot.java new file mode 100644 index 00000000000..6167f41cb90 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstRoot.java @@ -0,0 +1,18 @@ +package appeng.libs.mdast.model; + +/** + * Root (Parent) represents a document. + *

+ * Root can be used as the root of a tree, never as a child. Its content model is not limited to flow content, + * but instead can contain any mdast content with the restriction that all content must be of the same category. + */ +public class MdAstRoot extends MdAstParent { + public MdAstRoot() { + super("root"); + } + + @Override + protected Class childClass() { + return MdAstAnyContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStaticPhrasingContent.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStaticPhrasingContent.java new file mode 100644 index 00000000000..a34ae848385 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStaticPhrasingContent.java @@ -0,0 +1,7 @@ +package appeng.libs.mdast.model; + +/** + * StaticPhrasing content represent the text in a document, and its markup, that is not intended for user interaction. + */ +public interface MdAstStaticPhrasingContent extends MdAstPhrasingContent { +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStrong.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStrong.java new file mode 100644 index 00000000000..fc9f5f66586 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstStrong.java @@ -0,0 +1,42 @@ +package appeng.libs.mdast.model; + +import java.util.List; + +/** + * Strong (Parent) represents strong importance, seriousness, or urgency for its contents. + *

+ * Strong can be used where phrasing content is expected. Its content model is transparent content. + *

+ * For example, the following markdown: + *

+ * **alpha** __bravo__ + *

+ * Yields: + *

+ *

+ * {
+ * type: 'paragraph',
+ * children: [
+ * {
+ * type: 'strong',
+ * children: [{type: 'text', value: 'alpha'}]
+ * },
+ * {type: 'text', value: ' '},
+ * {
+ * type: 'strong',
+ * children: [{type: 'text', value: 'bravo'}]
+ * }
+ * ]
+ * }
+ * 
+ */ +public class MdAstStrong extends MdAstParent implements MdAstStaticPhrasingContent { + public MdAstStrong() { + super("strong"); + } + + @Override + protected Class childClass() { + return MdAstPhrasingContent.class; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstText.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstText.java new file mode 100644 index 00000000000..b393efc54a4 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstText.java @@ -0,0 +1,20 @@ +package appeng.libs.mdast.model; + +/** + * Represents everything that is just text. + *

+ * Text can be used where phrasing content is expected. Its content is represented by its value field. + *

+ * For example, the following markdown: + *

+ * Alpha bravo charlie. + *

+ * Yields: + *

+ * {type: 'text', value: 'Alpha bravo charlie.'} + */ +public class MdAstText extends MdAstLiteral implements MdAstStaticPhrasingContent { + public MdAstText() { + super("text"); + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstThematicBreak.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstThematicBreak.java new file mode 100644 index 00000000000..4ad320df5eb --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/MdAstThematicBreak.java @@ -0,0 +1,24 @@ +package appeng.libs.mdast.model; + +/** + * ThematicBreak (Node) represents a thematic break, such as a scene change in a story, a transition to another topic, or a new document. + *

+ * ThematicBreak can be used where flow content is expected. It has no content model. + *

+ * For example, the following markdown: + *

+ * *** + *

+ * Yields: + *

+ * {type: 'thematicBreak'} + */ +public class MdAstThematicBreak extends MdAstNode implements MdAstFlowContent { + public MdAstThematicBreak() { + super("thematicBreak"); + } + + @Override + public void toText(StringBuilder buffer) { + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdast/model/package-info.java b/libs/markdown/src/main/java/appeng/libs/mdast/model/package-info.java new file mode 100644 index 00000000000..c4936d1c9c4 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdast/model/package-info.java @@ -0,0 +1,5 @@ +/** + * Markdown Abstract Syntax Tree. + * See https://github.com/syntax-tree/mdast + */ +package appeng.libs.mdast.model; diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/EcmaScriptIdentifiers.java b/libs/markdown/src/main/java/appeng/libs/mdx/EcmaScriptIdentifiers.java new file mode 100644 index 00000000000..3d15852c38b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/EcmaScriptIdentifiers.java @@ -0,0 +1,28 @@ +package appeng.libs.mdx; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public final class EcmaScriptIdentifiers { + + // See https://raw.githubusercontent.com/syntax-tree/estree-util-is-identifier-name/main/regex.js + private static final Predicate start = Pattern.compile( + "[$A-Z_a-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08C7\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7CA\uA7F5-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]" + ).asMatchPredicate(); + + private static final Predicate cont = Pattern.compile( + "[\\d\u00B7\u0300-\u036F\u0387\u0483-\u0487\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u0669\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0966-\u096F\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09E6-\u09EF\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B66-\u0B6F\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BEF\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D6F\u0D81-\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0E50-\u0E59\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1040-\u1049\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109D\u135D-\u135F\u1369-\u1371\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1946-\u194F\u19D0-\u19DA\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AB0-\u1ABD\u1ABF\u1AC0\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BF3\u1C24-\u1C37\u1C40-\u1C49\u1C50-\u1C59\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200C\u200D\u203F\u2040\u2054\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA620-\uA629\uA66F\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA82C\uA880\uA881\uA8B4-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F1\uA8FF-\uA909\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9D0-\uA9D9\uA9E5\uA9F0-\uA9F9\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uABF0-\uABF9\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFF10-\uFF19\uFF3F]" + ).asMatchPredicate(); + + private EcmaScriptIdentifiers() { + } + + public static boolean isStart(int code) { + return start.test(String.valueOf((char) code)); + } + + public static boolean isCont(int code) { + return isStart(code) || cont.test(String.valueOf((char) code)); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/FactoryMdxExpression.java b/libs/markdown/src/main/java/appeng/libs/mdx/FactoryMdxExpression.java new file mode 100644 index 00000000000..34e97b69da2 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/FactoryMdxExpression.java @@ -0,0 +1,146 @@ +package appeng.libs.mdx; + +import appeng.libs.micromark.Assert; +import appeng.libs.micromark.CharUtil; +import appeng.libs.micromark.ParseException; +import appeng.libs.micromark.Point; +import appeng.libs.micromark.State; +import appeng.libs.micromark.TokenizeContext; +import appeng.libs.micromark.Tokenizer; +import appeng.libs.micromark.Types; +import appeng.libs.micromark.factory.FactorySpace; +import appeng.libs.micromark.symbol.Codes; +import appeng.libs.micromark.symbol.Constants; + +public final class FactoryMdxExpression { + + private FactoryMdxExpression() { + } + + public static State create( + TokenizeContext context, + Tokenizer.Effects effects, + State ok, + String type, + String markerType, + String chunkType, + boolean allowLazy, + int startColumn + ) { + + class StateMachine { + final Tokenizer.Event tail = context.getLastEvent(); + final int initialPrefix = + tail != null && tail.token().type.equals(Types.linePrefix) + ? tail.context().sliceSerialize(tail.token(), true).length() + : 0; + final int prefixExpressionIndent = initialPrefix != 0 ? initialPrefix + 1 : 0; + int balance = 1; + Point startPosition; + RuntimeException lastCrash; + + State start(int code) { + Assert.check(code == Codes.leftCurlyBrace, "expected `{`"); + effects.enter(type); + effects.enter(markerType); + effects.consume(code); + effects.exit(markerType); + startPosition = context.now(); + return this::atBreak; + } + + State atBreak(int code) { + if (code == Codes.eof) { + if (lastCrash != null) { + throw lastCrash; + } + throw new ParseException( + "Unexpected end of file in expression, expected a corresponding closing brace for `{`", + context.now(), + "micromark-extension-mdx-expression:unexpected-eof" + ); + } + + if (code == Codes.rightCurlyBrace) { + return atClosingBrace(code); + } + + if (CharUtil.markdownLineEnding(code)) { + effects.enter(Types.lineEnding); + effects.consume(code); + effects.exit(Types.lineEnding); + // `startColumn` is used by the JSX extensions that also wraps this + // factory. + // JSX can be indented arbitrarily, but expressions can’t exdent + // arbitrarily, due to that they might contain template strings + // (backticked strings). + // We’ll eat up to where that tag starts (`startColumn`), and a tab size. + /* c8 ignore next 3 */ + var prefixTagIndent = startColumn != 0 + ? startColumn + Constants.tabSize - context.now().column() + : 0; + var indent = Math.max(prefixExpressionIndent, prefixTagIndent); + return indent != 0 + ? FactorySpace.create(effects, this::atBreak, Types.linePrefix, indent) + : this::atBreak; + } + + var now = context.now(); + + if ( + now.line() != startPosition.line() && + !allowLazy && + context.isOnLazyLine() + ) { + throw new ParseException( + "Unexpected end of file in expression, expected a corresponding closing brace for `{`", + context.now(), + "micromark-extension-mdx-expression:unexpected-eof" + ); + } + + effects.enter(chunkType); + return inside(code); + } + + State inside(int code) { + if ( + code == Codes.eof || + code == Codes.rightCurlyBrace || + CharUtil.markdownLineEnding(code) + ) { + effects.exit(chunkType); + return atBreak(code); + } + + if (code == Codes.leftCurlyBrace) { + effects.consume(code); + balance++; + return this::inside; + } + + effects.consume(code); + return this::inside; + } + + State atClosingBrace(int code) { + balance--; + + // Agnostic mode: count balanced braces. + if (balance != 0) { + effects.enter(chunkType); + effects.consume(code); + return this::inside; + } + + effects.enter(markerType); + effects.consume(code); + effects.exit(markerType); + effects.exit(type); + return ok; + } + } + + return new StateMachine()::start; + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/FactoryTag.java b/libs/markdown/src/main/java/appeng/libs/mdx/FactoryTag.java new file mode 100644 index 00000000000..98e504655f3 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/FactoryTag.java @@ -0,0 +1,792 @@ +package appeng.libs.mdx; + +import appeng.libs.micromark.Assert; +import appeng.libs.micromark.CharUtil; +import appeng.libs.micromark.Construct; +import appeng.libs.micromark.ParseException; +import appeng.libs.micromark.Point; +import appeng.libs.micromark.State; +import appeng.libs.micromark.TokenizeContext; +import appeng.libs.micromark.Tokenizer; +import appeng.libs.micromark.Types; +import appeng.libs.micromark.factory.FactorySpace; +import appeng.libs.micromark.symbol.Codes; +import appeng.libs.micromark.symbol.Constants; + +import java.util.Locale; + +import static appeng.libs.mdx.EcmaScriptIdentifiers.isCont; +import static appeng.libs.mdx.EcmaScriptIdentifiers.isStart; + +public final class FactoryTag { + private FactoryTag() { + } + + private static final Construct lazyLineEnd; + + static { + lazyLineEnd = new Construct(); + lazyLineEnd.tokenize = FactoryTag::tokenizeLazyLineEnd; + lazyLineEnd.partial = true; + } + + public static State create( + TokenizeContext context, + Tokenizer.Effects effects, + State ok, + State nok, + boolean allowLazy, + String tagType, + String tagMarkerType, + String tagClosingMarkerType, + String tagSelfClosingMarker, + String tagNameType, + String tagNamePrimaryType, + String tagNameMemberMarkerType, + String tagNameMemberType, + String tagNamePrefixMarkerType, + String tagNameLocalType, + String tagExpressionAttributeType, + String tagExpressionAttributeMarkerType, + String tagExpressionAttributeValueType, + String tagAttributeType, + String tagAttributeNameType, + String tagAttributeNamePrimaryType, + String tagAttributeNamePrefixMarkerType, + String tagAttributeNameLocalType, + String tagAttributeInitializerMarkerType, + String tagAttributeValueLiteralType, + String tagAttributeValueLiteralMarkerType, + String tagAttributeValueLiteralValueType, + String tagAttributeValueExpressionType, + String tagAttributeValueExpressionMarkerType, + String tagAttributeValueExpressionValueType + ) { + + class StateMachine { + State returnState; + Integer marker; + Point startPoint; + + State start(int code) { + Assert.check(code == Codes.lessThan, "expected `<`"); + startPoint = context.now(); + effects.enter(tagType); + effects.enter(tagMarkerType); + effects.consume(code); + effects.exit(tagMarkerType); + return this::afterStart; + } + + State afterStart(int code) { + // Deviate from JSX, which allows arbitrary whitespace. + // See: . + if (CharUtil.markdownLineEnding(code) || CharUtil.markdownSpace(code)) { + return nok.step(code); + } + + // Any other ES whitespace does not get this treatment. + returnState = this::beforeName; + return optionalEsWhitespace(code); + } + + // Right after `<`, before an optional name. + State beforeName(int code) { + // Closing tag. + if (code == Codes.slash) { + effects.enter(tagClosingMarkerType); + effects.consume(code); + effects.exit(tagClosingMarkerType); + returnState = this::beforeClosingTagName; + return this::optionalEsWhitespace; + } + + // Fragment opening tag. + if (code == Codes.greaterThan) { + return tagEnd(code); + } + + // Start of a name. + if (code != Codes.eof && isStart(code)) { + effects.enter(tagNameType); + effects.enter(tagNamePrimaryType); + effects.consume(code); + return this::primaryName; + } + + return crash( + code, + "before name", + "a character that can start a name, such as a letter, `$`, or `_`" + + (code == Codes.exclamationMark + ? " (note: to create a comment in MDX, use `{/* text */}`)" + : "") + ); + } + + // At the start of a closing tag, right after ` Codes.dot && + code < Codes.colon) /* `/` - `9` */ + ? " (note: to create a link in MDX, use `[text](url)`)" + : "") + ); + } + + // Inside the local name. + State localName(int code) { + // Continuation of local name: stay in state + if (code == Codes.dash || (code != Codes.eof && isCont(code))) { + effects.consume(code); + return this::localName; + } + + // End of local name (note that we don’t expect another colon, or a member). + if ( + code == Codes.slash || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + CharUtil.markdownLineEndingOrSpace(code) || + CharUtil.unicodeWhitespace(code) + ) { + effects.exit(tagNameLocalType); + returnState = this::afterLocalName; + return optionalEsWhitespace(code); + } + + return crash( + code, + "in local name", + "a name character such as letters, digits, `$`, or `_`; whitespace before attributes; or the end of the tag" + ); + } + + // After a local name: this is the same as `afterPrimaryName` but we don’t + // expect colons or periods. + State afterLocalName(int code) { + // End of name. + if ( + code == Codes.slash || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + (code != Codes.eof && isStart(code)) + ) { + effects.exit(tagNameType); + return beforeAttribute(code); + } + + return crash( + code, + "after local name", + "a character that can start an attribute name, such as a letter, `$`, or `_`; whitespace before attributes; or the end of the tag" + ); + } + + State beforeAttribute(int code) { + // Mark as self closing. + if (code == Codes.slash) { + effects.enter(tagSelfClosingMarker); + effects.consume(code); + effects.exit(tagSelfClosingMarker); + returnState = this::selfClosing; + return this::optionalEsWhitespace; + } + + // End of tag. + if (code == Codes.greaterThan) { + return tagEnd(code); + } + + // Attribute expression. + if (code == Codes.leftCurlyBrace) { + Assert.check(startPoint != null, "expected `startPoint` to be defined"); + return FactoryMdxExpression.create( + context, + effects, + this::afterAttributeExpression, + tagExpressionAttributeType, + tagExpressionAttributeMarkerType, + tagExpressionAttributeValueType, + allowLazy, + startPoint.column() + ).step(code); + } + + // Start of an attribute name. + if (code != Codes.eof && isStart(code)) { + effects.enter(tagAttributeType); + effects.enter(tagAttributeNameType); + effects.enter(tagAttributeNamePrimaryType); + effects.consume(code); + return this::attributePrimaryName; + } + + return crash( + code, + "before attribute name", + "a character that can start an attribute name, such as a letter, `$`, or `_`; whitespace before attributes; or the end of the tag" + ); + } + + // At the start of an attribute expression. + State afterAttributeExpression(int code) { + returnState = this::beforeAttribute; + return optionalEsWhitespace(code); + } + + // In the attribute name. + State attributePrimaryName(int code) { + // Continuation of the attribute name. + if (code == Codes.dash || (code != Codes.eof && isCont(code))) { + effects.consume(code); + return this::attributePrimaryName; + } + + // End of attribute name or tag. + if ( + code == Codes.slash || + code == Codes.colon || + code == Codes.equalsTo || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + CharUtil.markdownLineEndingOrSpace(code) || + CharUtil.unicodeWhitespace(code) + ) { + effects.exit(tagAttributeNamePrimaryType); + returnState = this::afterAttributePrimaryName; + return optionalEsWhitespace(code); + } + + return crash( + code, + "in attribute name", + "an attribute name character such as letters, digits, `$`, or `_`; `=` to initialize a value; whitespace before attributes; or the end of the tag" + ); + } + + // After an attribute name, probably finding an equals. + State afterAttributePrimaryName(int code) { + // Start of a local name. + if (code == Codes.colon) { + effects.enter(tagAttributeNamePrefixMarkerType); + effects.consume(code); + effects.exit(tagAttributeNamePrefixMarkerType); + returnState = this::beforeAttributeLocalName; + return this::optionalEsWhitespace; + } + + // Start of an attribute value. + if (code == Codes.equalsTo) { + effects.exit(tagAttributeNameType); + effects.enter(tagAttributeInitializerMarkerType); + effects.consume(code); + effects.exit(tagAttributeInitializerMarkerType); + returnState = this::beforeAttributeValue; + return this::optionalEsWhitespace; + } + + // End of tag / new attribute. + if ( + code == Codes.slash || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + CharUtil.markdownLineEndingOrSpace(code) || + CharUtil.unicodeWhitespace(code) || + (code != Codes.eof && isStart(code)) + ) { + effects.exit(tagAttributeNameType); + effects.exit(tagAttributeType); + returnState = this::beforeAttribute; + return optionalEsWhitespace(code); + } + + return crash( + code, + "after attribute name", + "a character that can start an attribute name, such as a letter, `$`, or `_`; `=` to initialize a value; or the end of the tag" + ); + } + + // We’ve seen a `:`, and are expecting a local name. + State beforeAttributeLocalName(int code) { + // Start of a local name. + if (code != Codes.eof && isStart(code)) { + effects.enter(tagAttributeNameLocalType); + effects.consume(code); + return this::attributeLocalName; + } + + return crash( + code, + "before local attribute name", + "a character that can start an attribute name, such as a letter, `$`, or `_`; `=` to initialize a value; or the end of the tag" + ); + } + + // In the local attribute name. + State attributeLocalName(int code) { + // Continuation of the local attribute name. + if (code == Codes.dash || (code != Codes.eof && isCont(code))) { + effects.consume(code); + return this::attributeLocalName; + } + + // End of tag / attribute name. + if ( + code == Codes.slash || + code == Codes.equalsTo || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + CharUtil.markdownLineEndingOrSpace(code) || + CharUtil.unicodeWhitespace(code) + ) { + effects.exit(tagAttributeNameLocalType); + effects.exit(tagAttributeNameType); + returnState = this::afterAttributeLocalName; + return optionalEsWhitespace(code); + } + + return crash( + code, + "in local attribute name", + "an attribute name character such as letters, digits, `$`, or `_`; `=` to initialize a value; whitespace before attributes; or the end of the tag" + ); + } + + // After a local attribute name, expecting an equals. + State afterAttributeLocalName(int code) { + // Start of an attribute value. + if (code == Codes.equalsTo) { + effects.enter(tagAttributeInitializerMarkerType); + effects.consume(code); + effects.exit(tagAttributeInitializerMarkerType); + returnState = this::beforeAttributeValue; + return this::optionalEsWhitespace; + } + + // End of tag / new attribute. + if ( + code == Codes.slash || + code == Codes.greaterThan || + code == Codes.leftCurlyBrace || + (code != Codes.eof && isStart(code)) + ) { + effects.exit(tagAttributeType); + return beforeAttribute(code); + } + + return crash( + code, + "after local attribute name", + "a character that can start an attribute name, such as a letter, `$`, or `_`; `=` to initialize a value; or the end of the tag" + ); + } + + // After an attribute value initializer, expecting quotes and such. + State beforeAttributeValue(int code) { + // Start of double- or single quoted value. + if (code == Codes.quotationMark || code == Codes.apostrophe) { + effects.enter(tagAttributeValueLiteralType); + effects.enter(tagAttributeValueLiteralMarkerType); + effects.consume(code); + effects.exit(tagAttributeValueLiteralMarkerType); + marker = code; + return this::attributeValueQuotedStart; + } + + // Start of an assignment expression. + if (code == Codes.leftCurlyBrace) { + Assert.check(startPoint != null, "expected `startPoint` to be defined"); + return FactoryMdxExpression.create( + context, + effects, + this::afterAttributeValueExpression, + tagAttributeValueExpressionType, + tagAttributeValueExpressionMarkerType, + tagAttributeValueExpressionValueType, + allowLazy, + startPoint.column() + ).step(code); + } + + return crash( + code, + "before attribute value", + "a character that can start an attribute value, such as `\"`, `'`, or `{`" + + (code == Codes.lessThan + ? " (note: to use an element or fragment as a prop value in MDX, use `{}`)" + : "") + ); + } + + State afterAttributeValueExpression(int code) { + effects.exit(tagAttributeType); + returnState = this::beforeAttribute; + return optionalEsWhitespace(code); + } + + // At the start of a quoted attribute value. + State attributeValueQuotedStart(int code) { + Assert.check(marker != null, "expected `marker` to be defined"); + + if (code == Codes.eof) { + crash( + code, + "in attribute value", + "a corresponding closing quote `" + (char) marker.intValue() + '`' + ); + } + + if (code == marker) { + effects.enter(tagAttributeValueLiteralMarkerType); + effects.consume(code); + effects.exit(tagAttributeValueLiteralMarkerType); + effects.exit(tagAttributeValueLiteralType); + effects.exit(tagAttributeType); + marker = null; + returnState = this::beforeAttribute; + return this::optionalEsWhitespace; + } + + if (CharUtil.markdownLineEnding(code)) { + returnState = this::attributeValueQuotedStart; + return optionalEsWhitespace(code); + } + + effects.enter(tagAttributeValueLiteralValueType); + return attributeValueQuoted(code); + } + + // In a quoted attribute value. + State attributeValueQuoted(int code) { + if (code == Codes.eof || code == marker || CharUtil.markdownLineEnding(code)) { + effects.exit(tagAttributeValueLiteralValueType); + return attributeValueQuotedStart(code); + } + + // Continuation. + effects.consume(code); + return this::attributeValueQuoted; + } + + // Right after the slash on a tag, e.g., `` to end the tag" + + (code == Codes.asterisk || code == Codes.slash + ? " (note: JS comments in JSX tags are not supported in MDX)" + : "") + ); + } + + // At a `>`. + State tagEnd(int code) { + Assert.check(code == Codes.greaterThan, "expected `>`"); + effects.enter(tagMarkerType); + effects.consume(code); + effects.exit(tagMarkerType); + effects.exit(tagType); + return ok; + } + + // Optionally start whitespace. + State optionalEsWhitespace(int code) { + if (CharUtil.markdownLineEnding(code)) { + if (allowLazy) { + effects.enter(Types.lineEnding); + effects.consume(code); + effects.exit(Types.lineEnding); + return FactorySpace.create( + effects, + this::optionalEsWhitespace, + Types.linePrefix, + Constants.tabSize + ); + } + + return effects.attempt.hook( + lazyLineEnd, + FactorySpace.create( + effects, + this::optionalEsWhitespace, + Types.linePrefix, + Constants.tabSize + ), + this::crashEol + ).step(code); + } + + if (CharUtil.markdownSpace(code) || CharUtil.unicodeWhitespace(code)) { + effects.enter("esWhitespace"); + return optionalEsWhitespaceContinue(code); + } + + return returnState.step(code); + } + + // Continue optional whitespace. + State optionalEsWhitespaceContinue(int code) { + if ( + CharUtil.markdownLineEnding(code) || + !(CharUtil.markdownSpace(code) || CharUtil.unicodeWhitespace(code)) + ) { + effects.exit("esWhitespace"); + return optionalEsWhitespace(code); + } + + effects.consume(code); + return this::optionalEsWhitespaceContinue; + } + + private State crashEol(int code) { + throw new ParseException( + "Unexpected lazy line in container, expected line to be prefixed with `>` when in a block quote, whitespace when in a list, etc", + context.now(), + "micromark-extension-mdx-jsx:unexpected-eof" + ); + } + + // Crash at a nonconforming character. + private T crash(int code, String at, String expect) { + throw new ParseException( + "Unexpected " + + (code == Codes.eof + ? "end of file" + : "character `" + + (code == Codes.graveAccent + ? "` ` `" + : String.valueOf((char) code)) + + "` (" + + serializeCharCode(code) + + ')') + + ' ' + + at + + ", expected " + + expect, + context.now(), + "micromark-extension-mdx-jsx:unexpected-" + + (code == Codes.eof ? "eof" : "character") + ); + } + } + + return new StateMachine()::start; + } + + private static State tokenizeLazyLineEnd(TokenizeContext context, Tokenizer.Effects effects, State ok, State nok) { + class StateMachine { + + State start(int code) { + Assert.check(CharUtil.markdownLineEnding(code), "expected eol"); + effects.enter(Types.lineEnding); + effects.consume(code); + effects.exit(Types.lineEnding); + return this::lineStart; + } + + private State lineStart(int code) { + return context.isOnLazyLine() ? nok.step(code) : ok.step(code); + } + } + + return new StateMachine()::start; + } + + private static String serializeCharCode(int code) { + return String.format(Locale.ROOT, "U+%04X", code); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/JsxFlow.java b/libs/markdown/src/main/java/appeng/libs/mdx/JsxFlow.java new file mode 100644 index 00000000000..ba6a31f509b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/JsxFlow.java @@ -0,0 +1,75 @@ +package appeng.libs.mdx; + +import appeng.libs.micromark.Assert; +import appeng.libs.micromark.CharUtil; +import appeng.libs.micromark.Construct; +import appeng.libs.micromark.State; +import appeng.libs.micromark.TokenizeContext; +import appeng.libs.micromark.Tokenizer; +import appeng.libs.micromark.Types; +import appeng.libs.micromark.factory.FactorySpace; +import appeng.libs.micromark.symbol.Codes; + +import java.util.ArrayList; + +final class JsxFlow { + + public static final Construct INSTANCE = new Construct(); + + static { + INSTANCE.tokenize = JsxFlow::tokenize; + INSTANCE.concrete = true; + } + + private static State tokenize(TokenizeContext context, Tokenizer.Effects effects, State ok, State nok) { + class StateMachine { + State start(int code) { + Assert.check(code == Codes.lessThan, "expected `<`"); + return FactoryTag.create( + context, + effects, + FactorySpace.create(effects, this::after, Types.whitespace), + nok, + false, + "mdxJsxFlowTag", + "mdxJsxFlowTagMarker", + "mdxJsxFlowTagClosingMarker", + "mdxJsxFlowTagSelfClosingMarker", + "mdxJsxFlowTagName", + "mdxJsxFlowTagNamePrimary", + "mdxJsxFlowTagNameMemberMarker", + "mdxJsxFlowTagNameMember", + "mdxJsxFlowTagNamePrefixMarker", + "mdxJsxFlowTagNameLocal", + "mdxJsxFlowTagExpressionAttribute", + "mdxJsxFlowTagExpressionAttributeMarker", + "mdxJsxFlowTagExpressionAttributeValue", + "mdxJsxFlowTagAttribute", + "mdxJsxFlowTagAttributeName", + "mdxJsxFlowTagAttributeNamePrimary", + "mdxJsxFlowTagAttributeNamePrefixMarker", + "mdxJsxFlowTagAttributeNameLocal", + "mdxJsxFlowTagAttributeInitializerMarker", + "mdxJsxFlowTagAttributeValueLiteral", + "mdxJsxFlowTagAttributeValueLiteralMarker", + "mdxJsxFlowTagAttributeValueLiteralValue", + "mdxJsxFlowTagAttributeValueExpression", + "mdxJsxFlowTagAttributeValueExpressionMarker", + "mdxJsxFlowTagAttributeValueExpressionValue" + ).step(code); + } + + State after(int code) { + // Another tag. + return code == Codes.lessThan + ? start(code) + : code == Codes.eof || CharUtil.markdownLineEnding(code) + ? ok.step(code) + : nok.step(code); + } + } + + return new StateMachine()::start; + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/JsxText.java b/libs/markdown/src/main/java/appeng/libs/mdx/JsxText.java new file mode 100644 index 00000000000..aa6df977d3a --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/JsxText.java @@ -0,0 +1,51 @@ +package appeng.libs.mdx; + +import appeng.libs.micromark.Construct; +import appeng.libs.micromark.State; +import appeng.libs.micromark.TokenizeContext; +import appeng.libs.micromark.Tokenizer; + +final class JsxText { + + public static final Construct INSTANCE = new Construct(); + + static { + INSTANCE.tokenize = JsxText::tokenize; + } + + private static State tokenize(TokenizeContext context, Tokenizer.Effects effects, State ok, State nok) { + return FactoryTag.create( + context, + effects, + ok, + nok, + true, + "mdxJsxTextTag", + "mdxJsxTextTagMarker", + "mdxJsxTextTagClosingMarker", + "mdxJsxTextTagSelfClosingMarker", + "mdxJsxTextTagName", + "mdxJsxTextTagNamePrimary", + "mdxJsxTextTagNameMemberMarker", + "mdxJsxTextTagNameMember", + "mdxJsxTextTagNamePrefixMarker", + "mdxJsxTextTagNameLocal", + "mdxJsxTextTagExpressionAttribute", + "mdxJsxTextTagExpressionAttributeMarker", + "mdxJsxTextTagExpressionAttributeValue", + "mdxJsxTextTagAttribute", + "mdxJsxTextTagAttributeName", + "mdxJsxTextTagAttributeNamePrimary", + "mdxJsxTextTagAttributeNamePrefixMarker", + "mdxJsxTextTagAttributeNameLocal", + "mdxJsxTextTagAttributeInitializerMarker", + "mdxJsxTextTagAttributeValueLiteral", + "mdxJsxTextTagAttributeValueLiteralMarker", + "mdxJsxTextTagAttributeValueLiteralValue", + "mdxJsxTextTagAttributeValueExpression", + "mdxJsxTextTagAttributeValueExpressionMarker", + "mdxJsxTextTagAttributeValueExpressionValue" + ); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/mdx/MdxSyntax.java b/libs/markdown/src/main/java/appeng/libs/mdx/MdxSyntax.java new file mode 100644 index 00000000000..d9f912f97bd --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/mdx/MdxSyntax.java @@ -0,0 +1,24 @@ +package appeng.libs.mdx; + +import appeng.libs.micromark.Extension; +import appeng.libs.micromark.symbol.Codes; + +import java.util.Collections; +import java.util.List; + +public class MdxSyntax { + + public static final Extension INSTANCE = new Extension(); + + static { + INSTANCE.flow.put(Codes.lessThan, List.of(JsxFlow.INSTANCE)); + INSTANCE.text.put(Codes.lessThan, List.of(JsxText.INSTANCE)); + + // See https://github.com/micromark/micromark-extension-mdx-md/blob/main/index.js + Collections.addAll( + INSTANCE.nullDisable, + "autolink", "codeIndented", "htmlFlow", "htmlText" + ); + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/Assert.java b/libs/markdown/src/main/java/appeng/libs/micromark/Assert.java new file mode 100644 index 00000000000..62281d27742 --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/Assert.java @@ -0,0 +1,12 @@ +package appeng.libs.micromark; + +public final class Assert { + private Assert() { + } + + public static void check(boolean condition, String message) { + if (!condition) { + throw new IllegalStateException(message); + } + } +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/CharUtil.java b/libs/markdown/src/main/java/appeng/libs/micromark/CharUtil.java new file mode 100644 index 00000000000..4ea571b899b --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/CharUtil.java @@ -0,0 +1,204 @@ +package appeng.libs.micromark; + +import appeng.libs.micromark.symbol.Codes; + +public final class CharUtil { + private CharUtil() { + } + + /** + * Check whether the character code represents an ASCII alpha (`a` through `z`, + * case insensitive). + *

+ * An **ASCII alpha** is an ASCII upper alpha or ASCII lower alpha. + *

+ * An **ASCII upper alpha** is a character in the inclusive range U+0041 (`A`) + * to U+005A (`Z`). + *

+ * An **ASCII lower alpha** is a character in the inclusive range U+0061 (`a`) + * to U+007A (`z`). + */ + public static boolean asciiAlpha(int code) { + return code >= 'a' && code <= 'z' || code >= 'A' && code <= 'Z'; + } + + /** + * Check whether the character code represents an ASCII digit (`0` through `9`). + *

+ * An **ASCII digit** is a character in the inclusive range U+0030 (`0`) to + * U+0039 (`9`). + */ + public static boolean asciiDigit(int code) { + return code >= '0' && code <= '9'; + } + + /** + * Check whether the character code represents an ASCII hex digit (`a` through + * `f`, case insensitive, or `0` through `9`). + *

+ * An **ASCII hex digit** is an ASCII digit (see `asciiDigit`), ASCII upper hex + * digit, or an ASCII lower hex digit. + *

+ * An **ASCII upper hex digit** is a character in the inclusive range U+0041 + * (`A`) to U+0046 (`F`). + *

+ * An **ASCII lower hex digit** is a character in the inclusive range U+0061 + * (`a`) to U+0066 (`f`). + */ + public static boolean asciiHexDigit(int code) { + return asciiDigit(code) || code >= 'a' && code <= 'f' || code >= 'A' && code <= 'F'; + } + + /** + * Check whether the character code represents an ASCII alphanumeric (`a` + * through `z`, case insensitive, or `0` through `9`). + *

+ * An **ASCII alphanumeric** is an ASCII digit (see `asciiDigit`) or ASCII alpha + * (see `asciiAlpha`). + */ + public static boolean asciiAlphanumeric(int code) { + return asciiDigit(code) || asciiAlpha(code); + } + + /** + * Check whether the character code represents ASCII punctuation. + *

+ * An **ASCII punctuation** is a character in the inclusive ranges U+0021 + * EXCLAMATION MARK (`!`) to U+002F SLASH (`/`), U+003A COLON (`:`) to U+0040 AT + * SIGN (`@`), U+005B LEFT SQUARE BRACKET (`[`) to U+0060 GRAVE ACCENT + * (`` ` ``), or U+007B LEFT CURLY BRACE (`{`) to U+007E TILDE (`~`). + */ + public static boolean asciiPunctuation(int code) { + return code >= '!' && code <= '/' + || code >= ':' && code <= '@' + || code >= '[' && code <= '`' + || code >= '{' && code <= '~'; + } + + /** + * Check whether the character code represents an ASCII atext. + *

+ * atext is an ASCII alphanumeric (see `asciiAlphanumeric`), or a character in + * the inclusive ranges U+0023 NUMBER SIGN (`#`) to U+0027 APOSTROPHE (`'`), + * U+002A ASTERISK (`*`), U+002B PLUS SIGN (`+`), U+002D DASH (`-`), U+002F + * SLASH (`/`), U+003D EQUALS TO (`=`), U+003F QUESTION MARK (`?`), U+005E + * CARET (`^`) to U+0060 GRAVE ACCENT (`` ` ``), or U+007B LEFT CURLY BRACE + * (`{`) to U+007E TILDE (`~`). + *

+ * See: + * **\[RFC5322]**: + * [Internet Message Format](https://tools.ietf.org/html/rfc5322). + * P. Resnick. + * IETF. + */ + public static boolean asciiAtext(int code) { + return asciiAlphanumeric(code) + || code >= '#' && code <= '\'' + || code == '*' || code == '+' || code == '-' + || code == '/' || code == '=' || code == '?' + || code >= '^' && code <= '~'; + } + + /** + * Check whether a character code is an ASCII control character. + *

+ * An **ASCII control** is a character in the inclusive range U+0000 NULL (NUL) + * to U+001F (US), or U+007F (DEL). + */ + public static boolean asciiControl(int code) { + return ( + // Special whitespace codes (which have negative values), C0 and Control + // character DEL + code != Codes.eof && (code < Codes.space || code == Codes.del) + ); + } + + /** + * Check whether a character code is a markdown line ending (see + * `markdownLineEnding`) or markdown space (see `markdownSpace`). + */ + public static boolean markdownLineEndingOrSpace(int code) { + return code != Codes.eof && (code < Codes.nul || code == Codes.space); + } + + /** + * Check whether a character code is a markdown line ending. + *

+ * A **markdown line ending** is the virtual characters M-0003 CARRIAGE RETURN + * LINE FEED (CRLF), M-0004 LINE FEED (LF) and M-0005 CARRIAGE RETURN (CR). + *

+ * In micromark, the actual character U+000A LINE FEED (LF) and U+000D CARRIAGE + * RETURN (CR) are replaced by these virtual characters depending on whether + * they occurred together. + */ + public static boolean markdownLineEnding(int code) { + return code != Codes.eof && code < Codes.horizontalTab; + } + + /** + * Check whether a character code is a markdown space. + *

+ * A **markdown space** is the concrete character U+0020 SPACE (SP) and the + * virtual characters M-0001 VIRTUAL SPACE (VS) and M-0002 HORIZONTAL TAB (HT). + *

+ * In micromark, the actual character U+0009 CHARACTER TABULATION (HT) is + * replaced by one M-0002 HORIZONTAL TAB (HT) and between 0 and 3 M-0001 VIRTUAL + * SPACE (VS) characters, depending on the column at which the tab occurred. + */ + public static boolean markdownSpace(int code) { + return ( + code == Codes.horizontalTab || + code == Codes.virtualSpace || + code == Codes.space + ); + } + + /** + * Check whether the character code represents Unicode whitespace. + *

+ * Note that this does handle micromark specific markdown whitespace characters. + * See `markdownLineEndingOrSpace` to check that. + *

+ * A **Unicode whitespace** is a character in the Unicode `Zs` (Separator, + * Space) category, or U+0009 CHARACTER TABULATION (HT), U+000A LINE FEED (LF), + * U+000C (FF), or U+000D CARRIAGE RETURN (CR) (**\[UNICODE]**). + *

+ * See: + * **\[UNICODE]**: + * [The Unicode Standard](https://www.unicode.org/versions/). + * Unicode Consortium. + */ + public static boolean unicodeWhitespace(int code) { + return Character.isSpaceChar((char) code); + } + + /** + * Check whether the character code represents Unicode punctuation. + *

+ * A **Unicode punctuation** is a character in the Unicode `Pc` (Punctuation, + * Connector), `Pd` (Punctuation, Dash), `Pe` (Punctuation, Close), `Pf` + * (Punctuation, Final quote), `Pi` (Punctuation, Initial quote), `Po` + * (Punctuation, Other), or `Ps` (Punctuation, Open) categories, or an ASCII + * punctuation (see `asciiPunctuation`). + *

+ * See: + * **\[UNICODE]**: + * [The Unicode Standard](https://www.unicode.org/versions/). + * Unicode Consortium. + */ + public static boolean unicodePunctuation(int code) { + if (asciiPunctuation(code)) { + return true; + } + + var type = Character.getType((char) code); + return type == Character.CONNECTOR_PUNCTUATION + || type == Character.DASH_PUNCTUATION + || type == Character.END_PUNCTUATION + || type == Character.FINAL_QUOTE_PUNCTUATION + || type == Character.INITIAL_QUOTE_PUNCTUATION + || type == Character.OTHER_PUNCTUATION + || type == Character.START_PUNCTUATION; + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/ClassifyCharacter.java b/libs/markdown/src/main/java/appeng/libs/micromark/ClassifyCharacter.java new file mode 100644 index 00000000000..2056a4502cd --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/ClassifyCharacter.java @@ -0,0 +1,38 @@ +package appeng.libs.micromark; + +import appeng.libs.micromark.symbol.Codes; +import appeng.libs.micromark.symbol.Constants; +import org.jetbrains.annotations.Nullable; + +import java.util.OptionalInt; + +public final class ClassifyCharacter { + private ClassifyCharacter() { + } + + /** + * Classify whether a character code represents whitespace, punctuation, or + * something else. + *

+ * Used for attention (emphasis, strong), whose sequences can open or close + * based on the class of surrounding characters. + *

+ * Note that eof (`null`) is seen as whitespace. + */ + public static int classifyCharacter(int code) { + if ( + code == Codes.eof || + CharUtil.markdownLineEndingOrSpace(code) || + CharUtil.unicodeWhitespace(code) + ) { + return Constants.characterGroupWhitespace; + } + + if (CharUtil.unicodePunctuation(code)) { + return Constants.characterGroupPunctuation; + } + + return 0; + } + +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/Construct.java b/libs/markdown/src/main/java/appeng/libs/micromark/Construct.java new file mode 100644 index 00000000000..138fcf374fd --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/Construct.java @@ -0,0 +1,175 @@ +package appeng.libs.micromark; + +import java.util.HashSet; +import java.util.List; + +/** + * An object describing how to parse a markdown construct. + */ +public class Construct { + + /** + * Guard whether the previous character can come before the construct + */ + public Previous previous; + + /** + * For containers, a continuation construct. + */ + public Construct continuation; + + /** + * For containers, a final hook. + */ + public Exiter exit; + + /** + * Name of the construct, used to toggle constructs off. + * Named constructs must not be `partial`. + */ + public String name; + + /** + * Whether this construct represents a partial construct. + * Partial constructs must not have a `name`. + */ + public boolean partial; + + /** + * Resolve the events parsed by `tokenize`. + *

+ * For example, if we’re currently parsing a link title and this construct + * parses character references, then `resolve` is called with the events + * ranging from the start to the end of a character reference each time one is + * found. + */ + public Resolver resolve; + + /** + * Resolve the events from the start of the content (which includes other + * constructs) to the last one parsed by `tokenize`. + *

+ * For example, if we’re currently parsing a link title and this construct + * parses character references, then `resolveTo` is called with the events + * ranging from the start of the link title to the end of a character + * reference each time one is found. + */ + public Resolver resolveTo; + + /** + * Resolve all events when the content is complete, from the start to the end. + * Only used if `tokenize` is successful once in the content. + *

+ * For example, if we’re currently parsing a link title and this construct + * parses character references, then `resolveAll` is called *if* at least one + * character reference is found, ranging from the start to the end of the link + * title to the end. + */ + public Resolver resolveAll; + + /** + * Concrete constructs cannot be interrupted by more containers. + *

+ * For example, when parsing the document (containers, such as block quotes + * and lists) and this construct is parsing fenced code: + *

+ *

+     *  > ```js
+     *  > - list?
+     *  
+ * …then `- list?` cannot form if this fenced code construct is concrete. + *

+ * An example of a construct that is not concrete is a GFM table: + *

+ *

+     *  | a |
+     *  | - |
+     *  > | b |
+     *  
+ *

+ * …`b` is not part of the table. + */ + public boolean concrete; + + /** + * Whether the construct, when in a `ConstructRecord`, precedes over existing + * constructs for the same character code when merged + * The default is that new constructs precede over existing ones. + */ + public ConstructPrecedence add = ConstructPrecedence.BEFORE; + + /** + * A resolver handles and cleans events coming from `tokenize`. + */ + @FunctionalInterface + public interface Resolver { + /** + * @param events List of events. + * @param context Context. + */ + List resolve(List events, TokenizeContext context); + } + + /** + * Like a tokenizer, but without `ok` or `nok`, and returning void. + * This is the final hook when a container must be closed. + */ + @FunctionalInterface + public interface Exiter { + void exit(TokenizeContext context, Tokenizer.Effects effects); + } + + /** + * Guard whether `code` can come before the construct. + * In certain cases a construct can hook into many potential start characters. + * Instead of setting up an attempt to parse that construct for most + * characters, this is a speedy way to reduce that. + */ + @FunctionalInterface + public interface Previous { + boolean previous(TokenizeContext context, int code); + } + + /** + * Call all `resolveAll`s. + */ + public static List resolveAll(List constructs, + List events, + TokenizeContext context) { + var called = new HashSet(); + + for (var construct : constructs) { + var resolver = construct.resolveAll; + if (resolver != null && called.add(resolver)) { + events = resolver.resolve(events, context); + } + } + + return events; + } + + /** + * Call all `resolveAll`s. + */ + public static List resolveAll(Iterable resolvers, + List events, + TokenizeContext context) { + var called = new HashSet(); + + for (var resolver : resolvers) { + if (resolver != null && called.add(resolver)) { + events = resolver.resolve(events, context); + } + } + + return events; + } + + @FunctionalInterface + public interface TokenizerFunction { + State tokenize(TokenizeContext context, Tokenizer.Effects effects, State ok, State nok); + } + + public TokenizerFunction tokenize; + +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/ConstructPrecedence.java b/libs/markdown/src/main/java/appeng/libs/micromark/ConstructPrecedence.java new file mode 100644 index 00000000000..13f69099abb --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/ConstructPrecedence.java @@ -0,0 +1,6 @@ +package appeng.libs.micromark; + +public enum ConstructPrecedence { + BEFORE, + AFTER +} diff --git a/libs/markdown/src/main/java/appeng/libs/micromark/ContentType.java b/libs/markdown/src/main/java/appeng/libs/micromark/ContentType.java new file mode 100644 index 00000000000..31db94a2aae --- /dev/null +++ b/libs/markdown/src/main/java/appeng/libs/micromark/ContentType.java @@ -0,0 +1,58 @@ +package appeng.libs.micromark; + +/** + * Enumeration of the content types of {@link Token}. + */ +public enum ContentType { + /** + * Technically `document` is also a content type, which includes containers + * (lists, block quotes) and flow. + * As `ContentType` is used on tokens to define the type of subcontent but + * `document` is the highest level of content, so it’s not listed here. + *

+ * Containers in markdown come from the margin and include more constructs + * on the lines that define them. + * Take for example a block quote with a paragraph inside it (such as + * `> asd`). + */ + DOCUMENT, + /** + * `flow` represents the sections, such as headings, code, and content, which + * is also parsed per line + * An example is HTML, which has a certain starting condition (such as + * `1. *bar*||1. *bar*||should support raw tags w/ more data on ending line", + "</script^nmore

||should not support a raw closing tag", + "||||should support blank lines in raw", + "> ", + "okay" + ), + List.of( + "", + "

okay

" + ) + ); + } + @Test + public void testShouldSupportRawStyleTags() { + TestUtil.assertGeneratedHtmlLinesUnsafe( + List.of( + "", + "h1 {color:red;}", + "", + "p {color:blue;}", + "", + "okay" + ), + List.of( + "", + "h1 {color:red;}", + "", + "p {color:blue;}", + "", + "

okay

" + ) + ); + } + } + + @Nested + public class CommentTest { + @ParameterizedTest(name = "[{index}] {2}") + @CsvSource(delimiterString = "||", ignoreLeadingAndTrailingWhitespace = false, value = { + "^nokay||^n

okay

||should support comments (type 2)", + "*bar*^n*baz*||*bar*^n

baz

||should support comments w/ start and end on a single line", + "||

<!-asd-->

||should not support a single dash to start comments", + "||||should support comments where the start dashes are the end dashes (1)", + "||||should support comments where the start dashes are the end dashes (2)", + "||||should support empty comments", + // If the `\"` is encoded, we’re in text. If it remains, we’re in HTML. + "^n\"||^n

"

||should end a comment at two dashes (`-->`)", + "^n\"||^n

"

||should end a comment at three dashes (`--->`)", + "^n\"||^n

"

||should end a comment at four dashes (`---->`)", + " || ||should support comments w/ indent", + " ||
<!-- foo -->^n
||should not support comments w/ a 4 character indent", + // Extra. + "Foo^n||||should support blank lines in comments", + "> ||

foo

||should support comments", + "foo ||

foo <!-- not a comment -- two hyphens -->

||should not support comments w/ two dashes inside", + "foo foo -->||

foo <!--> foo -->

||should not support nonconforming comments (1)", + "foo ||

foo <!-- foo--->

||should not support nonconforming comments (2)", + "foo ||

foo

||should support instructions", + "foo ||

foo

||should support declarations", + "foo &<]]>||

foo &<]]>

||should support cdata", + "foo ||

foo

||should support (ignore) character references", + "foo ||

foo

||should not support character escapes (1)", + "||

<a href=""">

||should not support character escapes (2)", + // Extra: + "foo ||

foo <!1>

||should not support non-comment, non-cdata, and non-named declaration", + "foo ||

foo <!-not enough!-->

||should not support comments w/ not enough dashes", + "foo ||

foo

||should support comments that start w/ a dash, if it’s not followed by a greater than", + "foo ||

foo <!--->

||should not support comments that start w/ `->`", + "foo ||

foo

||should support `->` in a comment", + "foo ", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 22 + }, + "end": { + "line": 5, + "column": 9, + "offset": 30 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 32 + }, + "end": { + "line": 7, + "column": 6, + "offset": 37 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 39 + }, + "end": { + "line": 9, + "column": 5, + "offset": 43 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 45 + }, + "end": { + "line": 11, + "column": 14, + "offset": 58 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 16, + "column": 1, + "offset": 68 + }, + "end": { + "line": 16, + "column": 4, + "offset": 71 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 17, + "column": 1, + "offset": 72 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-flow.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-flow.md new file mode 100644 index 00000000000..d33624e8a47 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-flow.md @@ -0,0 +1,16 @@ + + + + + + + + + + +
diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.json new file mode 100644 index 00000000000..c5e75d11ce5 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.json @@ -0,0 +1,375 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 3, + "offset": 2 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 1, + "column": 3, + "offset": 2 + }, + "end": { + "line": 1, + "column": 6, + "offset": 5 + } + } + }, + { + "type": "text", + "value": "c", + "position": { + "start": { + "line": 1, + "column": 6, + "offset": 5 + }, + "end": { + "line": 1, + "column": 7, + "offset": 6 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 1, + "column": 7, + "offset": 6 + }, + "end": { + "line": 1, + "column": 11, + "offset": 10 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 1, + "column": 11, + "offset": 10 + }, + "end": { + "line": 1, + "column": 13, + "offset": 12 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 13, + "offset": 12 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "e ", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 14 + }, + "end": { + "line": 3, + "column": 3, + "offset": 16 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 3, + "column": 3, + "offset": 16 + }, + "end": { + "line": 3, + "column": 11, + "offset": 24 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 14 + }, + "end": { + "line": 3, + "column": 11, + "offset": 24 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "g ", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 26 + }, + "end": { + "line": 5, + "column": 3, + "offset": 28 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 5, + "column": 3, + "offset": 28 + }, + "end": { + "line": 5, + "column": 8, + "offset": 33 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 26 + }, + "end": { + "line": 5, + "column": 8, + "offset": 33 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "i ", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 35 + }, + "end": { + "line": 7, + "column": 3, + "offset": 37 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 7, + "column": 3, + "offset": 37 + }, + "end": { + "line": 7, + "column": 7, + "offset": 41 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 35 + }, + "end": { + "line": 7, + "column": 7, + "offset": 41 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "k ", + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 43 + }, + "end": { + "line": 9, + "column": 3, + "offset": 45 + } + } + }, + { + "type": "html", + "value": "", + "position": { + "start": { + "line": 9, + "column": 3, + "offset": 45 + }, + "end": { + "line": 9, + "column": 16, + "offset": 58 + } + } + } + ], + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 43 + }, + "end": { + "line": 9, + "column": 16, + "offset": 58 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "m ", + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 60 + }, + "end": { + "line": 11, + "column": 3, + "offset": 62 + } + } + }, + { + "type": "html", + "value": "
", + "position": { + "start": { + "line": 11, + "column": 3, + "offset": 62 + }, + "end": { + "line": 11, + "column": 8, + "offset": 67 + } + } + }, + { + "type": "text", + "value": " n", + "position": { + "start": { + "line": 11, + "column": 8, + "offset": 67 + }, + "end": { + "line": 11, + "column": 10, + "offset": 69 + } + } + } + ], + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 60 + }, + "end": { + "line": 11, + "column": 10, + "offset": 69 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 12, + "column": 1, + "offset": 70 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.md new file mode 100644 index 00000000000..ce8f12a079a --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/html-text.md @@ -0,0 +1,11 @@ +a c d + +e + +g + +i + +k + +m
n diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.json new file mode 100644 index 00000000000..1aecbe39287 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.json @@ -0,0 +1,229 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Not references, as they’re not defined ![a], ![b][], ![c][d].", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 62, + "offset": 61 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 62, + "offset": 61 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "References! ", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 63 + }, + "end": { + "line": 3, + "column": 13, + "offset": 75 + } + } + }, + { + "type": "imageReference", + "alt": "e", + "position": { + "start": { + "line": 3, + "column": 13, + "offset": 75 + }, + "end": { + "line": 3, + "column": 17, + "offset": 79 + } + }, + "label": "e", + "identifier": "e", + "referenceType": "shortcut" + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 3, + "column": 17, + "offset": 79 + }, + "end": { + "line": 3, + "column": 19, + "offset": 81 + } + } + }, + { + "type": "imageReference", + "alt": "f", + "position": { + "start": { + "line": 3, + "column": 19, + "offset": 81 + }, + "end": { + "line": 3, + "column": 25, + "offset": 87 + } + }, + "label": "f", + "identifier": "f", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 3, + "column": 25, + "offset": 87 + }, + "end": { + "line": 3, + "column": 27, + "offset": 89 + } + } + }, + { + "type": "imageReference", + "alt": "g", + "position": { + "start": { + "line": 3, + "column": 27, + "offset": 89 + }, + "end": { + "line": 3, + "column": 34, + "offset": 96 + } + }, + "label": "h", + "identifier": "h", + "referenceType": "full" + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 63 + }, + "end": { + "line": 3, + "column": 34, + "offset": 96 + } + } + }, + { + "type": "definition", + "identifier": "e", + "label": "e", + "title": null, + "url": "x", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 98 + }, + "end": { + "line": 5, + "column": 7, + "offset": 104 + } + } + }, + { + "type": "definition", + "identifier": "f", + "label": "f", + "title": null, + "url": "y", + "position": { + "start": { + "line": 6, + "column": 1, + "offset": 105 + }, + "end": { + "line": 6, + "column": 7, + "offset": 111 + } + } + }, + { + "type": "definition", + "identifier": "h", + "label": "h", + "title": null, + "url": "z", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 112 + }, + "end": { + "line": 7, + "column": 7, + "offset": 118 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 8, + "column": 1, + "offset": 119 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.md new file mode 100644 index 00000000000..4260dc00cda --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-reference.md @@ -0,0 +1,7 @@ +Not references, as they’re not defined ![a], ![b][], ![c][d]. + +References! ![e], ![f][], ![g][h] + +[e]: x +[f]: y +[h]: z diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.json new file mode 100644 index 00000000000..ffbb7b675bc --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.json @@ -0,0 +1,553 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 3, + "offset": 2 + } + } + }, + { + "type": "image", + "title": null, + "url": "c", + "alt": "\nb", + "position": { + "start": { + "line": 1, + "column": 3, + "offset": 2 + }, + "end": { + "line": 2, + "column": 6, + "offset": 10 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 2, + "column": 6, + "offset": 10 + }, + "end": { + "line": 2, + "column": 8, + "offset": 12 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 2, + "column": 8, + "offset": 12 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 4, + "column": 1, + "offset": 14 + }, + "end": { + "line": 4, + "column": 3, + "offset": 16 + } + } + }, + { + "type": "image", + "title": null, + "url": "c", + "alt": "b\n", + "position": { + "start": { + "line": 4, + "column": 3, + "offset": 16 + }, + "end": { + "line": 5, + "column": 5, + "offset": 24 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 5, + "column": 5, + "offset": 24 + }, + "end": { + "line": 5, + "column": 7, + "offset": 26 + } + } + } + ], + "position": { + "start": { + "line": 4, + "column": 1, + "offset": 14 + }, + "end": { + "line": 5, + "column": 7, + "offset": 26 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 28 + }, + "end": { + "line": 7, + "column": 3, + "offset": 30 + } + } + }, + { + "type": "image", + "title": null, + "url": "d", + "alt": "b\nc", + "position": { + "start": { + "line": 7, + "column": 3, + "offset": 30 + }, + "end": { + "line": 8, + "column": 6, + "offset": 39 + } + } + }, + { + "type": "text", + "value": " e", + "position": { + "start": { + "line": 8, + "column": 6, + "offset": 39 + }, + "end": { + "line": 8, + "column": 8, + "offset": 41 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 28 + }, + "end": { + "line": 8, + "column": 8, + "offset": 41 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 10, + "column": 1, + "offset": 43 + }, + "end": { + "line": 10, + "column": 3, + "offset": 45 + } + } + }, + { + "type": "image", + "title": null, + "url": "c", + "alt": "b", + "position": { + "start": { + "line": 10, + "column": 3, + "offset": 45 + }, + "end": { + "line": 11, + "column": 3, + "offset": 53 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 11, + "column": 3, + "offset": 53 + }, + "end": { + "line": 11, + "column": 5, + "offset": 55 + } + } + } + ], + "position": { + "start": { + "line": 10, + "column": 1, + "offset": 43 + }, + "end": { + "line": 11, + "column": 5, + "offset": 55 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 57 + }, + "end": { + "line": 13, + "column": 3, + "offset": 59 + } + } + }, + { + "type": "image", + "title": null, + "url": "c", + "alt": "b", + "position": { + "start": { + "line": 13, + "column": 3, + "offset": 59 + }, + "end": { + "line": 14, + "column": 2, + "offset": 67 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 14, + "column": 2, + "offset": 67 + }, + "end": { + "line": 14, + "column": 4, + "offset": 69 + } + } + } + ], + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 57 + }, + "end": { + "line": 14, + "column": 4, + "offset": 69 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 16, + "column": 1, + "offset": 71 + }, + "end": { + "line": 16, + "column": 3, + "offset": 73 + } + } + }, + { + "type": "image", + "title": "d", + "url": "c", + "alt": "b", + "position": { + "start": { + "line": 16, + "column": 3, + "offset": 73 + }, + "end": { + "line": 17, + "column": 5, + "offset": 84 + } + } + }, + { + "type": "text", + "value": " e", + "position": { + "start": { + "line": 17, + "column": 5, + "offset": 84 + }, + "end": { + "line": 17, + "column": 7, + "offset": 86 + } + } + } + ], + "position": { + "start": { + "line": 16, + "column": 1, + "offset": 71 + }, + "end": { + "line": 17, + "column": 7, + "offset": 86 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 88 + }, + "end": { + "line": 19, + "column": 3, + "offset": 90 + } + } + }, + { + "type": "image", + "title": "d\ne", + "url": "c", + "alt": "b", + "position": { + "start": { + "line": 19, + "column": 3, + "offset": 90 + }, + "end": { + "line": 20, + "column": 4, + "offset": 103 + } + } + }, + { + "type": "text", + "value": " f", + "position": { + "start": { + "line": 20, + "column": 4, + "offset": 103 + }, + "end": { + "line": 20, + "column": 6, + "offset": 105 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 88 + }, + "end": { + "line": 20, + "column": 6, + "offset": 105 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 22, + "column": 1, + "offset": 107 + }, + "end": { + "line": 22, + "column": 3, + "offset": 109 + } + } + }, + { + "type": "image", + "title": "d\ne\nf", + "url": "c", + "alt": "b", + "position": { + "start": { + "line": 22, + "column": 3, + "offset": 109 + }, + "end": { + "line": 24, + "column": 4, + "offset": 124 + } + } + }, + { + "type": "text", + "value": " g", + "position": { + "start": { + "line": 24, + "column": 4, + "offset": 124 + }, + "end": { + "line": 24, + "column": 6, + "offset": 126 + } + } + } + ], + "position": { + "start": { + "line": 22, + "column": 1, + "offset": 107 + }, + "end": { + "line": 24, + "column": 6, + "offset": 126 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 25, + "column": 1, + "offset": 127 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.md new file mode 100644 index 00000000000..d9acc485c3b --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource-eol.md @@ -0,0 +1,24 @@ +a ![ +b](c) d + +a ![b +](c) d + +a ![b +c](d) e + +a ![b]( +c) d + +a ![b](c +) d + +a ![b](c +"d") e + +a ![b](c "d +e") f + +a ![b](c "d +e +f") g diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.json new file mode 100644 index 00000000000..825a1c31fcb --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.json @@ -0,0 +1,405 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Resources: ", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 12, + "offset": 11 + } + } + }, + { + "type": "image", + "title": null, + "url": "b", + "alt": "a", + "position": { + "start": { + "line": 1, + "column": 12, + "offset": 11 + }, + "end": { + "line": 1, + "column": 19, + "offset": 18 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 19, + "offset": 18 + }, + "end": { + "line": 1, + "column": 21, + "offset": 20 + } + } + }, + { + "type": "image", + "title": "e", + "url": "d", + "alt": "c", + "position": { + "start": { + "line": 1, + "column": 21, + "offset": 20 + }, + "end": { + "line": 1, + "column": 32, + "offset": 31 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 32, + "offset": 31 + }, + "end": { + "line": 1, + "column": 34, + "offset": 33 + } + } + }, + { + "type": "image", + "title": "h", + "url": "g", + "alt": "f", + "position": { + "start": { + "line": 1, + "column": 34, + "offset": 33 + }, + "end": { + "line": 1, + "column": 45, + "offset": 44 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 45, + "offset": 44 + }, + "end": { + "line": 1, + "column": 47, + "offset": 46 + } + } + }, + { + "type": "image", + "title": "k", + "url": "j", + "alt": "i", + "position": { + "start": { + "line": 1, + "column": 47, + "offset": 46 + }, + "end": { + "line": 1, + "column": 58, + "offset": 57 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 58, + "offset": 57 + }, + "end": { + "line": 1, + "column": 60, + "offset": 59 + } + } + }, + { + "type": "image", + "title": null, + "url": "m", + "alt": "l", + "position": { + "start": { + "line": 1, + "column": 60, + "offset": 59 + }, + "end": { + "line": 1, + "column": 69, + "offset": 68 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 1, + "column": 69, + "offset": 68 + }, + "end": { + "line": 1, + "column": 70, + "offset": 69 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 70, + "offset": 69 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "“Content” in images: ", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 71 + }, + "end": { + "line": 3, + "column": 22, + "offset": 92 + } + } + }, + { + "type": "image", + "title": null, + "url": "d", + "alt": "a b c", + "position": { + "start": { + "line": 3, + "column": 22, + "offset": 92 + }, + "end": { + "line": 3, + "column": 37, + "offset": 107 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 3, + "column": 37, + "offset": 107 + }, + "end": { + "line": 3, + "column": 38, + "offset": 108 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 71 + }, + "end": { + "line": 3, + "column": 38, + "offset": 108 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Empty: ", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 110 + }, + "end": { + "line": 5, + "column": 8, + "offset": 117 + } + } + }, + { + "type": "image", + "title": null, + "url": "", + "alt": "", + "position": { + "start": { + "line": 5, + "column": 8, + "offset": 117 + }, + "end": { + "line": 5, + "column": 13, + "offset": 122 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 5, + "column": 13, + "offset": 122 + }, + "end": { + "line": 5, + "column": 14, + "offset": 123 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 110 + }, + "end": { + "line": 5, + "column": 14, + "offset": 123 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Character references and escapes:\n", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 125 + }, + "end": { + "line": 8, + "column": 1, + "offset": 159 + } + } + }, + { + "type": "image", + "title": "i*j\tk&l", + "url": "e*f\tg&h", + "alt": "a*b\tc&d", + "position": { + "start": { + "line": 8, + "column": 1, + "offset": 159 + }, + "end": { + "line": 8, + "column": 54, + "offset": 212 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 125 + }, + "end": { + "line": 8, + "column": 54, + "offset": 212 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 8, + "column": 54, + "offset": 212 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.md new file mode 100644 index 00000000000..8442217ca76 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/image-resource.md @@ -0,0 +1,8 @@ +Resources: ![a](b), ![c](d "e"), ![f](g 'h'), ![i](j (k)), ![l](). + +“Content” in images: ![a *b* `c`](d). + +Empty: ![](). + +Character references and escapes: +![a\*b c&d](e\*f g&h "i\*j k&l") \ No newline at end of file diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.json new file mode 100644 index 00000000000..bc3a1c6da7b --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.json @@ -0,0 +1,563 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "linkReference", + "children": [ + { + "type": "inlineCode", + "value": "f", + "position": { + "start": { + "line": 1, + "column": 2, + "offset": 1 + }, + "end": { + "line": 1, + "column": 5, + "offset": 4 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 8, + "offset": 7 + } + }, + "identifier": "`f`", + "label": "`f`", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 1, + "column": 8, + "offset": 7 + }, + "end": { + "line": 2, + "column": 1, + "offset": 8 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 2, + "column": 2, + "offset": 9 + }, + "end": { + "line": 2, + "column": 7, + "offset": 14 + } + } + } + ], + "position": { + "start": { + "line": 2, + "column": 1, + "offset": 8 + }, + "end": { + "line": 2, + "column": 11, + "offset": 18 + } + }, + "identifier": ";", + "label": ";", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 2, + "column": 11, + "offset": 18 + }, + "end": { + "line": 3, + "column": 1, + "offset": 19 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 3, + "column": 2, + "offset": 20 + }, + "end": { + "line": 3, + "column": 4, + "offset": 22 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 19 + }, + "end": { + "line": 3, + "column": 7, + "offset": 25 + } + }, + "identifier": "\\;", + "label": ";", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 3, + "column": 7, + "offset": 25 + }, + "end": { + "line": 4, + "column": 1, + "offset": 26 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 4, + "column": 2, + "offset": 27 + }, + "end": { + "line": 4, + "column": 3, + "offset": 28 + } + } + } + ], + "position": { + "start": { + "line": 4, + "column": 1, + "offset": 26 + }, + "end": { + "line": 4, + "column": 6, + "offset": 31 + } + }, + "identifier": ";", + "label": ";", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 4, + "column": 6, + "offset": 31 + }, + "end": { + "line": 5, + "column": 1, + "offset": 32 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "inlineCode", + "value": "f", + "position": { + "start": { + "line": 5, + "column": 2, + "offset": 33 + }, + "end": { + "line": 5, + "column": 5, + "offset": 36 + } + } + }, + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 5, + "column": 5, + "offset": 36 + }, + "end": { + "line": 5, + "column": 10, + "offset": 41 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 32 + }, + "end": { + "line": 5, + "column": 14, + "offset": 45 + } + }, + "identifier": "`f`;", + "label": "`f`;", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 5, + "column": 14, + "offset": 45 + }, + "end": { + "line": 6, + "column": 1, + "offset": 46 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "inlineCode", + "value": "f", + "position": { + "start": { + "line": 6, + "column": 2, + "offset": 47 + }, + "end": { + "line": 6, + "column": 5, + "offset": 50 + } + } + }, + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 6, + "column": 5, + "offset": 50 + }, + "end": { + "line": 6, + "column": 7, + "offset": 52 + } + } + } + ], + "position": { + "start": { + "line": 6, + "column": 1, + "offset": 46 + }, + "end": { + "line": 6, + "column": 10, + "offset": 55 + } + }, + "identifier": "`f`\\;", + "label": "`f`;", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": "\n", + "position": { + "start": { + "line": 6, + "column": 10, + "offset": 55 + }, + "end": { + "line": 7, + "column": 1, + "offset": 56 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "inlineCode", + "value": "f", + "position": { + "start": { + "line": 7, + "column": 2, + "offset": 57 + }, + "end": { + "line": 7, + "column": 5, + "offset": 60 + } + } + }, + { + "type": "text", + "value": ";", + "position": { + "start": { + "line": 7, + "column": 5, + "offset": 60 + }, + "end": { + "line": 7, + "column": 6, + "offset": 61 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 56 + }, + "end": { + "line": 7, + "column": 9, + "offset": 64 + } + }, + "identifier": "`f`;", + "label": "`f`;", + "referenceType": "collapsed" + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 7, + "column": 9, + "offset": 64 + } + } + }, + { + "type": "definition", + "identifier": "`f`", + "label": "`f`", + "title": null, + "url": "alpha", + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 66 + }, + "end": { + "line": 9, + "column": 13, + "offset": 78 + } + } + }, + { + "type": "definition", + "identifier": ";", + "label": ";", + "title": null, + "url": "bravo", + "position": { + "start": { + "line": 10, + "column": 1, + "offset": 79 + }, + "end": { + "line": 10, + "column": 16, + "offset": 94 + } + } + }, + { + "type": "definition", + "identifier": "\\;", + "label": ";", + "title": null, + "url": "charlie", + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 95 + }, + "end": { + "line": 11, + "column": 14, + "offset": 108 + } + } + }, + { + "type": "definition", + "identifier": ";", + "label": ";", + "title": null, + "url": "delta", + "position": { + "start": { + "line": 12, + "column": 1, + "offset": 109 + }, + "end": { + "line": 12, + "column": 11, + "offset": 119 + } + } + }, + { + "type": "definition", + "identifier": "`f`;", + "label": "`f`;", + "title": null, + "url": "echo", + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 120 + }, + "end": { + "line": 13, + "column": 18, + "offset": 137 + } + } + }, + { + "type": "definition", + "identifier": "`f`\\;", + "label": "`f`;", + "title": null, + "url": "foxtrot", + "position": { + "start": { + "line": 14, + "column": 1, + "offset": 138 + }, + "end": { + "line": 14, + "column": 17, + "offset": 154 + } + } + }, + { + "type": "definition", + "identifier": "`f`;", + "label": "`f`;", + "title": null, + "url": "golf", + "position": { + "start": { + "line": 15, + "column": 1, + "offset": 155 + }, + "end": { + "line": 15, + "column": 13, + "offset": 167 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 16, + "column": 1, + "offset": 168 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.md new file mode 100644 index 00000000000..1cd827aaa0f --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference-with-phrasing.md @@ -0,0 +1,15 @@ +[`f`][] +[;][] +[\;][] +[;][] +[`f`;][] +[`f`\;][] +[`f`;][] + +[`f`]: alpha +[;]: bravo +[\;]: charlie +[;]: delta +[`f`;]: echo +[`f`\;]: foxtrot +[`f`;]: golf diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.json new file mode 100644 index 00000000000..4c76edce3fa --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.json @@ -0,0 +1,280 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Not references, as they’re not defined [a], [b][], [c][d].", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 59, + "offset": 58 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 59, + "offset": 58 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "References! ", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 60 + }, + "end": { + "line": 3, + "column": 13, + "offset": 72 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": "e", + "position": { + "start": { + "line": 3, + "column": 14, + "offset": 73 + }, + "end": { + "line": 3, + "column": 15, + "offset": 74 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 13, + "offset": 72 + }, + "end": { + "line": 3, + "column": 16, + "offset": 75 + } + }, + "label": "e", + "identifier": "e", + "referenceType": "shortcut" + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 3, + "column": 16, + "offset": 75 + }, + "end": { + "line": 3, + "column": 18, + "offset": 77 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": "f", + "position": { + "start": { + "line": 3, + "column": 19, + "offset": 78 + }, + "end": { + "line": 3, + "column": 20, + "offset": 79 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 18, + "offset": 77 + }, + "end": { + "line": 3, + "column": 23, + "offset": 82 + } + }, + "label": "f", + "identifier": "f", + "referenceType": "collapsed" + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 3, + "column": 23, + "offset": 82 + }, + "end": { + "line": 3, + "column": 25, + "offset": 84 + } + } + }, + { + "type": "linkReference", + "children": [ + { + "type": "text", + "value": "g", + "position": { + "start": { + "line": 3, + "column": 26, + "offset": 85 + }, + "end": { + "line": 3, + "column": 27, + "offset": 86 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 25, + "offset": 84 + }, + "end": { + "line": 3, + "column": 31, + "offset": 90 + } + }, + "label": "h", + "identifier": "h", + "referenceType": "full" + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 60 + }, + "end": { + "line": 3, + "column": 31, + "offset": 90 + } + } + }, + { + "type": "definition", + "identifier": "e", + "label": "e", + "title": null, + "url": "x", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 92 + }, + "end": { + "line": 5, + "column": 7, + "offset": 98 + } + } + }, + { + "type": "definition", + "identifier": "f", + "label": "f", + "title": null, + "url": "y", + "position": { + "start": { + "line": 6, + "column": 1, + "offset": 99 + }, + "end": { + "line": 6, + "column": 7, + "offset": 105 + } + } + }, + { + "type": "definition", + "identifier": "h", + "label": "h", + "title": null, + "url": "z", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 106 + }, + "end": { + "line": 7, + "column": 7, + "offset": 112 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 8, + "column": 1, + "offset": 113 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.md new file mode 100644 index 00000000000..2cf27f2e61a --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-reference.md @@ -0,0 +1,7 @@ +Not references, as they’re not defined [a], [b][], [c][d]. + +References! [e], [f][], [g][h] + +[e]: x +[f]: y +[h]: z diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.json new file mode 100644 index 00000000000..ea6dcd84720 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.json @@ -0,0 +1,689 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 3, + "offset": 2 + } + } + }, + { + "type": "link", + "title": null, + "url": "c", + "children": [ + { + "type": "text", + "value": "\nb", + "position": { + "start": { + "line": 1, + "column": 4, + "offset": 3 + }, + "end": { + "line": 2, + "column": 2, + "offset": 5 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 3, + "offset": 2 + }, + "end": { + "line": 2, + "column": 6, + "offset": 9 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 2, + "column": 6, + "offset": 9 + }, + "end": { + "line": 2, + "column": 8, + "offset": 11 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 2, + "column": 8, + "offset": 11 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 4, + "column": 1, + "offset": 13 + }, + "end": { + "line": 4, + "column": 3, + "offset": 15 + } + } + }, + { + "type": "link", + "title": null, + "url": "c", + "children": [ + { + "type": "text", + "value": "b\n", + "position": { + "start": { + "line": 4, + "column": 4, + "offset": 16 + }, + "end": { + "line": 5, + "column": 1, + "offset": 18 + } + } + } + ], + "position": { + "start": { + "line": 4, + "column": 3, + "offset": 15 + }, + "end": { + "line": 5, + "column": 5, + "offset": 22 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 5, + "column": 5, + "offset": 22 + }, + "end": { + "line": 5, + "column": 7, + "offset": 24 + } + } + } + ], + "position": { + "start": { + "line": 4, + "column": 1, + "offset": 13 + }, + "end": { + "line": 5, + "column": 7, + "offset": 24 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 26 + }, + "end": { + "line": 7, + "column": 3, + "offset": 28 + } + } + }, + { + "type": "link", + "title": null, + "url": "d", + "children": [ + { + "type": "text", + "value": "b\nc", + "position": { + "start": { + "line": 7, + "column": 4, + "offset": 29 + }, + "end": { + "line": 8, + "column": 2, + "offset": 32 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 3, + "offset": 28 + }, + "end": { + "line": 8, + "column": 6, + "offset": 36 + } + } + }, + { + "type": "text", + "value": " e", + "position": { + "start": { + "line": 8, + "column": 6, + "offset": 36 + }, + "end": { + "line": 8, + "column": 8, + "offset": 38 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 26 + }, + "end": { + "line": 8, + "column": 8, + "offset": 38 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 10, + "column": 1, + "offset": 40 + }, + "end": { + "line": 10, + "column": 3, + "offset": 42 + } + } + }, + { + "type": "link", + "title": null, + "url": "c", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 10, + "column": 4, + "offset": 43 + }, + "end": { + "line": 10, + "column": 5, + "offset": 44 + } + } + } + ], + "position": { + "start": { + "line": 10, + "column": 3, + "offset": 42 + }, + "end": { + "line": 11, + "column": 3, + "offset": 49 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 11, + "column": 3, + "offset": 49 + }, + "end": { + "line": 11, + "column": 5, + "offset": 51 + } + } + } + ], + "position": { + "start": { + "line": 10, + "column": 1, + "offset": 40 + }, + "end": { + "line": 11, + "column": 5, + "offset": 51 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 53 + }, + "end": { + "line": 13, + "column": 3, + "offset": 55 + } + } + }, + { + "type": "link", + "title": null, + "url": "c", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 13, + "column": 4, + "offset": 56 + }, + "end": { + "line": 13, + "column": 5, + "offset": 57 + } + } + } + ], + "position": { + "start": { + "line": 13, + "column": 3, + "offset": 55 + }, + "end": { + "line": 14, + "column": 2, + "offset": 62 + } + } + }, + { + "type": "text", + "value": " d", + "position": { + "start": { + "line": 14, + "column": 2, + "offset": 62 + }, + "end": { + "line": 14, + "column": 4, + "offset": 64 + } + } + } + ], + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 53 + }, + "end": { + "line": 14, + "column": 4, + "offset": 64 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 16, + "column": 1, + "offset": 66 + }, + "end": { + "line": 16, + "column": 3, + "offset": 68 + } + } + }, + { + "type": "link", + "title": "d", + "url": "c", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 16, + "column": 4, + "offset": 69 + }, + "end": { + "line": 16, + "column": 5, + "offset": 70 + } + } + } + ], + "position": { + "start": { + "line": 16, + "column": 3, + "offset": 68 + }, + "end": { + "line": 17, + "column": 5, + "offset": 78 + } + } + }, + { + "type": "text", + "value": " e", + "position": { + "start": { + "line": 17, + "column": 5, + "offset": 78 + }, + "end": { + "line": 17, + "column": 7, + "offset": 80 + } + } + } + ], + "position": { + "start": { + "line": 16, + "column": 1, + "offset": 66 + }, + "end": { + "line": 17, + "column": 7, + "offset": 80 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 82 + }, + "end": { + "line": 19, + "column": 3, + "offset": 84 + } + } + }, + { + "type": "link", + "title": "d\ne", + "url": "c", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 19, + "column": 4, + "offset": 85 + }, + "end": { + "line": 19, + "column": 5, + "offset": 86 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 3, + "offset": 84 + }, + "end": { + "line": 20, + "column": 4, + "offset": 96 + } + } + }, + { + "type": "text", + "value": " f", + "position": { + "start": { + "line": 20, + "column": 4, + "offset": 96 + }, + "end": { + "line": 20, + "column": 6, + "offset": 98 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 82 + }, + "end": { + "line": 20, + "column": 6, + "offset": 98 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 22, + "column": 1, + "offset": 100 + }, + "end": { + "line": 22, + "column": 3, + "offset": 102 + } + } + }, + { + "type": "link", + "title": "d\ne\nf", + "url": "c", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 22, + "column": 4, + "offset": 103 + }, + "end": { + "line": 22, + "column": 5, + "offset": 104 + } + } + } + ], + "position": { + "start": { + "line": 22, + "column": 3, + "offset": 102 + }, + "end": { + "line": 24, + "column": 4, + "offset": 116 + } + } + }, + { + "type": "text", + "value": " g", + "position": { + "start": { + "line": 24, + "column": 4, + "offset": 116 + }, + "end": { + "line": 24, + "column": 6, + "offset": 118 + } + } + } + ], + "position": { + "start": { + "line": 22, + "column": 1, + "offset": 100 + }, + "end": { + "line": 24, + "column": 6, + "offset": 118 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 25, + "column": 1, + "offset": 119 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.md new file mode 100644 index 00000000000..2911374904a --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource-eol.md @@ -0,0 +1,24 @@ +a [ +b](c) d + +a [b +](c) d + +a [b +c](d) e + +a [b]( +c) d + +a [b](c +) d + +a [b](c +"d") e + +a [b](c "d +e") f + +a [b](c "d +e +f") g diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.json new file mode 100644 index 00000000000..0a3549b5e0b --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.json @@ -0,0 +1,589 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Resources: ", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 12, + "offset": 11 + } + } + }, + { + "type": "link", + "title": null, + "url": "b", + "children": [ + { + "type": "text", + "value": "a", + "position": { + "start": { + "line": 1, + "column": 13, + "offset": 12 + }, + "end": { + "line": 1, + "column": 14, + "offset": 13 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 12, + "offset": 11 + }, + "end": { + "line": 1, + "column": 18, + "offset": 17 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 18, + "offset": 17 + }, + "end": { + "line": 1, + "column": 20, + "offset": 19 + } + } + }, + { + "type": "link", + "title": "e", + "url": "d", + "children": [ + { + "type": "text", + "value": "c", + "position": { + "start": { + "line": 1, + "column": 21, + "offset": 20 + }, + "end": { + "line": 1, + "column": 22, + "offset": 21 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 20, + "offset": 19 + }, + "end": { + "line": 1, + "column": 30, + "offset": 29 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 30, + "offset": 29 + }, + "end": { + "line": 1, + "column": 32, + "offset": 31 + } + } + }, + { + "type": "link", + "title": "h", + "url": "g", + "children": [ + { + "type": "text", + "value": "f", + "position": { + "start": { + "line": 1, + "column": 33, + "offset": 32 + }, + "end": { + "line": 1, + "column": 34, + "offset": 33 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 32, + "offset": 31 + }, + "end": { + "line": 1, + "column": 42, + "offset": 41 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 42, + "offset": 41 + }, + "end": { + "line": 1, + "column": 44, + "offset": 43 + } + } + }, + { + "type": "link", + "title": "k", + "url": "j", + "children": [ + { + "type": "text", + "value": "i", + "position": { + "start": { + "line": 1, + "column": 45, + "offset": 44 + }, + "end": { + "line": 1, + "column": 46, + "offset": 45 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 44, + "offset": 43 + }, + "end": { + "line": 1, + "column": 54, + "offset": 53 + } + } + }, + { + "type": "text", + "value": ", ", + "position": { + "start": { + "line": 1, + "column": 54, + "offset": 53 + }, + "end": { + "line": 1, + "column": 56, + "offset": 55 + } + } + }, + { + "type": "link", + "title": null, + "url": "m", + "children": [ + { + "type": "text", + "value": "l", + "position": { + "start": { + "line": 1, + "column": 57, + "offset": 56 + }, + "end": { + "line": 1, + "column": 58, + "offset": 57 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 56, + "offset": 55 + }, + "end": { + "line": 1, + "column": 64, + "offset": 63 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 1, + "column": 64, + "offset": 63 + }, + "end": { + "line": 1, + "column": 65, + "offset": 64 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 65, + "offset": 64 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "“Content” in images: ", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 66 + }, + "end": { + "line": 3, + "column": 22, + "offset": 87 + } + } + }, + { + "type": "link", + "title": null, + "url": "d", + "children": [ + { + "type": "text", + "value": "a ", + "position": { + "start": { + "line": 3, + "column": 23, + "offset": 88 + }, + "end": { + "line": 3, + "column": 25, + "offset": 90 + } + } + }, + { + "type": "emphasis", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 3, + "column": 26, + "offset": 91 + }, + "end": { + "line": 3, + "column": 27, + "offset": 92 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 25, + "offset": 90 + }, + "end": { + "line": 3, + "column": 28, + "offset": 93 + } + } + }, + { + "type": "text", + "value": " ", + "position": { + "start": { + "line": 3, + "column": 28, + "offset": 93 + }, + "end": { + "line": 3, + "column": 29, + "offset": 94 + } + } + }, + { + "type": "inlineCode", + "value": "c", + "position": { + "start": { + "line": 3, + "column": 29, + "offset": 94 + }, + "end": { + "line": 3, + "column": 32, + "offset": 97 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 22, + "offset": 87 + }, + "end": { + "line": 3, + "column": 36, + "offset": 101 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 3, + "column": 36, + "offset": 101 + }, + "end": { + "line": 3, + "column": 37, + "offset": 102 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 66 + }, + "end": { + "line": 3, + "column": 37, + "offset": 102 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Empty: ", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 104 + }, + "end": { + "line": 5, + "column": 8, + "offset": 111 + } + } + }, + { + "type": "link", + "title": null, + "url": "", + "children": [], + "position": { + "start": { + "line": 5, + "column": 8, + "offset": 111 + }, + "end": { + "line": 5, + "column": 12, + "offset": 115 + } + } + }, + { + "type": "text", + "value": ".", + "position": { + "start": { + "line": 5, + "column": 12, + "offset": 115 + }, + "end": { + "line": 5, + "column": 13, + "offset": 116 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 104 + }, + "end": { + "line": 5, + "column": 13, + "offset": 116 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Character references and escapes:\n", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 118 + }, + "end": { + "line": 8, + "column": 1, + "offset": 152 + } + } + }, + { + "type": "link", + "title": "i*j\tk&l", + "url": "e*f\tg&h", + "children": [ + { + "type": "text", + "value": "a*b\tc&d", + "position": { + "start": { + "line": 8, + "column": 2, + "offset": 153 + }, + "end": { + "line": 8, + "column": 17, + "offset": 168 + } + } + } + ], + "position": { + "start": { + "line": 8, + "column": 1, + "offset": 152 + }, + "end": { + "line": 8, + "column": 53, + "offset": 204 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 118 + }, + "end": { + "line": 8, + "column": 53, + "offset": 204 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 8, + "column": 53, + "offset": 204 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.md new file mode 100644 index 00000000000..c2e02263611 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/link-resource.md @@ -0,0 +1,8 @@ +Resources: [a](b), [c](d "e"), [f](g 'h'), [i](j (k)), [l](). + +“Content” in images: [a *b* `c`](d). + +Empty: [](). + +Character references and escapes: +[a\*b c&d](e\*f g&h "i\*j k&l") \ No newline at end of file diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.json new file mode 100644 index 00000000000..0ee53a2e394 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.json @@ -0,0 +1,1714 @@ +{ + "type": "root", + "children": [ + { + "type": "list", + "ordered": true, + "start": 1, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a", + "position": { + "start": { + "line": 1, + "column": 4, + "offset": 3 + }, + "end": { + "line": 1, + "column": 5, + "offset": 4 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 4, + "offset": 3 + }, + "end": { + "line": 1, + "column": 5, + "offset": 4 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 5, + "offset": 4 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 2, + "column": 4, + "offset": 8 + }, + "end": { + "line": 2, + "column": 5, + "offset": 9 + } + } + } + ], + "position": { + "start": { + "line": 2, + "column": 4, + "offset": 8 + }, + "end": { + "line": 2, + "column": 5, + "offset": 9 + } + } + } + ], + "position": { + "start": { + "line": 2, + "column": 1, + "offset": 5 + }, + "end": { + "line": 2, + "column": 5, + "offset": 9 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "c", + "position": { + "start": { + "line": 3, + "column": 4, + "offset": 13 + }, + "end": { + "line": 3, + "column": 5, + "offset": 14 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 4, + "offset": 13 + }, + "end": { + "line": 3, + "column": 5, + "offset": 14 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 10 + }, + "end": { + "line": 3, + "column": 5, + "offset": 14 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 3, + "column": 5, + "offset": 14 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "d", + "position": { + "start": { + "line": 5, + "column": 3, + "offset": 18 + }, + "end": { + "line": 5, + "column": 4, + "offset": 19 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 3, + "offset": 18 + }, + "end": { + "line": 5, + "column": 4, + "offset": 19 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 16 + }, + "end": { + "line": 5, + "column": 4, + "offset": 19 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 16 + }, + "end": { + "line": 5, + "column": 4, + "offset": 19 + } + } + }, + { + "type": "list", + "ordered": true, + "start": 1, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "e", + "position": { + "start": { + "line": 7, + "column": 5, + "offset": 25 + }, + "end": { + "line": 7, + "column": 6, + "offset": 26 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 5, + "offset": 25 + }, + "end": { + "line": 7, + "column": 6, + "offset": 26 + } + } + }, + { + "type": "code", + "lang": "js", + "meta": null, + "value": "", + "position": { + "start": { + "line": 8, + "column": 5, + "offset": 31 + }, + "end": { + "line": 10, + "column": 8, + "offset": 45 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "f", + "position": { + "start": { + "line": 11, + "column": 5, + "offset": 50 + }, + "end": { + "line": 11, + "column": 6, + "offset": 51 + } + } + } + ], + "position": { + "start": { + "line": 11, + "column": 5, + "offset": 50 + }, + "end": { + "line": 11, + "column": 6, + "offset": 51 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 21 + }, + "end": { + "line": 11, + "column": 6, + "offset": 51 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "g\nh", + "position": { + "start": { + "line": 12, + "column": 5, + "offset": 56 + }, + "end": { + "line": 13, + "column": 6, + "offset": 63 + } + } + } + ], + "position": { + "start": { + "line": 12, + "column": 5, + "offset": 56 + }, + "end": { + "line": 13, + "column": 6, + "offset": 63 + } + } + } + ], + "position": { + "start": { + "line": 12, + "column": 1, + "offset": 52 + }, + "end": { + "line": 13, + "column": 6, + "offset": 63 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 21 + }, + "end": { + "line": 13, + "column": 6, + "offset": 63 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": true, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "i", + "position": { + "start": { + "line": 15, + "column": 3, + "offset": 67 + }, + "end": { + "line": 15, + "column": 4, + "offset": 68 + } + } + } + ], + "position": { + "start": { + "line": 15, + "column": 3, + "offset": 67 + }, + "end": { + "line": 15, + "column": 4, + "offset": 68 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "j", + "position": { + "start": { + "line": 17, + "column": 3, + "offset": 72 + }, + "end": { + "line": 17, + "column": 4, + "offset": 73 + } + } + } + ], + "position": { + "start": { + "line": 17, + "column": 3, + "offset": 72 + }, + "end": { + "line": 17, + "column": 4, + "offset": 73 + } + } + } + ], + "position": { + "start": { + "line": 15, + "column": 1, + "offset": 65 + }, + "end": { + "line": 17, + "column": 4, + "offset": 73 + } + } + } + ], + "position": { + "start": { + "line": 15, + "column": 1, + "offset": 65 + }, + "end": { + "line": 17, + "column": 4, + "offset": 73 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": true, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "k", + "position": { + "start": { + "line": 19, + "column": 3, + "offset": 77 + }, + "end": { + "line": 19, + "column": 4, + "offset": 78 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 3, + "offset": 77 + }, + "end": { + "line": 19, + "column": 4, + "offset": 78 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 75 + }, + "end": { + "line": 19, + "column": 4, + "offset": 78 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "l", + "position": { + "start": { + "line": 21, + "column": 3, + "offset": 82 + }, + "end": { + "line": 21, + "column": 4, + "offset": 83 + } + } + } + ], + "position": { + "start": { + "line": 21, + "column": 3, + "offset": 82 + }, + "end": { + "line": 21, + "column": 4, + "offset": 83 + } + } + } + ], + "position": { + "start": { + "line": 21, + "column": 1, + "offset": 80 + }, + "end": { + "line": 21, + "column": 4, + "offset": 83 + } + } + } + ], + "position": { + "start": { + "line": 19, + "column": 1, + "offset": 75 + }, + "end": { + "line": 21, + "column": 4, + "offset": 83 + } + } + }, + { + "type": "list", + "ordered": true, + "start": 9, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "m", + "position": { + "start": { + "line": 23, + "column": 4, + "offset": 88 + }, + "end": { + "line": 23, + "column": 5, + "offset": 89 + } + } + } + ], + "position": { + "start": { + "line": 23, + "column": 4, + "offset": 88 + }, + "end": { + "line": 23, + "column": 5, + "offset": 89 + } + } + } + ], + "position": { + "start": { + "line": 23, + "column": 1, + "offset": 85 + }, + "end": { + "line": 23, + "column": 5, + "offset": 89 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "n", + "position": { + "start": { + "line": 24, + "column": 5, + "offset": 94 + }, + "end": { + "line": 24, + "column": 6, + "offset": 95 + } + } + } + ], + "position": { + "start": { + "line": 24, + "column": 5, + "offset": 94 + }, + "end": { + "line": 24, + "column": 6, + "offset": 95 + } + } + } + ], + "position": { + "start": { + "line": 24, + "column": 1, + "offset": 90 + }, + "end": { + "line": 24, + "column": 6, + "offset": 95 + } + } + } + ], + "position": { + "start": { + "line": 23, + "column": 1, + "offset": 85 + }, + "end": { + "line": 24, + "column": 6, + "offset": 95 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "o", + "position": { + "start": { + "line": 27, + "column": 3, + "offset": 101 + }, + "end": { + "line": 27, + "column": 4, + "offset": 102 + } + } + } + ], + "position": { + "start": { + "line": 27, + "column": 3, + "offset": 101 + }, + "end": { + "line": 27, + "column": 4, + "offset": 102 + } + } + } + ], + "position": { + "start": { + "line": 26, + "column": 1, + "offset": 97 + }, + "end": { + "line": 27, + "column": 4, + "offset": 102 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "code", + "lang": "p", + "meta": null, + "value": "", + "position": { + "start": { + "line": 29, + "column": 3, + "offset": 107 + }, + "end": { + "line": 31, + "column": 6, + "offset": 118 + } + } + } + ], + "position": { + "start": { + "line": 28, + "column": 1, + "offset": 103 + }, + "end": { + "line": 31, + "column": 6, + "offset": 118 + } + } + } + ], + "position": { + "start": { + "line": 26, + "column": 1, + "offset": 97 + }, + "end": { + "line": 31, + "column": 6, + "offset": 118 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": true, + "children": [ + { + "type": "listItem", + "spread": true, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "q", + "position": { + "start": { + "line": 33, + "column": 3, + "offset": 122 + }, + "end": { + "line": 33, + "column": 4, + "offset": 123 + } + } + } + ], + "position": { + "start": { + "line": 33, + "column": 3, + "offset": 122 + }, + "end": { + "line": 33, + "column": 4, + "offset": 123 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "r", + "position": { + "start": { + "line": 35, + "column": 3, + "offset": 129 + }, + "end": { + "line": 35, + "column": 4, + "offset": 130 + } + } + } + ], + "position": { + "start": { + "line": 35, + "column": 3, + "offset": 129 + }, + "end": { + "line": 35, + "column": 4, + "offset": 130 + } + } + } + ], + "position": { + "start": { + "line": 33, + "column": 1, + "offset": 120 + }, + "end": { + "line": 35, + "column": 4, + "offset": 130 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "s", + "position": { + "start": { + "line": 37, + "column": 3, + "offset": 135 + }, + "end": { + "line": 37, + "column": 4, + "offset": 136 + } + } + } + ], + "position": { + "start": { + "line": 37, + "column": 3, + "offset": 135 + }, + "end": { + "line": 37, + "column": 4, + "offset": 136 + } + } + } + ], + "position": { + "start": { + "line": 36, + "column": 1, + "offset": 131 + }, + "end": { + "line": 37, + "column": 4, + "offset": 136 + } + } + }, + { + "type": "listItem", + "spread": true, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "t", + "position": { + "start": { + "line": 38, + "column": 3, + "offset": 139 + }, + "end": { + "line": 38, + "column": 4, + "offset": 140 + } + } + } + ], + "position": { + "start": { + "line": 38, + "column": 3, + "offset": 139 + }, + "end": { + "line": 38, + "column": 4, + "offset": 140 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "u", + "position": { + "start": { + "line": 41, + "column": 3, + "offset": 148 + }, + "end": { + "line": 41, + "column": 4, + "offset": 149 + } + } + } + ], + "position": { + "start": { + "line": 41, + "column": 3, + "offset": 148 + }, + "end": { + "line": 41, + "column": 4, + "offset": 149 + } + } + } + ], + "position": { + "start": { + "line": 38, + "column": 1, + "offset": 137 + }, + "end": { + "line": 41, + "column": 4, + "offset": 149 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [], + "position": { + "start": { + "line": 42, + "column": 1, + "offset": 150 + }, + "end": { + "line": 42, + "column": 3, + "offset": 152 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "v", + "position": { + "start": { + "line": 45, + "column": 3, + "offset": 162 + }, + "end": { + "line": 45, + "column": 4, + "offset": 163 + } + } + } + ], + "position": { + "start": { + "line": 45, + "column": 3, + "offset": 162 + }, + "end": { + "line": 45, + "column": 4, + "offset": 163 + } + } + } + ], + "position": { + "start": { + "line": 45, + "column": 1, + "offset": 160 + }, + "end": { + "line": 45, + "column": 4, + "offset": 163 + } + } + } + ], + "position": { + "start": { + "line": 33, + "column": 1, + "offset": 120 + }, + "end": { + "line": 45, + "column": 4, + "offset": 163 + } + } + }, + { + "type": "blockquote", + "children": [ + { + "type": "list", + "ordered": false, + "start": null, + "spread": true, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "w", + "position": { + "start": { + "line": 47, + "column": 5, + "offset": 169 + }, + "end": { + "line": 47, + "column": 6, + "offset": 170 + } + } + } + ], + "position": { + "start": { + "line": 47, + "column": 5, + "offset": 169 + }, + "end": { + "line": 47, + "column": 6, + "offset": 170 + } + } + } + ], + "position": { + "start": { + "line": 47, + "column": 3, + "offset": 167 + }, + "end": { + "line": 47, + "column": 6, + "offset": 170 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "x", + "position": { + "start": { + "line": 49, + "column": 5, + "offset": 177 + }, + "end": { + "line": 49, + "column": 6, + "offset": 178 + } + } + } + ], + "position": { + "start": { + "line": 49, + "column": 5, + "offset": 177 + }, + "end": { + "line": 49, + "column": 6, + "offset": 178 + } + } + } + ], + "position": { + "start": { + "line": 49, + "column": 3, + "offset": 175 + }, + "end": { + "line": 49, + "column": 6, + "offset": 178 + } + } + } + ], + "position": { + "start": { + "line": 47, + "column": 3, + "offset": 167 + }, + "end": { + "line": 49, + "column": 6, + "offset": 178 + } + } + } + ], + "position": { + "start": { + "line": 47, + "column": 1, + "offset": 165 + }, + "end": { + "line": 49, + "column": 6, + "offset": 178 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": true, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "y", + "position": { + "start": { + "line": 51, + "column": 3, + "offset": 182 + }, + "end": { + "line": 51, + "column": 4, + "offset": 183 + } + } + } + ], + "position": { + "start": { + "line": 51, + "column": 3, + "offset": 182 + }, + "end": { + "line": 51, + "column": 4, + "offset": 183 + } + } + }, + { + "type": "list", + "ordered": true, + "start": 1, + "spread": true, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "z", + "position": { + "start": { + "line": 53, + "column": 6, + "offset": 190 + }, + "end": { + "line": 53, + "column": 7, + "offset": 191 + } + } + } + ], + "position": { + "start": { + "line": 53, + "column": 6, + "offset": 190 + }, + "end": { + "line": 53, + "column": 7, + "offset": 191 + } + } + } + ], + "position": { + "start": { + "line": 53, + "column": 3, + "offset": 187 + }, + "end": { + "line": 53, + "column": 7, + "offset": 191 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "a", + "position": { + "start": { + "line": 55, + "column": 6, + "offset": 198 + }, + "end": { + "line": 55, + "column": 7, + "offset": 199 + } + } + } + ], + "position": { + "start": { + "line": 55, + "column": 6, + "offset": 198 + }, + "end": { + "line": 55, + "column": 7, + "offset": 199 + } + } + } + ], + "position": { + "start": { + "line": 55, + "column": 3, + "offset": 195 + }, + "end": { + "line": 55, + "column": 7, + "offset": 199 + } + } + } + ], + "position": { + "start": { + "line": 53, + "column": 3, + "offset": 187 + }, + "end": { + "line": 56, + "column": 3, + "offset": 202 + } + } + } + ], + "position": { + "start": { + "line": 51, + "column": 1, + "offset": 180 + }, + "end": { + "line": 56, + "column": 3, + "offset": 202 + } + } + } + ], + "position": { + "start": { + "line": 51, + "column": 1, + "offset": 180 + }, + "end": { + "line": 56, + "column": 3, + "offset": 202 + } + } + }, + { + "type": "list", + "ordered": false, + "start": null, + "spread": false, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "list", + "ordered": true, + "start": 1, + "spread": true, + "children": [ + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "b", + "position": { + "start": { + "line": 57, + "column": 6, + "offset": 208 + }, + "end": { + "line": 57, + "column": 7, + "offset": 209 + } + } + } + ], + "position": { + "start": { + "line": 57, + "column": 6, + "offset": 208 + }, + "end": { + "line": 57, + "column": 7, + "offset": 209 + } + } + } + ], + "position": { + "start": { + "line": 57, + "column": 3, + "offset": 205 + }, + "end": { + "line": 57, + "column": 7, + "offset": 209 + } + } + }, + { + "type": "listItem", + "spread": false, + "checked": null, + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "c", + "position": { + "start": { + "line": 59, + "column": 6, + "offset": 216 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 59, + "column": 6, + "offset": 216 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 59, + "column": 3, + "offset": 213 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 57, + "column": 3, + "offset": 205 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 57, + "column": 1, + "offset": 203 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 57, + "column": 1, + "offset": 203 + }, + "end": { + "line": 59, + "column": 7, + "offset": 217 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 60, + "column": 1, + "offset": 218 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.md new file mode 100644 index 00000000000..269f97d287e --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/list.md @@ -0,0 +1,59 @@ +1. a +1. b +1. c + +* d + +1. e + ```js + + ``` + f +2. g + h + +- i + + j + ++ k + ++ l + +9. m +10. n + +* + o +* + ~~~p + + ~~~ + +- q + + r +- + s +- t + + + u +- + + +- v + +> + w +> +> + x + +* y + + 1. z + + 1. a + +- 1) b + + 2) c diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.json new file mode 100644 index 00000000000..3b580f3cf5a --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.json @@ -0,0 +1,149 @@ +{ + "type": "root", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "A short paragraph.", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 19, + "offset": 18 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 19, + "offset": 18 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "A\nlonger paragraph.", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 20 + }, + "end": { + "line": 4, + "column": 18, + "offset": 39 + } + } + } + ], + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 20 + }, + "end": { + "line": 4, + "column": 18, + "offset": 39 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "A couple blank lines.", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 42 + }, + "end": { + "line": 7, + "column": 22, + "offset": 63 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 42 + }, + "end": { + "line": 7, + "column": 22, + "offset": 63 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "Lots of blank lines.", + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 67 + }, + "end": { + "line": 11, + "column": 21, + "offset": 87 + } + } + } + ], + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 67 + }, + "end": { + "line": 11, + "column": 21, + "offset": 87 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 12, + "column": 1, + "offset": 88 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.md new file mode 100644 index 00000000000..ae3dac87bd9 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/paragraph.md @@ -0,0 +1,11 @@ +A short paragraph. + +A +longer paragraph. + + +A couple blank lines. + + + +Lots of blank lines. diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.json b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.json new file mode 100644 index 00000000000..92ed1ebe545 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.json @@ -0,0 +1,176 @@ +{ + "type": "root", + "children": [ + { + "type": "thematicBreak", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 4, + "offset": 3 + } + } + }, + { + "type": "thematicBreak", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 5 + }, + "end": { + "line": 3, + "column": 4, + "offset": 8 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "+++", + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 10 + }, + "end": { + "line": 5, + "column": 4, + "offset": 13 + } + } + } + ], + "position": { + "start": { + "line": 5, + "column": 1, + "offset": 10 + }, + "end": { + "line": 5, + "column": 4, + "offset": 13 + } + } + }, + { + "type": "thematicBreak", + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 15 + }, + "end": { + "line": 7, + "column": 6, + "offset": 20 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "++ ++ ++", + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 22 + }, + "end": { + "line": 9, + "column": 9, + "offset": 30 + } + } + } + ], + "position": { + "start": { + "line": 9, + "column": 1, + "offset": 22 + }, + "end": { + "line": 9, + "column": 9, + "offset": 30 + } + } + }, + { + "type": "thematicBreak", + "position": { + "start": { + "line": 11, + "column": 1, + "offset": 32 + }, + "end": { + "line": 11, + "column": 4, + "offset": 35 + } + } + }, + { + "type": "paragraph", + "children": [ + { + "type": "text", + "value": "-+-", + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 37 + }, + "end": { + "line": 13, + "column": 4, + "offset": 40 + } + } + } + ], + "position": { + "start": { + "line": 13, + "column": 1, + "offset": 37 + }, + "end": { + "line": 13, + "column": 4, + "offset": 40 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 14, + "column": 1, + "offset": 41 + } + } +} diff --git a/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.md b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.md new file mode 100644 index 00000000000..e14a7decd02 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/mdast/fixtures/thematic-break.md @@ -0,0 +1,13 @@ +*** + +--- + ++++ + +- - - + +++ ++ ++ + +___ + +-+- diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.html new file mode 100644 index 00000000000..568bf9e1b71 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.html @@ -0,0 +1,104 @@ +

Align

+

An empty initial cell

+ + + + + + + + + + + + + + + + + + + + +
ac
abc
abc
+

Missing alignment characters

+

| a | b | c | +| |---|---| +| d | e | f |

+
+

| a | b | c | +|---|---| | +| d | e | f |

+

Incorrect characters

+

| a | b | c | +|---|-*-|---| +| d | e | f |

+

Two alignments

+

|a| +|::|

+ + + + + + +
a
+

Two at the start or end

+

|a| +|::-|

+

|a| +|-::|

+

In the middle

+

|a| +|-:-|

+

A space in the middle

+

|a| +|- -|

+

No pipe

+ + + + + + +
a
+ + + + + + +
a
+ + + + + + +
a
+

A single colon

+

|a| +|:|

+

a +:

+

Alignment on empty cells

+ + + + + + + + + + + + + + + + + + + +
abcde
f
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.md new file mode 100644 index 00000000000..8404df94f6a --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/align.md @@ -0,0 +1,77 @@ +# Align + +## An empty initial cell + +| | a|c| +|--|:----:|:---| +|a|b|c| +|a|b|c| + +## Missing alignment characters + +| a | b | c | +| |---|---| +| d | e | f | + +* * * + +| a | b | c | +|---|---| | +| d | e | f | + +## Incorrect characters + +| a | b | c | +|---|-*-|---| +| d | e | f | + +## Two alignments + +|a| +|::| + +|a| +|:-:| + +## Two at the start or end + +|a| +|::-| + +|a| +|-::| + +## In the middle + +|a| +|-:-| + +## A space in the middle + +|a| +|- -| + +## No pipe + +a +:-: + +a +:- + +a +-: + +## A single colon + +|a| +|:| + +a +: + +## Alignment on empty cells + +| a | b | c | d | e | +| - | - | :- | -: | :-: | +| f | diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.html new file mode 100644 index 00000000000..7a487853aeb --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.html @@ -0,0 +1,40 @@ +

Tables

+ + + + + + + + + + + + + + + +
abc
def
+

No body

+ + + + + + + + +
abc
+

One column

+ + + + + + + + + + + +
a
b
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.md new file mode 100644 index 00000000000..058f6a7bde0 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/basic.md @@ -0,0 +1,16 @@ +# Tables + +| a | b | c | +| - | - | - | +| d | e | f | + +## No body + +| a | b | c | +| - | - | - | + +## One column + +| a | +| - | +| b | diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.html new file mode 100644 index 00000000000..c4b2dda75ba --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.html @@ -0,0 +1,132 @@ +

Tables in things

+

In lists

+
    +
  • +

    Unordered:

    + + + + + + + + + + + + + +
    AB
    12
    +
  • +
+
    +
  1. +

    Ordered:

    + + + + + + + + + + + + + +
    AB
    12
    +
  2. +
+
    +
  • Lazy? + + + + + + + +
    AB
    +
  • +
+

| 1 | 2 | +| 3 | 4 | +| 5 | 6 | +| 7 | 8 |

+

In block quotes

+
+

W/ space:

+ + + + + + + + + + + + + +
AB
12
+
+
+

W/o space:

+ + + + + + + + + + + + + +
AB
12
+
+
+

Lazy?

+ + + + + + + + + + + + + + + + + +
AB
12
34
+
+

| 5 | 6 |

+

List interrupting delimiters

+

a |

+
    +
  • |
  • +
+ + + + + + +
a
+ + + + + + +
a
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.md new file mode 100644 index 00000000000..642614fd0da --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/containers.md @@ -0,0 +1,53 @@ +# Tables in things + +## In lists + +* Unordered: + + | A | B | + | - | - | + | 1 | 2 | + +1. Ordered: + + | A | B | + | - | - | + | 1 | 2 | + +* Lazy? + | A | B | + | - | - | + | 1 | 2 | + | 3 | 4 | + | 5 | 6 | +| 7 | 8 | + +## In block quotes + +> W/ space: +> | A | B | +> | - | - | +> | 1 | 2 | + +>W/o space: +>| A | B | +>| - | - | +>| 1 | 2 | + +> Lazy? +> | A | B | +> | - | - | +> | 1 | 2 | +>| 3 | 4 | +| 5 | 6 | + +### List interrupting delimiters + +a | +- | + +a +-| + +a +|- diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.html new file mode 100644 index 00000000000..cd0f81978b4 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.html @@ -0,0 +1,117 @@ +

Examples from GFM

+

A

+ + + + + + + + + + + + + +
foobar
bazbim
+

B

+ + + + + + + + + + + + + +
abcdefghi
barbaz
+

C

+ + + + + + + + + + + + + + +
f|oo
b | az
b | im
+

D

+ + + + + + + + + + + + + +
abcdef
barbaz
+
+

bar

+
+

E

+ + + + + + + + + + + + + + + + + +
abcdef
barbaz
bar
+

bar

+

F

+

| abc | def | +| --- | +| bar |

+

G

+ + + + + + + + + + + + + + + + + +
abcdef
bar
barbaz
+

H

+ + + + + + + +
abcdef
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.md new file mode 100644 index 00000000000..eaab09996b7 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/gfm.md @@ -0,0 +1,54 @@ +# Examples from GFM + +## A + +| foo | bar | +| --- | --- | +| baz | bim | + +## B + +| abc | defghi | +:-: | -----------: +bar | baz + +## C + +| f\|oo | +| ------ | +| b `\|` az | +| b **\|** im | + +## D + +| abc | def | +| --- | --- | +| bar | baz | +> bar + +## E + +| abc | def | +| --- | --- | +| bar | baz | +bar + +bar + +## F + +| abc | def | +| --- | +| bar | + +## G + +| abc | def | +| --- | --- | +| bar | +| bar | baz | boo | + +## H + +| abc | def | +| --- | --- | diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.html new file mode 100644 index 00000000000..d560de9ccf0 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.html @@ -0,0 +1,85 @@ +

Grave accents

+

Grave accent in cell

+ + + + + + + + + + + + + +
AB
`C
+

Escaped grave accent in “inline code” in cell

+ + + + + + + + + + + +
A
\
+

“Empty” inline code

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
123
a``
b````
c``
d``
e|
f|
+

Escaped pipes in code in cells

+ + + + + + + + + + + +
|\\
|\\
+

\|\\

diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.md new file mode 100644 index 00000000000..78692f7739d --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/grave.md @@ -0,0 +1,32 @@ +# Grave accents + +## Grave accent in cell + +| A | B | +|--------------|---| +| ` | C | + +## Escaped grave accent in “inline code” in cell + +| A | +|-----| +| `\` | + +## “Empty” inline code + +| 1 | 2 | 3 | +|---|------|----| +| a | `` | | +| b | `` | `` | +| c | ` | ` | +| d | `|` | +| e | `\|` | | +| f | \| | | + +## Escaped pipes in code in cells + +| `\|\\` | +| --- | +| `\|\\` | + +`\|\\` diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.html new file mode 100644 index 00000000000..f7ede88b151 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.html @@ -0,0 +1,29 @@ +

Code

+

Indented delimiter row

+ + + + + + +
a
+

a +|-

+

Indented body

+ + + + + + + + + + + + + + +
a
C
D
+
| E |
+
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.md new file mode 100644 index 00000000000..bc194b4c8b0 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/indent.md @@ -0,0 +1,17 @@ +# Code + +## Indented delimiter row + +a + |- + +a + |- + +## Indented body + +| a | + | - | + | C | + | D | + | E | diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.html new file mode 100644 index 00000000000..7faef228a54 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.html @@ -0,0 +1,31 @@ +

Loose

+

Loose

+ + + + + + + + + + + + + + + + + +
Header 1Header 2
Cell 1Cell 2
Cell 3Cell 4
+

One “column”, loose

+

a

+

b

+

No pipe in first row

+ + + + + + +
a
diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.md new file mode 100644 index 00000000000..53d70426c5c --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/loose.md @@ -0,0 +1,19 @@ +# Loose + +## Loose + +Header 1 | Header 2 +-------- | -------- +Cell 1 | Cell 2 +Cell 3 | Cell 4 + +## One “column”, loose + +a +- +b + +## No pipe in first row + +a +| - | diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.html b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.html new file mode 100644 index 00000000000..03a4a17563f --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.html @@ -0,0 +1,27 @@ +

Some more escapes

+ + + + + + + + + + + + + + + + + + + + + + + +
Head
A
B | Bravo
C | Charlie
D \| Delta
E \| Echo
+

Note: GH has a bug where in case C and E, the escaped escape is treated as a +normal escape.

diff --git a/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.md b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.md new file mode 100644 index 00000000000..985d5081291 --- /dev/null +++ b/libs/markdown/src/test/resources/appeng/libs/micromark/extensions/gfm/some-escapes.md @@ -0,0 +1,12 @@ +# Some more escapes + +| Head | +| ------------- | +| A | Alpha | +| B \| Bravo | +| C \\| Charlie | +| D \\\| Delta | +| E \\\\| Echo | + +Note: GH has a bug where in case C and E, the escaped escape is treated as a +normal escape. diff --git a/libs/markdown/src/test/resources/commonmark.json b/libs/markdown/src/test/resources/commonmark.json new file mode 100644 index 00000000000..d742f941312 --- /dev/null +++ b/libs/markdown/src/test/resources/commonmark.json @@ -0,0 +1,5218 @@ +[ + { + "markdown": "\tfoo\tbaz\t\tbim\n", + "html": "
foo\tbaz\t\tbim\n
\n", + "example": 1, + "start_line": 356, + "end_line": 361, + "section": "Tabs" + }, + { + "markdown": " \tfoo\tbaz\t\tbim\n", + "html": "
foo\tbaz\t\tbim\n
\n", + "example": 2, + "start_line": 363, + "end_line": 368, + "section": "Tabs" + }, + { + "markdown": " a\ta\n ὐ\ta\n", + "html": "
a\ta\nὐ\ta\n
\n", + "example": 3, + "start_line": 370, + "end_line": 377, + "section": "Tabs" + }, + { + "markdown": " - foo\n\n\tbar\n", + "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", + "example": 4, + "start_line": 383, + "end_line": 394, + "section": "Tabs" + }, + { + "markdown": "- foo\n\n\t\tbar\n", + "html": "
    \n
  • \n

    foo

    \n
      bar\n
    \n
  • \n
\n", + "example": 5, + "start_line": 396, + "end_line": 408, + "section": "Tabs" + }, + { + "markdown": ">\t\tfoo\n", + "html": "
\n
  foo\n
\n
\n", + "example": 6, + "start_line": 419, + "end_line": 426, + "section": "Tabs" + }, + { + "markdown": "-\t\tfoo\n", + "html": "
    \n
  • \n
      foo\n
    \n
  • \n
\n", + "example": 7, + "start_line": 428, + "end_line": 437, + "section": "Tabs" + }, + { + "markdown": " foo\n\tbar\n", + "html": "
foo\nbar\n
\n", + "example": 8, + "start_line": 440, + "end_line": 447, + "section": "Tabs" + }, + { + "markdown": " - foo\n - bar\n\t - baz\n", + "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz
      • \n
      \n
    • \n
    \n
  • \n
\n", + "example": 9, + "start_line": 449, + "end_line": 465, + "section": "Tabs" + }, + { + "markdown": "#\tFoo\n", + "html": "

Foo

\n", + "example": 10, + "start_line": 467, + "end_line": 471, + "section": "Tabs" + }, + { + "markdown": "*\t*\t*\t\n", + "html": "
\n", + "example": 11, + "start_line": 473, + "end_line": 477, + "section": "Tabs" + }, + { + "markdown": "\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n", + "html": "

!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~

\n", + "example": 12, + "start_line": 490, + "end_line": 494, + "section": "Backslash escapes" + }, + { + "markdown": "\\\t\\A\\a\\ \\3\\φ\\«\n", + "html": "

\\\t\\A\\a\\ \\3\\φ\\«

\n", + "example": 13, + "start_line": 500, + "end_line": 504, + "section": "Backslash escapes" + }, + { + "markdown": "\\*not emphasized*\n\\
not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n\\ö not a character entity\n", + "html": "

*not emphasized*\n<br/> not a tag\n[not a link](/foo)\n`not code`\n1. not a list\n* not a list\n# not a heading\n[foo]: /url "not a reference"\n&ouml; not a character entity

\n", + "example": 14, + "start_line": 510, + "end_line": 530, + "section": "Backslash escapes" + }, + { + "markdown": "\\\\*emphasis*\n", + "html": "

\\emphasis

\n", + "example": 15, + "start_line": 535, + "end_line": 539, + "section": "Backslash escapes" + }, + { + "markdown": "foo\\\nbar\n", + "html": "

foo
\nbar

\n", + "example": 16, + "start_line": 544, + "end_line": 550, + "section": "Backslash escapes" + }, + { + "markdown": "`` \\[\\` ``\n", + "html": "

\\[\\`

\n", + "example": 17, + "start_line": 556, + "end_line": 560, + "section": "Backslash escapes" + }, + { + "markdown": " \\[\\]\n", + "html": "
\\[\\]\n
\n", + "example": 18, + "start_line": 563, + "end_line": 568, + "section": "Backslash escapes" + }, + { + "markdown": "~~~\n\\[\\]\n~~~\n", + "html": "
\\[\\]\n
\n", + "example": 19, + "start_line": 571, + "end_line": 578, + "section": "Backslash escapes" + }, + { + "markdown": "\n", + "html": "

http://example.com?find=\\*

\n", + "example": 20, + "start_line": 581, + "end_line": 585, + "section": "Backslash escapes" + }, + { + "markdown": "\n", + "html": "\n", + "example": 21, + "start_line": 588, + "end_line": 592, + "section": "Backslash escapes" + }, + { + "markdown": "[foo](/bar\\* \"ti\\*tle\")\n", + "html": "

foo

\n", + "example": 22, + "start_line": 598, + "end_line": 602, + "section": "Backslash escapes" + }, + { + "markdown": "[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n", + "html": "

foo

\n", + "example": 23, + "start_line": 605, + "end_line": 611, + "section": "Backslash escapes" + }, + { + "markdown": "``` foo\\+bar\nfoo\n```\n", + "html": "
foo\n
\n", + "example": 24, + "start_line": 614, + "end_line": 621, + "section": "Backslash escapes" + }, + { + "markdown": "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n", + "html": "

  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸

\n", + "example": 25, + "start_line": 650, + "end_line": 658, + "section": "Entity and numeric character references" + }, + { + "markdown": "# Ӓ Ϡ �\n", + "html": "

# Ӓ Ϡ �

\n", + "example": 26, + "start_line": 669, + "end_line": 673, + "section": "Entity and numeric character references" + }, + { + "markdown": "" ആ ಫ\n", + "html": "

" ആ ಫ

\n", + "example": 27, + "start_line": 682, + "end_line": 686, + "section": "Entity and numeric character references" + }, + { + "markdown": "  &x; &#; &#x;\n�\n&#abcdef0;\n&ThisIsNotDefined; &hi?;\n", + "html": "

&nbsp &x; &#; &#x;\n&#87654321;\n&#abcdef0;\n&ThisIsNotDefined; &hi?;

\n", + "example": 28, + "start_line": 691, + "end_line": 701, + "section": "Entity and numeric character references" + }, + { + "markdown": "©\n", + "html": "

&copy

\n", + "example": 29, + "start_line": 708, + "end_line": 712, + "section": "Entity and numeric character references" + }, + { + "markdown": "&MadeUpEntity;\n", + "html": "

&MadeUpEntity;

\n", + "example": 30, + "start_line": 718, + "end_line": 722, + "section": "Entity and numeric character references" + }, + { + "markdown": "\n", + "html": "\n", + "example": 31, + "start_line": 729, + "end_line": 733, + "section": "Entity and numeric character references" + }, + { + "markdown": "[foo](/föö \"föö\")\n", + "html": "

foo

\n", + "example": 32, + "start_line": 736, + "end_line": 740, + "section": "Entity and numeric character references" + }, + { + "markdown": "[foo]\n\n[foo]: /föö \"föö\"\n", + "html": "

foo

\n", + "example": 33, + "start_line": 743, + "end_line": 749, + "section": "Entity and numeric character references" + }, + { + "markdown": "``` föö\nfoo\n```\n", + "html": "
foo\n
\n", + "example": 34, + "start_line": 752, + "end_line": 759, + "section": "Entity and numeric character references" + }, + { + "markdown": "`föö`\n", + "html": "

f&ouml;&ouml;

\n", + "example": 35, + "start_line": 765, + "end_line": 769, + "section": "Entity and numeric character references" + }, + { + "markdown": " föfö\n", + "html": "
f&ouml;f&ouml;\n
\n", + "example": 36, + "start_line": 772, + "end_line": 777, + "section": "Entity and numeric character references" + }, + { + "markdown": "*foo*\n*foo*\n", + "html": "

*foo*\nfoo

\n", + "example": 37, + "start_line": 784, + "end_line": 790, + "section": "Entity and numeric character references" + }, + { + "markdown": "* foo\n\n* foo\n", + "html": "

* foo

\n
    \n
  • foo
  • \n
\n", + "example": 38, + "start_line": 792, + "end_line": 801, + "section": "Entity and numeric character references" + }, + { + "markdown": "foo bar\n", + "html": "

foo\n\nbar

\n", + "example": 39, + "start_line": 803, + "end_line": 809, + "section": "Entity and numeric character references" + }, + { + "markdown": " foo\n", + "html": "

\tfoo

\n", + "example": 40, + "start_line": 811, + "end_line": 815, + "section": "Entity and numeric character references" + }, + { + "markdown": "[a](url "tit")\n", + "html": "

[a](url "tit")

\n", + "example": 41, + "start_line": 818, + "end_line": 822, + "section": "Entity and numeric character references" + }, + { + "markdown": "- `one\n- two`\n", + "html": "
    \n
  • `one
  • \n
  • two`
  • \n
\n", + "example": 42, + "start_line": 841, + "end_line": 849, + "section": "Precedence" + }, + { + "markdown": "***\n---\n___\n", + "html": "
\n
\n
\n", + "example": 43, + "start_line": 880, + "end_line": 888, + "section": "Thematic breaks" + }, + { + "markdown": "+++\n", + "html": "

+++

\n", + "example": 44, + "start_line": 893, + "end_line": 897, + "section": "Thematic breaks" + }, + { + "markdown": "===\n", + "html": "

===

\n", + "example": 45, + "start_line": 900, + "end_line": 904, + "section": "Thematic breaks" + }, + { + "markdown": "--\n**\n__\n", + "html": "

--\n**\n__

\n", + "example": 46, + "start_line": 909, + "end_line": 917, + "section": "Thematic breaks" + }, + { + "markdown": " ***\n ***\n ***\n", + "html": "
\n
\n
\n", + "example": 47, + "start_line": 922, + "end_line": 930, + "section": "Thematic breaks" + }, + { + "markdown": " ***\n", + "html": "
***\n
\n", + "example": 48, + "start_line": 935, + "end_line": 940, + "section": "Thematic breaks" + }, + { + "markdown": "Foo\n ***\n", + "html": "

Foo\n***

\n", + "example": 49, + "start_line": 943, + "end_line": 949, + "section": "Thematic breaks" + }, + { + "markdown": "_____________________________________\n", + "html": "
\n", + "example": 50, + "start_line": 954, + "end_line": 958, + "section": "Thematic breaks" + }, + { + "markdown": " - - -\n", + "html": "
\n", + "example": 51, + "start_line": 963, + "end_line": 967, + "section": "Thematic breaks" + }, + { + "markdown": " ** * ** * ** * **\n", + "html": "
\n", + "example": 52, + "start_line": 970, + "end_line": 974, + "section": "Thematic breaks" + }, + { + "markdown": "- - - -\n", + "html": "
\n", + "example": 53, + "start_line": 977, + "end_line": 981, + "section": "Thematic breaks" + }, + { + "markdown": "- - - - \n", + "html": "
\n", + "example": 54, + "start_line": 986, + "end_line": 990, + "section": "Thematic breaks" + }, + { + "markdown": "_ _ _ _ a\n\na------\n\n---a---\n", + "html": "

_ _ _ _ a

\n

a------

\n

---a---

\n", + "example": 55, + "start_line": 995, + "end_line": 1005, + "section": "Thematic breaks" + }, + { + "markdown": " *-*\n", + "html": "

-

\n", + "example": 56, + "start_line": 1011, + "end_line": 1015, + "section": "Thematic breaks" + }, + { + "markdown": "- foo\n***\n- bar\n", + "html": "
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
\n", + "example": 57, + "start_line": 1020, + "end_line": 1032, + "section": "Thematic breaks" + }, + { + "markdown": "Foo\n***\nbar\n", + "html": "

Foo

\n
\n

bar

\n", + "example": 58, + "start_line": 1037, + "end_line": 1045, + "section": "Thematic breaks" + }, + { + "markdown": "Foo\n---\nbar\n", + "html": "

Foo

\n

bar

\n", + "example": 59, + "start_line": 1054, + "end_line": 1061, + "section": "Thematic breaks" + }, + { + "markdown": "* Foo\n* * *\n* Bar\n", + "html": "
    \n
  • Foo
  • \n
\n
\n
    \n
  • Bar
  • \n
\n", + "example": 60, + "start_line": 1067, + "end_line": 1079, + "section": "Thematic breaks" + }, + { + "markdown": "- Foo\n- * * *\n", + "html": "
    \n
  • Foo
  • \n
  • \n
    \n
  • \n
\n", + "example": 61, + "start_line": 1084, + "end_line": 1094, + "section": "Thematic breaks" + }, + { + "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n", + "html": "

foo

\n

foo

\n

foo

\n

foo

\n
foo
\n
foo
\n", + "example": 62, + "start_line": 1113, + "end_line": 1127, + "section": "ATX headings" + }, + { + "markdown": "####### foo\n", + "html": "

####### foo

\n", + "example": 63, + "start_line": 1132, + "end_line": 1136, + "section": "ATX headings" + }, + { + "markdown": "#5 bolt\n\n#hashtag\n", + "html": "

#5 bolt

\n

#hashtag

\n", + "example": 64, + "start_line": 1147, + "end_line": 1154, + "section": "ATX headings" + }, + { + "markdown": "\\## foo\n", + "html": "

## foo

\n", + "example": 65, + "start_line": 1159, + "end_line": 1163, + "section": "ATX headings" + }, + { + "markdown": "# foo *bar* \\*baz\\*\n", + "html": "

foo bar *baz*

\n", + "example": 66, + "start_line": 1168, + "end_line": 1172, + "section": "ATX headings" + }, + { + "markdown": "# foo \n", + "html": "

foo

\n", + "example": 67, + "start_line": 1177, + "end_line": 1181, + "section": "ATX headings" + }, + { + "markdown": " ### foo\n ## foo\n # foo\n", + "html": "

foo

\n

foo

\n

foo

\n", + "example": 68, + "start_line": 1186, + "end_line": 1194, + "section": "ATX headings" + }, + { + "markdown": " # foo\n", + "html": "
# foo\n
\n", + "example": 69, + "start_line": 1199, + "end_line": 1204, + "section": "ATX headings" + }, + { + "markdown": "foo\n # bar\n", + "html": "

foo\n# bar

\n", + "example": 70, + "start_line": 1207, + "end_line": 1213, + "section": "ATX headings" + }, + { + "markdown": "## foo ##\n ### bar ###\n", + "html": "

foo

\n

bar

\n", + "example": 71, + "start_line": 1218, + "end_line": 1224, + "section": "ATX headings" + }, + { + "markdown": "# foo ##################################\n##### foo ##\n", + "html": "

foo

\n
foo
\n", + "example": 72, + "start_line": 1229, + "end_line": 1235, + "section": "ATX headings" + }, + { + "markdown": "### foo ### \n", + "html": "

foo

\n", + "example": 73, + "start_line": 1240, + "end_line": 1244, + "section": "ATX headings" + }, + { + "markdown": "### foo ### b\n", + "html": "

foo ### b

\n", + "example": 74, + "start_line": 1251, + "end_line": 1255, + "section": "ATX headings" + }, + { + "markdown": "# foo#\n", + "html": "

foo#

\n", + "example": 75, + "start_line": 1260, + "end_line": 1264, + "section": "ATX headings" + }, + { + "markdown": "### foo \\###\n## foo #\\##\n# foo \\#\n", + "html": "

foo ###

\n

foo ###

\n

foo #

\n", + "example": 76, + "start_line": 1270, + "end_line": 1278, + "section": "ATX headings" + }, + { + "markdown": "****\n## foo\n****\n", + "html": "
\n

foo

\n
\n", + "example": 77, + "start_line": 1284, + "end_line": 1292, + "section": "ATX headings" + }, + { + "markdown": "Foo bar\n# baz\nBar foo\n", + "html": "

Foo bar

\n

baz

\n

Bar foo

\n", + "example": 78, + "start_line": 1295, + "end_line": 1303, + "section": "ATX headings" + }, + { + "markdown": "## \n#\n### ###\n", + "html": "

\n

\n

\n", + "example": 79, + "start_line": 1308, + "end_line": 1316, + "section": "ATX headings" + }, + { + "markdown": "Foo *bar*\n=========\n\nFoo *bar*\n---------\n", + "html": "

Foo bar

\n

Foo bar

\n", + "example": 80, + "start_line": 1351, + "end_line": 1360, + "section": "Setext headings" + }, + { + "markdown": "Foo *bar\nbaz*\n====\n", + "html": "

Foo bar\nbaz

\n", + "example": 81, + "start_line": 1365, + "end_line": 1372, + "section": "Setext headings" + }, + { + "markdown": " Foo *bar\nbaz*\t\n====\n", + "html": "

Foo bar\nbaz

\n", + "example": 82, + "start_line": 1379, + "end_line": 1386, + "section": "Setext headings" + }, + { + "markdown": "Foo\n-------------------------\n\nFoo\n=\n", + "html": "

Foo

\n

Foo

\n", + "example": 83, + "start_line": 1391, + "end_line": 1400, + "section": "Setext headings" + }, + { + "markdown": " Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n", + "html": "

Foo

\n

Foo

\n

Foo

\n", + "example": 84, + "start_line": 1406, + "end_line": 1419, + "section": "Setext headings" + }, + { + "markdown": " Foo\n ---\n\n Foo\n---\n", + "html": "
Foo\n---\n\nFoo\n
\n
\n", + "example": 85, + "start_line": 1424, + "end_line": 1437, + "section": "Setext headings" + }, + { + "markdown": "Foo\n ---- \n", + "html": "

Foo

\n", + "example": 86, + "start_line": 1443, + "end_line": 1448, + "section": "Setext headings" + }, + { + "markdown": "Foo\n ---\n", + "html": "

Foo\n---

\n", + "example": 87, + "start_line": 1453, + "end_line": 1459, + "section": "Setext headings" + }, + { + "markdown": "Foo\n= =\n\nFoo\n--- -\n", + "html": "

Foo\n= =

\n

Foo

\n
\n", + "example": 88, + "start_line": 1464, + "end_line": 1475, + "section": "Setext headings" + }, + { + "markdown": "Foo \n-----\n", + "html": "

Foo

\n", + "example": 89, + "start_line": 1480, + "end_line": 1485, + "section": "Setext headings" + }, + { + "markdown": "Foo\\\n----\n", + "html": "

Foo\\

\n", + "example": 90, + "start_line": 1490, + "end_line": 1495, + "section": "Setext headings" + }, + { + "markdown": "`Foo\n----\n`\n\n\n", + "html": "

`Foo

\n

`

\n

<a title="a lot

\n

of dashes"/>

\n", + "example": 91, + "start_line": 1501, + "end_line": 1514, + "section": "Setext headings" + }, + { + "markdown": "> Foo\n---\n", + "html": "
\n

Foo

\n
\n
\n", + "example": 92, + "start_line": 1520, + "end_line": 1528, + "section": "Setext headings" + }, + { + "markdown": "> foo\nbar\n===\n", + "html": "
\n

foo\nbar\n===

\n
\n", + "example": 93, + "start_line": 1531, + "end_line": 1541, + "section": "Setext headings" + }, + { + "markdown": "- Foo\n---\n", + "html": "
    \n
  • Foo
  • \n
\n
\n", + "example": 94, + "start_line": 1544, + "end_line": 1552, + "section": "Setext headings" + }, + { + "markdown": "Foo\nBar\n---\n", + "html": "

Foo\nBar

\n", + "example": 95, + "start_line": 1559, + "end_line": 1566, + "section": "Setext headings" + }, + { + "markdown": "---\nFoo\n---\nBar\n---\nBaz\n", + "html": "
\n

Foo

\n

Bar

\n

Baz

\n", + "example": 96, + "start_line": 1572, + "end_line": 1584, + "section": "Setext headings" + }, + { + "markdown": "\n====\n", + "html": "

====

\n", + "example": 97, + "start_line": 1589, + "end_line": 1594, + "section": "Setext headings" + }, + { + "markdown": "---\n---\n", + "html": "
\n
\n", + "example": 98, + "start_line": 1601, + "end_line": 1607, + "section": "Setext headings" + }, + { + "markdown": "- foo\n-----\n", + "html": "
    \n
  • foo
  • \n
\n
\n", + "example": 99, + "start_line": 1610, + "end_line": 1618, + "section": "Setext headings" + }, + { + "markdown": " foo\n---\n", + "html": "
foo\n
\n
\n", + "example": 100, + "start_line": 1621, + "end_line": 1628, + "section": "Setext headings" + }, + { + "markdown": "> foo\n-----\n", + "html": "
\n

foo

\n
\n
\n", + "example": 101, + "start_line": 1631, + "end_line": 1639, + "section": "Setext headings" + }, + { + "markdown": "\\> foo\n------\n", + "html": "

> foo

\n", + "example": 102, + "start_line": 1645, + "end_line": 1650, + "section": "Setext headings" + }, + { + "markdown": "Foo\n\nbar\n---\nbaz\n", + "html": "

Foo

\n

bar

\n

baz

\n", + "example": 103, + "start_line": 1676, + "end_line": 1686, + "section": "Setext headings" + }, + { + "markdown": "Foo\nbar\n\n---\n\nbaz\n", + "html": "

Foo\nbar

\n
\n

baz

\n", + "example": 104, + "start_line": 1692, + "end_line": 1704, + "section": "Setext headings" + }, + { + "markdown": "Foo\nbar\n* * *\nbaz\n", + "html": "

Foo\nbar

\n
\n

baz

\n", + "example": 105, + "start_line": 1710, + "end_line": 1720, + "section": "Setext headings" + }, + { + "markdown": "Foo\nbar\n\\---\nbaz\n", + "html": "

Foo\nbar\n---\nbaz

\n", + "example": 106, + "start_line": 1725, + "end_line": 1735, + "section": "Setext headings" + }, + { + "markdown": " a simple\n indented code block\n", + "html": "
a simple\n  indented code block\n
\n", + "example": 107, + "start_line": 1753, + "end_line": 1760, + "section": "Indented code blocks" + }, + { + "markdown": " - foo\n\n bar\n", + "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", + "example": 108, + "start_line": 1767, + "end_line": 1778, + "section": "Indented code blocks" + }, + { + "markdown": "1. foo\n\n - bar\n", + "html": "
    \n
  1. \n

    foo

    \n
      \n
    • bar
    • \n
    \n
  2. \n
\n", + "example": 109, + "start_line": 1781, + "end_line": 1794, + "section": "Indented code blocks" + }, + { + "markdown": "
\n *hi*\n\n - one\n", + "html": "
<a/>\n*hi*\n\n- one\n
\n", + "example": 110, + "start_line": 1801, + "end_line": 1812, + "section": "Indented code blocks" + }, + { + "markdown": " chunk1\n\n chunk2\n \n \n \n chunk3\n", + "html": "
chunk1\n\nchunk2\n\n\n\nchunk3\n
\n", + "example": 111, + "start_line": 1817, + "end_line": 1834, + "section": "Indented code blocks" + }, + { + "markdown": " chunk1\n \n chunk2\n", + "html": "
chunk1\n  \n  chunk2\n
\n", + "example": 112, + "start_line": 1840, + "end_line": 1849, + "section": "Indented code blocks" + }, + { + "markdown": "Foo\n bar\n\n", + "html": "

Foo\nbar

\n", + "example": 113, + "start_line": 1855, + "end_line": 1862, + "section": "Indented code blocks" + }, + { + "markdown": " foo\nbar\n", + "html": "
foo\n
\n

bar

\n", + "example": 114, + "start_line": 1869, + "end_line": 1876, + "section": "Indented code blocks" + }, + { + "markdown": "# Heading\n foo\nHeading\n------\n foo\n----\n", + "html": "

Heading

\n
foo\n
\n

Heading

\n
foo\n
\n
\n", + "example": 115, + "start_line": 1882, + "end_line": 1897, + "section": "Indented code blocks" + }, + { + "markdown": " foo\n bar\n", + "html": "
    foo\nbar\n
\n", + "example": 116, + "start_line": 1902, + "end_line": 1909, + "section": "Indented code blocks" + }, + { + "markdown": "\n \n foo\n \n\n", + "html": "
foo\n
\n", + "example": 117, + "start_line": 1915, + "end_line": 1924, + "section": "Indented code blocks" + }, + { + "markdown": " foo \n", + "html": "
foo  \n
\n", + "example": 118, + "start_line": 1929, + "end_line": 1934, + "section": "Indented code blocks" + }, + { + "markdown": "```\n<\n >\n```\n", + "html": "
<\n >\n
\n", + "example": 119, + "start_line": 1984, + "end_line": 1993, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~\n<\n >\n~~~\n", + "html": "
<\n >\n
\n", + "example": 120, + "start_line": 1998, + "end_line": 2007, + "section": "Fenced code blocks" + }, + { + "markdown": "``\nfoo\n``\n", + "html": "

foo

\n", + "example": 121, + "start_line": 2011, + "end_line": 2017, + "section": "Fenced code blocks" + }, + { + "markdown": "```\naaa\n~~~\n```\n", + "html": "
aaa\n~~~\n
\n", + "example": 122, + "start_line": 2022, + "end_line": 2031, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~\naaa\n```\n~~~\n", + "html": "
aaa\n```\n
\n", + "example": 123, + "start_line": 2034, + "end_line": 2043, + "section": "Fenced code blocks" + }, + { + "markdown": "````\naaa\n```\n``````\n", + "html": "
aaa\n```\n
\n", + "example": 124, + "start_line": 2048, + "end_line": 2057, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~~\naaa\n~~~\n~~~~\n", + "html": "
aaa\n~~~\n
\n", + "example": 125, + "start_line": 2060, + "end_line": 2069, + "section": "Fenced code blocks" + }, + { + "markdown": "```\n", + "html": "
\n", + "example": 126, + "start_line": 2075, + "end_line": 2079, + "section": "Fenced code blocks" + }, + { + "markdown": "`````\n\n```\naaa\n", + "html": "
\n```\naaa\n
\n", + "example": 127, + "start_line": 2082, + "end_line": 2092, + "section": "Fenced code blocks" + }, + { + "markdown": "> ```\n> aaa\n\nbbb\n", + "html": "
\n
aaa\n
\n
\n

bbb

\n", + "example": 128, + "start_line": 2095, + "end_line": 2106, + "section": "Fenced code blocks" + }, + { + "markdown": "```\n\n \n```\n", + "html": "
\n  \n
\n", + "example": 129, + "start_line": 2111, + "end_line": 2120, + "section": "Fenced code blocks" + }, + { + "markdown": "```\n```\n", + "html": "
\n", + "example": 130, + "start_line": 2125, + "end_line": 2130, + "section": "Fenced code blocks" + }, + { + "markdown": " ```\n aaa\naaa\n```\n", + "html": "
aaa\naaa\n
\n", + "example": 131, + "start_line": 2137, + "end_line": 2146, + "section": "Fenced code blocks" + }, + { + "markdown": " ```\naaa\n aaa\naaa\n ```\n", + "html": "
aaa\naaa\naaa\n
\n", + "example": 132, + "start_line": 2149, + "end_line": 2160, + "section": "Fenced code blocks" + }, + { + "markdown": " ```\n aaa\n aaa\n aaa\n ```\n", + "html": "
aaa\n aaa\naaa\n
\n", + "example": 133, + "start_line": 2163, + "end_line": 2174, + "section": "Fenced code blocks" + }, + { + "markdown": " ```\n aaa\n ```\n", + "html": "
```\naaa\n```\n
\n", + "example": 134, + "start_line": 2179, + "end_line": 2188, + "section": "Fenced code blocks" + }, + { + "markdown": "```\naaa\n ```\n", + "html": "
aaa\n
\n", + "example": 135, + "start_line": 2194, + "end_line": 2201, + "section": "Fenced code blocks" + }, + { + "markdown": " ```\naaa\n ```\n", + "html": "
aaa\n
\n", + "example": 136, + "start_line": 2204, + "end_line": 2211, + "section": "Fenced code blocks" + }, + { + "markdown": "```\naaa\n ```\n", + "html": "
aaa\n    ```\n
\n", + "example": 137, + "start_line": 2216, + "end_line": 2224, + "section": "Fenced code blocks" + }, + { + "markdown": "``` ```\naaa\n", + "html": "

\naaa

\n", + "example": 138, + "start_line": 2230, + "end_line": 2236, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~~~~\naaa\n~~~ ~~\n", + "html": "
aaa\n~~~ ~~\n
\n", + "example": 139, + "start_line": 2239, + "end_line": 2247, + "section": "Fenced code blocks" + }, + { + "markdown": "foo\n```\nbar\n```\nbaz\n", + "html": "

foo

\n
bar\n
\n

baz

\n", + "example": 140, + "start_line": 2253, + "end_line": 2264, + "section": "Fenced code blocks" + }, + { + "markdown": "foo\n---\n~~~\nbar\n~~~\n# baz\n", + "html": "

foo

\n
bar\n
\n

baz

\n", + "example": 141, + "start_line": 2270, + "end_line": 2282, + "section": "Fenced code blocks" + }, + { + "markdown": "```ruby\ndef foo(x)\n return 3\nend\n```\n", + "html": "
def foo(x)\n  return 3\nend\n
\n", + "example": 142, + "start_line": 2292, + "end_line": 2303, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n", + "html": "
def foo(x)\n  return 3\nend\n
\n", + "example": 143, + "start_line": 2306, + "end_line": 2317, + "section": "Fenced code blocks" + }, + { + "markdown": "````;\n````\n", + "html": "
\n", + "example": 144, + "start_line": 2320, + "end_line": 2325, + "section": "Fenced code blocks" + }, + { + "markdown": "``` aa ```\nfoo\n", + "html": "

aa\nfoo

\n", + "example": 145, + "start_line": 2330, + "end_line": 2336, + "section": "Fenced code blocks" + }, + { + "markdown": "~~~ aa ``` ~~~\nfoo\n~~~\n", + "html": "
foo\n
\n", + "example": 146, + "start_line": 2341, + "end_line": 2348, + "section": "Fenced code blocks" + }, + { + "markdown": "```\n``` aaa\n```\n", + "html": "
``` aaa\n
\n", + "example": 147, + "start_line": 2353, + "end_line": 2360, + "section": "Fenced code blocks" + }, + { + "markdown": "
\n
\n**Hello**,\n\n_world_.\n
\n
\n", + "html": "
\n
\n**Hello**,\n

world.\n

\n
\n", + "example": 148, + "start_line": 2432, + "end_line": 2447, + "section": "HTML blocks" + }, + { + "markdown": "\n \n \n \n
\n hi\n
\n\nokay.\n", + "html": "\n \n \n \n
\n hi\n
\n

okay.

\n", + "example": 149, + "start_line": 2461, + "end_line": 2480, + "section": "HTML blocks" + }, + { + "markdown": "
\n*foo*\n", + "example": 151, + "start_line": 2496, + "end_line": 2502, + "section": "HTML blocks" + }, + { + "markdown": "
\n\n*Markdown*\n\n
\n", + "html": "
\n

Markdown

\n
\n", + "example": 152, + "start_line": 2507, + "end_line": 2517, + "section": "HTML blocks" + }, + { + "markdown": "
\n
\n", + "html": "
\n
\n", + "example": 153, + "start_line": 2523, + "end_line": 2531, + "section": "HTML blocks" + }, + { + "markdown": "
\n
\n", + "html": "
\n
\n", + "example": 154, + "start_line": 2534, + "end_line": 2542, + "section": "HTML blocks" + }, + { + "markdown": "
\n*foo*\n\n*bar*\n", + "html": "
\n*foo*\n

bar

\n", + "example": 155, + "start_line": 2546, + "end_line": 2555, + "section": "HTML blocks" + }, + { + "markdown": "
\n", + "html": "\n", + "example": 159, + "start_line": 2595, + "end_line": 2599, + "section": "HTML blocks" + }, + { + "markdown": "
\nfoo\n
\n", + "html": "
\nfoo\n
\n", + "example": 160, + "start_line": 2602, + "end_line": 2610, + "section": "HTML blocks" + }, + { + "markdown": "
\n``` c\nint x = 33;\n```\n", + "html": "
\n``` c\nint x = 33;\n```\n", + "example": 161, + "start_line": 2619, + "end_line": 2629, + "section": "HTML blocks" + }, + { + "markdown": "\n*bar*\n\n", + "html": "\n*bar*\n\n", + "example": 162, + "start_line": 2636, + "end_line": 2644, + "section": "HTML blocks" + }, + { + "markdown": "\n*bar*\n\n", + "html": "\n*bar*\n\n", + "example": 163, + "start_line": 2649, + "end_line": 2657, + "section": "HTML blocks" + }, + { + "markdown": "\n*bar*\n\n", + "html": "\n*bar*\n\n", + "example": 164, + "start_line": 2660, + "end_line": 2668, + "section": "HTML blocks" + }, + { + "markdown": "\n*bar*\n", + "html": "\n*bar*\n", + "example": 165, + "start_line": 2671, + "end_line": 2677, + "section": "HTML blocks" + }, + { + "markdown": "\n*foo*\n\n", + "html": "\n*foo*\n\n", + "example": 166, + "start_line": 2686, + "end_line": 2694, + "section": "HTML blocks" + }, + { + "markdown": "\n\n*foo*\n\n\n", + "html": "\n

foo

\n
\n", + "example": 167, + "start_line": 2701, + "end_line": 2711, + "section": "HTML blocks" + }, + { + "markdown": "*foo*\n", + "html": "

foo

\n", + "example": 168, + "start_line": 2719, + "end_line": 2723, + "section": "HTML blocks" + }, + { + "markdown": "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\nokay\n", + "html": "
\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n
\n

okay

\n", + "example": 169, + "start_line": 2735, + "end_line": 2751, + "section": "HTML blocks" + }, + { + "markdown": "\nokay\n", + "html": "\n

okay

\n", + "example": 170, + "start_line": 2756, + "end_line": 2770, + "section": "HTML blocks" + }, + { + "markdown": "\n", + "html": "\n", + "example": 171, + "start_line": 2775, + "end_line": 2791, + "section": "HTML blocks" + }, + { + "markdown": "\nh1 {color:red;}\n\np {color:blue;}\n\nokay\n", + "html": "\nh1 {color:red;}\n\np {color:blue;}\n\n

okay

\n", + "example": 172, + "start_line": 2795, + "end_line": 2811, + "section": "HTML blocks" + }, + { + "markdown": "\n\nfoo\n", + "html": "\n\nfoo\n", + "example": 173, + "start_line": 2818, + "end_line": 2828, + "section": "HTML blocks" + }, + { + "markdown": ">
\n> foo\n\nbar\n", + "html": "
\n
\nfoo\n
\n

bar

\n", + "example": 174, + "start_line": 2831, + "end_line": 2842, + "section": "HTML blocks" + }, + { + "markdown": "-
\n- foo\n", + "html": "
    \n
  • \n
    \n
  • \n
  • foo
  • \n
\n", + "example": 175, + "start_line": 2845, + "end_line": 2855, + "section": "HTML blocks" + }, + { + "markdown": "\n*foo*\n", + "html": "\n

foo

\n", + "example": 176, + "start_line": 2860, + "end_line": 2866, + "section": "HTML blocks" + }, + { + "markdown": "*bar*\n*baz*\n", + "html": "*bar*\n

baz

\n", + "example": 177, + "start_line": 2869, + "end_line": 2875, + "section": "HTML blocks" + }, + { + "markdown": "1. *bar*\n", + "html": "1. *bar*\n", + "example": 178, + "start_line": 2881, + "end_line": 2889, + "section": "HTML blocks" + }, + { + "markdown": "\nokay\n", + "html": "\n

okay

\n", + "example": 179, + "start_line": 2894, + "end_line": 2906, + "section": "HTML blocks" + }, + { + "markdown": "';\n\n?>\nokay\n", + "html": "';\n\n?>\n

okay

\n", + "example": 180, + "start_line": 2912, + "end_line": 2926, + "section": "HTML blocks" + }, + { + "markdown": "\n", + "html": "\n", + "example": 181, + "start_line": 2931, + "end_line": 2935, + "section": "HTML blocks" + }, + { + "markdown": "\nokay\n", + "html": "\n

okay

\n", + "example": 182, + "start_line": 2940, + "end_line": 2968, + "section": "HTML blocks" + }, + { + "markdown": " \n\n \n", + "html": " \n
<!-- foo -->\n
\n", + "example": 183, + "start_line": 2974, + "end_line": 2982, + "section": "HTML blocks" + }, + { + "markdown": "
\n\n
\n", + "html": "
\n
<div>\n
\n", + "example": 184, + "start_line": 2985, + "end_line": 2993, + "section": "HTML blocks" + }, + { + "markdown": "Foo\n
\nbar\n
\n", + "html": "

Foo

\n
\nbar\n
\n", + "example": 185, + "start_line": 2999, + "end_line": 3009, + "section": "HTML blocks" + }, + { + "markdown": "
\nbar\n
\n*foo*\n", + "html": "
\nbar\n
\n*foo*\n", + "example": 186, + "start_line": 3016, + "end_line": 3026, + "section": "HTML blocks" + }, + { + "markdown": "Foo\n\nbaz\n", + "html": "

Foo\n\nbaz

\n", + "example": 187, + "start_line": 3031, + "end_line": 3039, + "section": "HTML blocks" + }, + { + "markdown": "
\n\n*Emphasized* text.\n\n
\n", + "html": "
\n

Emphasized text.

\n
\n", + "example": 188, + "start_line": 3072, + "end_line": 3082, + "section": "HTML blocks" + }, + { + "markdown": "
\n*Emphasized* text.\n
\n", + "html": "
\n*Emphasized* text.\n
\n", + "example": 189, + "start_line": 3085, + "end_line": 3093, + "section": "HTML blocks" + }, + { + "markdown": "\n\n\n\n\n\n\n\n
\nHi\n
\n", + "html": "\n\n\n\n
\nHi\n
\n", + "example": 190, + "start_line": 3107, + "end_line": 3127, + "section": "HTML blocks" + }, + { + "markdown": "\n\n \n\n \n\n \n\n
\n Hi\n
\n", + "html": "\n \n
<td>\n  Hi\n</td>\n
\n \n
\n", + "example": 191, + "start_line": 3134, + "end_line": 3155, + "section": "HTML blocks" + }, + { + "markdown": "[foo]: /url \"title\"\n\n[foo]\n", + "html": "

foo

\n", + "example": 192, + "start_line": 3183, + "end_line": 3189, + "section": "Link reference definitions" + }, + { + "markdown": " [foo]: \n /url \n 'the title' \n\n[foo]\n", + "html": "

foo

\n", + "example": 193, + "start_line": 3192, + "end_line": 3200, + "section": "Link reference definitions" + }, + { + "markdown": "[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n", + "html": "

Foo*bar]

\n", + "example": 194, + "start_line": 3203, + "end_line": 3209, + "section": "Link reference definitions" + }, + { + "markdown": "[Foo bar]:\n\n'title'\n\n[Foo bar]\n", + "html": "

Foo bar

\n", + "example": 195, + "start_line": 3212, + "end_line": 3220, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n", + "html": "

foo

\n", + "example": 196, + "start_line": 3225, + "end_line": 3239, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n", + "html": "

[foo]: /url 'title

\n

with blank line'

\n

[foo]

\n", + "example": 197, + "start_line": 3244, + "end_line": 3254, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]:\n/url\n\n[foo]\n", + "html": "

foo

\n", + "example": 198, + "start_line": 3259, + "end_line": 3266, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]:\n\n[foo]\n", + "html": "

[foo]:

\n

[foo]

\n", + "example": 199, + "start_line": 3271, + "end_line": 3278, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: <>\n\n[foo]\n", + "html": "

foo

\n", + "example": 200, + "start_line": 3283, + "end_line": 3289, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: (baz)\n\n[foo]\n", + "html": "

[foo]: (baz)

\n

[foo]

\n", + "example": 201, + "start_line": 3294, + "end_line": 3301, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n", + "html": "

foo

\n", + "example": 202, + "start_line": 3307, + "end_line": 3313, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]\n\n[foo]: url\n", + "html": "

foo

\n", + "example": 203, + "start_line": 3318, + "end_line": 3324, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]\n\n[foo]: first\n[foo]: second\n", + "html": "

foo

\n", + "example": 204, + "start_line": 3330, + "end_line": 3337, + "section": "Link reference definitions" + }, + { + "markdown": "[FOO]: /url\n\n[Foo]\n", + "html": "

Foo

\n", + "example": 205, + "start_line": 3343, + "end_line": 3349, + "section": "Link reference definitions" + }, + { + "markdown": "[ΑΓΩ]: /φου\n\n[αγω]\n", + "html": "

αγω

\n", + "example": 206, + "start_line": 3352, + "end_line": 3358, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\n", + "html": "", + "example": 207, + "start_line": 3367, + "end_line": 3370, + "section": "Link reference definitions" + }, + { + "markdown": "[\nfoo\n]: /url\nbar\n", + "html": "

bar

\n", + "example": 208, + "start_line": 3375, + "end_line": 3382, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url \"title\" ok\n", + "html": "

[foo]: /url "title" ok

\n", + "example": 209, + "start_line": 3388, + "end_line": 3392, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\n\"title\" ok\n", + "html": "

"title" ok

\n", + "example": 210, + "start_line": 3397, + "end_line": 3402, + "section": "Link reference definitions" + }, + { + "markdown": " [foo]: /url \"title\"\n\n[foo]\n", + "html": "
[foo]: /url "title"\n
\n

[foo]

\n", + "example": 211, + "start_line": 3408, + "end_line": 3416, + "section": "Link reference definitions" + }, + { + "markdown": "```\n[foo]: /url\n```\n\n[foo]\n", + "html": "
[foo]: /url\n
\n

[foo]

\n", + "example": 212, + "start_line": 3422, + "end_line": 3432, + "section": "Link reference definitions" + }, + { + "markdown": "Foo\n[bar]: /baz\n\n[bar]\n", + "html": "

Foo\n[bar]: /baz

\n

[bar]

\n", + "example": 213, + "start_line": 3437, + "end_line": 3446, + "section": "Link reference definitions" + }, + { + "markdown": "# [Foo]\n[foo]: /url\n> bar\n", + "html": "

Foo

\n
\n

bar

\n
\n", + "example": 214, + "start_line": 3452, + "end_line": 3461, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\nbar\n===\n[foo]\n", + "html": "

bar

\n

foo

\n", + "example": 215, + "start_line": 3463, + "end_line": 3471, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /url\n===\n[foo]\n", + "html": "

===\nfoo

\n", + "example": 216, + "start_line": 3473, + "end_line": 3480, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n", + "html": "

foo,\nbar,\nbaz

\n", + "example": 217, + "start_line": 3486, + "end_line": 3499, + "section": "Link reference definitions" + }, + { + "markdown": "[foo]\n\n> [foo]: /url\n", + "html": "

foo

\n
\n
\n", + "example": 218, + "start_line": 3507, + "end_line": 3515, + "section": "Link reference definitions" + }, + { + "markdown": "aaa\n\nbbb\n", + "html": "

aaa

\n

bbb

\n", + "example": 219, + "start_line": 3529, + "end_line": 3536, + "section": "Paragraphs" + }, + { + "markdown": "aaa\nbbb\n\nccc\nddd\n", + "html": "

aaa\nbbb

\n

ccc\nddd

\n", + "example": 220, + "start_line": 3541, + "end_line": 3552, + "section": "Paragraphs" + }, + { + "markdown": "aaa\n\n\nbbb\n", + "html": "

aaa

\n

bbb

\n", + "example": 221, + "start_line": 3557, + "end_line": 3565, + "section": "Paragraphs" + }, + { + "markdown": " aaa\n bbb\n", + "html": "

aaa\nbbb

\n", + "example": 222, + "start_line": 3570, + "end_line": 3576, + "section": "Paragraphs" + }, + { + "markdown": "aaa\n bbb\n ccc\n", + "html": "

aaa\nbbb\nccc

\n", + "example": 223, + "start_line": 3582, + "end_line": 3590, + "section": "Paragraphs" + }, + { + "markdown": " aaa\nbbb\n", + "html": "

aaa\nbbb

\n", + "example": 224, + "start_line": 3596, + "end_line": 3602, + "section": "Paragraphs" + }, + { + "markdown": " aaa\nbbb\n", + "html": "
aaa\n
\n

bbb

\n", + "example": 225, + "start_line": 3605, + "end_line": 3612, + "section": "Paragraphs" + }, + { + "markdown": "aaa \nbbb \n", + "html": "

aaa
\nbbb

\n", + "example": 226, + "start_line": 3619, + "end_line": 3625, + "section": "Paragraphs" + }, + { + "markdown": " \n\naaa\n \n\n# aaa\n\n \n", + "html": "

aaa

\n

aaa

\n", + "example": 227, + "start_line": 3636, + "end_line": 3648, + "section": "Blank lines" + }, + { + "markdown": "> # Foo\n> bar\n> baz\n", + "html": "
\n

Foo

\n

bar\nbaz

\n
\n", + "example": 228, + "start_line": 3704, + "end_line": 3714, + "section": "Block quotes" + }, + { + "markdown": "># Foo\n>bar\n> baz\n", + "html": "
\n

Foo

\n

bar\nbaz

\n
\n", + "example": 229, + "start_line": 3719, + "end_line": 3729, + "section": "Block quotes" + }, + { + "markdown": " > # Foo\n > bar\n > baz\n", + "html": "
\n

Foo

\n

bar\nbaz

\n
\n", + "example": 230, + "start_line": 3734, + "end_line": 3744, + "section": "Block quotes" + }, + { + "markdown": " > # Foo\n > bar\n > baz\n", + "html": "
> # Foo\n> bar\n> baz\n
\n", + "example": 231, + "start_line": 3749, + "end_line": 3758, + "section": "Block quotes" + }, + { + "markdown": "> # Foo\n> bar\nbaz\n", + "html": "
\n

Foo

\n

bar\nbaz

\n
\n", + "example": 232, + "start_line": 3764, + "end_line": 3774, + "section": "Block quotes" + }, + { + "markdown": "> bar\nbaz\n> foo\n", + "html": "
\n

bar\nbaz\nfoo

\n
\n", + "example": 233, + "start_line": 3780, + "end_line": 3790, + "section": "Block quotes" + }, + { + "markdown": "> foo\n---\n", + "html": "
\n

foo

\n
\n
\n", + "example": 234, + "start_line": 3804, + "end_line": 3812, + "section": "Block quotes" + }, + { + "markdown": "> - foo\n- bar\n", + "html": "
\n
    \n
  • foo
  • \n
\n
\n
    \n
  • bar
  • \n
\n", + "example": 235, + "start_line": 3824, + "end_line": 3836, + "section": "Block quotes" + }, + { + "markdown": "> foo\n bar\n", + "html": "
\n
foo\n
\n
\n
bar\n
\n", + "example": 236, + "start_line": 3842, + "end_line": 3852, + "section": "Block quotes" + }, + { + "markdown": "> ```\nfoo\n```\n", + "html": "
\n
\n
\n

foo

\n
\n", + "example": 237, + "start_line": 3855, + "end_line": 3865, + "section": "Block quotes" + }, + { + "markdown": "> foo\n - bar\n", + "html": "
\n

foo\n- bar

\n
\n", + "example": 238, + "start_line": 3871, + "end_line": 3879, + "section": "Block quotes" + }, + { + "markdown": ">\n", + "html": "
\n
\n", + "example": 239, + "start_line": 3895, + "end_line": 3900, + "section": "Block quotes" + }, + { + "markdown": ">\n> \n> \n", + "html": "
\n
\n", + "example": 240, + "start_line": 3903, + "end_line": 3910, + "section": "Block quotes" + }, + { + "markdown": ">\n> foo\n> \n", + "html": "
\n

foo

\n
\n", + "example": 241, + "start_line": 3915, + "end_line": 3923, + "section": "Block quotes" + }, + { + "markdown": "> foo\n\n> bar\n", + "html": "
\n

foo

\n
\n
\n

bar

\n
\n", + "example": 242, + "start_line": 3928, + "end_line": 3939, + "section": "Block quotes" + }, + { + "markdown": "> foo\n> bar\n", + "html": "
\n

foo\nbar

\n
\n", + "example": 243, + "start_line": 3950, + "end_line": 3958, + "section": "Block quotes" + }, + { + "markdown": "> foo\n>\n> bar\n", + "html": "
\n

foo

\n

bar

\n
\n", + "example": 244, + "start_line": 3963, + "end_line": 3972, + "section": "Block quotes" + }, + { + "markdown": "foo\n> bar\n", + "html": "

foo

\n
\n

bar

\n
\n", + "example": 245, + "start_line": 3977, + "end_line": 3985, + "section": "Block quotes" + }, + { + "markdown": "> aaa\n***\n> bbb\n", + "html": "
\n

aaa

\n
\n
\n
\n

bbb

\n
\n", + "example": 246, + "start_line": 3991, + "end_line": 4003, + "section": "Block quotes" + }, + { + "markdown": "> bar\nbaz\n", + "html": "
\n

bar\nbaz

\n
\n", + "example": 247, + "start_line": 4009, + "end_line": 4017, + "section": "Block quotes" + }, + { + "markdown": "> bar\n\nbaz\n", + "html": "
\n

bar

\n
\n

baz

\n", + "example": 248, + "start_line": 4020, + "end_line": 4029, + "section": "Block quotes" + }, + { + "markdown": "> bar\n>\nbaz\n", + "html": "
\n

bar

\n
\n

baz

\n", + "example": 249, + "start_line": 4032, + "end_line": 4041, + "section": "Block quotes" + }, + { + "markdown": "> > > foo\nbar\n", + "html": "
\n
\n
\n

foo\nbar

\n
\n
\n
\n", + "example": 250, + "start_line": 4048, + "end_line": 4060, + "section": "Block quotes" + }, + { + "markdown": ">>> foo\n> bar\n>>baz\n", + "html": "
\n
\n
\n

foo\nbar\nbaz

\n
\n
\n
\n", + "example": 251, + "start_line": 4063, + "end_line": 4077, + "section": "Block quotes" + }, + { + "markdown": "> code\n\n> not code\n", + "html": "
\n
code\n
\n
\n
\n

not code

\n
\n", + "example": 252, + "start_line": 4085, + "end_line": 4097, + "section": "Block quotes" + }, + { + "markdown": "A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n", + "html": "

A paragraph\nwith two lines.

\n
indented code\n
\n
\n

A block quote.

\n
\n", + "example": 253, + "start_line": 4139, + "end_line": 4154, + "section": "List items" + }, + { + "markdown": "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + "example": 254, + "start_line": 4161, + "end_line": 4180, + "section": "List items" + }, + { + "markdown": "- one\n\n two\n", + "html": "
    \n
  • one
  • \n
\n

two

\n", + "example": 255, + "start_line": 4194, + "end_line": 4203, + "section": "List items" + }, + { + "markdown": "- one\n\n two\n", + "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", + "example": 256, + "start_line": 4206, + "end_line": 4217, + "section": "List items" + }, + { + "markdown": " - one\n\n two\n", + "html": "
    \n
  • one
  • \n
\n
 two\n
\n", + "example": 257, + "start_line": 4220, + "end_line": 4230, + "section": "List items" + }, + { + "markdown": " - one\n\n two\n", + "html": "
    \n
  • \n

    one

    \n

    two

    \n
  • \n
\n", + "example": 258, + "start_line": 4233, + "end_line": 4244, + "section": "List items" + }, + { + "markdown": " > > 1. one\n>>\n>> two\n", + "html": "
\n
\n
    \n
  1. \n

    one

    \n

    two

    \n
  2. \n
\n
\n
\n", + "example": 259, + "start_line": 4255, + "end_line": 4270, + "section": "List items" + }, + { + "markdown": ">>- one\n>>\n > > two\n", + "html": "
\n
\n
    \n
  • one
  • \n
\n

two

\n
\n
\n", + "example": 260, + "start_line": 4282, + "end_line": 4295, + "section": "List items" + }, + { + "markdown": "-one\n\n2.two\n", + "html": "

-one

\n

2.two

\n", + "example": 261, + "start_line": 4301, + "end_line": 4308, + "section": "List items" + }, + { + "markdown": "- foo\n\n\n bar\n", + "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", + "example": 262, + "start_line": 4314, + "end_line": 4326, + "section": "List items" + }, + { + "markdown": "1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n", + "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n

    baz

    \n
    \n

    bam

    \n
    \n
  2. \n
\n", + "example": 263, + "start_line": 4331, + "end_line": 4353, + "section": "List items" + }, + { + "markdown": "- Foo\n\n bar\n\n\n baz\n", + "html": "
    \n
  • \n

    Foo

    \n
    bar\n\n\nbaz\n
    \n
  • \n
\n", + "example": 264, + "start_line": 4359, + "end_line": 4377, + "section": "List items" + }, + { + "markdown": "123456789. ok\n", + "html": "
    \n
  1. ok
  2. \n
\n", + "example": 265, + "start_line": 4381, + "end_line": 4387, + "section": "List items" + }, + { + "markdown": "1234567890. not ok\n", + "html": "

1234567890. not ok

\n", + "example": 266, + "start_line": 4390, + "end_line": 4394, + "section": "List items" + }, + { + "markdown": "0. ok\n", + "html": "
    \n
  1. ok
  2. \n
\n", + "example": 267, + "start_line": 4399, + "end_line": 4405, + "section": "List items" + }, + { + "markdown": "003. ok\n", + "html": "
    \n
  1. ok
  2. \n
\n", + "example": 268, + "start_line": 4408, + "end_line": 4414, + "section": "List items" + }, + { + "markdown": "-1. not ok\n", + "html": "

-1. not ok

\n", + "example": 269, + "start_line": 4419, + "end_line": 4423, + "section": "List items" + }, + { + "markdown": "- foo\n\n bar\n", + "html": "
    \n
  • \n

    foo

    \n
    bar\n
    \n
  • \n
\n", + "example": 270, + "start_line": 4442, + "end_line": 4454, + "section": "List items" + }, + { + "markdown": " 10. foo\n\n bar\n", + "html": "
    \n
  1. \n

    foo

    \n
    bar\n
    \n
  2. \n
\n", + "example": 271, + "start_line": 4459, + "end_line": 4471, + "section": "List items" + }, + { + "markdown": " indented code\n\nparagraph\n\n more code\n", + "html": "
indented code\n
\n

paragraph

\n
more code\n
\n", + "example": 272, + "start_line": 4478, + "end_line": 4490, + "section": "List items" + }, + { + "markdown": "1. indented code\n\n paragraph\n\n more code\n", + "html": "
    \n
  1. \n
    indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", + "example": 273, + "start_line": 4493, + "end_line": 4509, + "section": "List items" + }, + { + "markdown": "1. indented code\n\n paragraph\n\n more code\n", + "html": "
    \n
  1. \n
     indented code\n
    \n

    paragraph

    \n
    more code\n
    \n
  2. \n
\n", + "example": 274, + "start_line": 4515, + "end_line": 4531, + "section": "List items" + }, + { + "markdown": " foo\n\nbar\n", + "html": "

foo

\n

bar

\n", + "example": 275, + "start_line": 4542, + "end_line": 4549, + "section": "List items" + }, + { + "markdown": "- foo\n\n bar\n", + "html": "
    \n
  • foo
  • \n
\n

bar

\n", + "example": 276, + "start_line": 4552, + "end_line": 4561, + "section": "List items" + }, + { + "markdown": "- foo\n\n bar\n", + "html": "
    \n
  • \n

    foo

    \n

    bar

    \n
  • \n
\n", + "example": 277, + "start_line": 4569, + "end_line": 4580, + "section": "List items" + }, + { + "markdown": "-\n foo\n-\n ```\n bar\n ```\n-\n baz\n", + "html": "
    \n
  • foo
  • \n
  • \n
    bar\n
    \n
  • \n
  • \n
    baz\n
    \n
  • \n
\n", + "example": 278, + "start_line": 4596, + "end_line": 4617, + "section": "List items" + }, + { + "markdown": "- \n foo\n", + "html": "
    \n
  • foo
  • \n
\n", + "example": 279, + "start_line": 4622, + "end_line": 4629, + "section": "List items" + }, + { + "markdown": "-\n\n foo\n", + "html": "
    \n
  • \n
\n

foo

\n", + "example": 280, + "start_line": 4636, + "end_line": 4645, + "section": "List items" + }, + { + "markdown": "- foo\n-\n- bar\n", + "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", + "example": 281, + "start_line": 4650, + "end_line": 4660, + "section": "List items" + }, + { + "markdown": "- foo\n- \n- bar\n", + "html": "
    \n
  • foo
  • \n
  • \n
  • bar
  • \n
\n", + "example": 282, + "start_line": 4665, + "end_line": 4675, + "section": "List items" + }, + { + "markdown": "1. foo\n2.\n3. bar\n", + "html": "
    \n
  1. foo
  2. \n
  3. \n
  4. bar
  5. \n
\n", + "example": 283, + "start_line": 4680, + "end_line": 4690, + "section": "List items" + }, + { + "markdown": "*\n", + "html": "
    \n
  • \n
\n", + "example": 284, + "start_line": 4695, + "end_line": 4701, + "section": "List items" + }, + { + "markdown": "foo\n*\n\nfoo\n1.\n", + "html": "

foo\n*

\n

foo\n1.

\n", + "example": 285, + "start_line": 4705, + "end_line": 4716, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + "example": 286, + "start_line": 4727, + "end_line": 4746, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + "example": 287, + "start_line": 4751, + "end_line": 4770, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + "example": 288, + "start_line": 4775, + "end_line": 4794, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
1.  A paragraph\n    with two lines.\n\n        indented code\n\n    > A block quote.\n
\n", + "example": 289, + "start_line": 4799, + "end_line": 4814, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n", + "html": "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + "example": 290, + "start_line": 4829, + "end_line": 4848, + "section": "List items" + }, + { + "markdown": " 1. A paragraph\n with two lines.\n", + "html": "
    \n
  1. A paragraph\nwith two lines.
  2. \n
\n", + "example": 291, + "start_line": 4853, + "end_line": 4861, + "section": "List items" + }, + { + "markdown": "> 1. > Blockquote\ncontinued here.\n", + "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", + "example": 292, + "start_line": 4866, + "end_line": 4880, + "section": "List items" + }, + { + "markdown": "> 1. > Blockquote\n> continued here.\n", + "html": "
\n
    \n
  1. \n
    \n

    Blockquote\ncontinued here.

    \n
    \n
  2. \n
\n
\n", + "example": 293, + "start_line": 4883, + "end_line": 4897, + "section": "List items" + }, + { + "markdown": "- foo\n - bar\n - baz\n - boo\n", + "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz\n
          \n
        • boo
        • \n
        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", + "example": 294, + "start_line": 4911, + "end_line": 4932, + "section": "List items" + }, + { + "markdown": "- foo\n - bar\n - baz\n - boo\n", + "html": "
    \n
  • foo
  • \n
  • bar
  • \n
  • baz
  • \n
  • boo
  • \n
\n", + "example": 295, + "start_line": 4937, + "end_line": 4949, + "section": "List items" + }, + { + "markdown": "10) foo\n - bar\n", + "html": "
    \n
  1. foo\n
      \n
    • bar
    • \n
    \n
  2. \n
\n", + "example": 296, + "start_line": 4954, + "end_line": 4965, + "section": "List items" + }, + { + "markdown": "10) foo\n - bar\n", + "html": "
    \n
  1. foo
  2. \n
\n
    \n
  • bar
  • \n
\n", + "example": 297, + "start_line": 4970, + "end_line": 4980, + "section": "List items" + }, + { + "markdown": "- - foo\n", + "html": "
    \n
  • \n
      \n
    • foo
    • \n
    \n
  • \n
\n", + "example": 298, + "start_line": 4985, + "end_line": 4995, + "section": "List items" + }, + { + "markdown": "1. - 2. foo\n", + "html": "
    \n
  1. \n
      \n
    • \n
        \n
      1. foo
      2. \n
      \n
    • \n
    \n
  2. \n
\n", + "example": 299, + "start_line": 4998, + "end_line": 5012, + "section": "List items" + }, + { + "markdown": "- # Foo\n- Bar\n ---\n baz\n", + "html": "
    \n
  • \n

    Foo

    \n
  • \n
  • \n

    Bar

    \nbaz
  • \n
\n", + "example": 300, + "start_line": 5017, + "end_line": 5031, + "section": "List items" + }, + { + "markdown": "- foo\n- bar\n+ baz\n", + "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n
    \n
  • baz
  • \n
\n", + "example": 301, + "start_line": 5253, + "end_line": 5265, + "section": "Lists" + }, + { + "markdown": "1. foo\n2. bar\n3) baz\n", + "html": "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
\n
    \n
  1. baz
  2. \n
\n", + "example": 302, + "start_line": 5268, + "end_line": 5280, + "section": "Lists" + }, + { + "markdown": "Foo\n- bar\n- baz\n", + "html": "

Foo

\n
    \n
  • bar
  • \n
  • baz
  • \n
\n", + "example": 303, + "start_line": 5287, + "end_line": 5297, + "section": "Lists" + }, + { + "markdown": "The number of windows in my house is\n14. The number of doors is 6.\n", + "html": "

The number of windows in my house is\n14. The number of doors is 6.

\n", + "example": 304, + "start_line": 5364, + "end_line": 5370, + "section": "Lists" + }, + { + "markdown": "The number of windows in my house is\n1. The number of doors is 6.\n", + "html": "

The number of windows in my house is

\n
    \n
  1. The number of doors is 6.
  2. \n
\n", + "example": 305, + "start_line": 5374, + "end_line": 5382, + "section": "Lists" + }, + { + "markdown": "- foo\n\n- bar\n\n\n- baz\n", + "html": "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
  • \n

    baz

    \n
  • \n
\n", + "example": 306, + "start_line": 5388, + "end_line": 5407, + "section": "Lists" + }, + { + "markdown": "- foo\n - bar\n - baz\n\n\n bim\n", + "html": "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • \n

        baz

        \n

        bim

        \n
      • \n
      \n
    • \n
    \n
  • \n
\n", + "example": 307, + "start_line": 5409, + "end_line": 5431, + "section": "Lists" + }, + { + "markdown": "- foo\n- bar\n\n\n\n- baz\n- bim\n", + "html": "
    \n
  • foo
  • \n
  • bar
  • \n
\n\n
    \n
  • baz
  • \n
  • bim
  • \n
\n", + "example": 308, + "start_line": 5439, + "end_line": 5457, + "section": "Lists" + }, + { + "markdown": "- foo\n\n notcode\n\n- foo\n\n\n\n code\n", + "html": "
    \n
  • \n

    foo

    \n

    notcode

    \n
  • \n
  • \n

    foo

    \n
  • \n
\n\n
code\n
\n", + "example": 309, + "start_line": 5460, + "end_line": 5483, + "section": "Lists" + }, + { + "markdown": "- a\n - b\n - c\n - d\n - e\n - f\n- g\n", + "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d
  • \n
  • e
  • \n
  • f
  • \n
  • g
  • \n
\n", + "example": 310, + "start_line": 5491, + "end_line": 5509, + "section": "Lists" + }, + { + "markdown": "1. a\n\n 2. b\n\n 3. c\n", + "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
  5. \n

    c

    \n
  6. \n
\n", + "example": 311, + "start_line": 5512, + "end_line": 5530, + "section": "Lists" + }, + { + "markdown": "- a\n - b\n - c\n - d\n - e\n", + "html": "
    \n
  • a
  • \n
  • b
  • \n
  • c
  • \n
  • d\n- e
  • \n
\n", + "example": 312, + "start_line": 5536, + "end_line": 5550, + "section": "Lists" + }, + { + "markdown": "1. a\n\n 2. b\n\n 3. c\n", + "html": "
    \n
  1. \n

    a

    \n
  2. \n
  3. \n

    b

    \n
  4. \n
\n
3. c\n
\n", + "example": 313, + "start_line": 5556, + "end_line": 5573, + "section": "Lists" + }, + { + "markdown": "- a\n- b\n\n- c\n", + "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    c

    \n
  • \n
\n", + "example": 314, + "start_line": 5579, + "end_line": 5596, + "section": "Lists" + }, + { + "markdown": "* a\n*\n\n* c\n", + "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n
  • \n

    c

    \n
  • \n
\n", + "example": 315, + "start_line": 5601, + "end_line": 5616, + "section": "Lists" + }, + { + "markdown": "- a\n- b\n\n c\n- d\n", + "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n

    c

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", + "example": 316, + "start_line": 5623, + "end_line": 5642, + "section": "Lists" + }, + { + "markdown": "- a\n- b\n\n [ref]: /url\n- d\n", + "html": "
    \n
  • \n

    a

    \n
  • \n
  • \n

    b

    \n
  • \n
  • \n

    d

    \n
  • \n
\n", + "example": 317, + "start_line": 5645, + "end_line": 5663, + "section": "Lists" + }, + { + "markdown": "- a\n- ```\n b\n\n\n ```\n- c\n", + "html": "
    \n
  • a
  • \n
  • \n
    b\n\n\n
    \n
  • \n
  • c
  • \n
\n", + "example": 318, + "start_line": 5668, + "end_line": 5687, + "section": "Lists" + }, + { + "markdown": "- a\n - b\n\n c\n- d\n", + "html": "
    \n
  • a\n
      \n
    • \n

      b

      \n

      c

      \n
    • \n
    \n
  • \n
  • d
  • \n
\n", + "example": 319, + "start_line": 5694, + "end_line": 5712, + "section": "Lists" + }, + { + "markdown": "* a\n > b\n >\n* c\n", + "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
  • \n
  • c
  • \n
\n", + "example": 320, + "start_line": 5718, + "end_line": 5732, + "section": "Lists" + }, + { + "markdown": "- a\n > b\n ```\n c\n ```\n- d\n", + "html": "
    \n
  • a\n
    \n

    b

    \n
    \n
    c\n
    \n
  • \n
  • d
  • \n
\n", + "example": 321, + "start_line": 5738, + "end_line": 5756, + "section": "Lists" + }, + { + "markdown": "- a\n", + "html": "
    \n
  • a
  • \n
\n", + "example": 322, + "start_line": 5761, + "end_line": 5767, + "section": "Lists" + }, + { + "markdown": "- a\n - b\n", + "html": "
    \n
  • a\n
      \n
    • b
    • \n
    \n
  • \n
\n", + "example": 323, + "start_line": 5770, + "end_line": 5781, + "section": "Lists" + }, + { + "markdown": "1. ```\n foo\n ```\n\n bar\n", + "html": "
    \n
  1. \n
    foo\n
    \n

    bar

    \n
  2. \n
\n", + "example": 324, + "start_line": 5787, + "end_line": 5801, + "section": "Lists" + }, + { + "markdown": "* foo\n * bar\n\n baz\n", + "html": "
    \n
  • \n

    foo

    \n
      \n
    • bar
    • \n
    \n

    baz

    \n
  • \n
\n", + "example": 325, + "start_line": 5806, + "end_line": 5821, + "section": "Lists" + }, + { + "markdown": "- a\n - b\n - c\n\n- d\n - e\n - f\n", + "html": "
    \n
  • \n

    a

    \n
      \n
    • b
    • \n
    • c
    • \n
    \n
  • \n
  • \n

    d

    \n
      \n
    • e
    • \n
    • f
    • \n
    \n
  • \n
\n", + "example": 326, + "start_line": 5824, + "end_line": 5849, + "section": "Lists" + }, + { + "markdown": "`hi`lo`\n", + "html": "

hilo`

\n", + "example": 327, + "start_line": 5858, + "end_line": 5862, + "section": "Inlines" + }, + { + "markdown": "`foo`\n", + "html": "

foo

\n", + "example": 328, + "start_line": 5890, + "end_line": 5894, + "section": "Code spans" + }, + { + "markdown": "`` foo ` bar ``\n", + "html": "

foo ` bar

\n", + "example": 329, + "start_line": 5901, + "end_line": 5905, + "section": "Code spans" + }, + { + "markdown": "` `` `\n", + "html": "

``

\n", + "example": 330, + "start_line": 5911, + "end_line": 5915, + "section": "Code spans" + }, + { + "markdown": "` `` `\n", + "html": "

``

\n", + "example": 331, + "start_line": 5919, + "end_line": 5923, + "section": "Code spans" + }, + { + "markdown": "` a`\n", + "html": "

a

\n", + "example": 332, + "start_line": 5928, + "end_line": 5932, + "section": "Code spans" + }, + { + "markdown": "` b `\n", + "html": "

 b 

\n", + "example": 333, + "start_line": 5937, + "end_line": 5941, + "section": "Code spans" + }, + { + "markdown": "` `\n` `\n", + "html": "

 \n

\n", + "example": 334, + "start_line": 5945, + "end_line": 5951, + "section": "Code spans" + }, + { + "markdown": "``\nfoo\nbar \nbaz\n``\n", + "html": "

foo bar baz

\n", + "example": 335, + "start_line": 5956, + "end_line": 5964, + "section": "Code spans" + }, + { + "markdown": "``\nfoo \n``\n", + "html": "

foo

\n", + "example": 336, + "start_line": 5966, + "end_line": 5972, + "section": "Code spans" + }, + { + "markdown": "`foo bar \nbaz`\n", + "html": "

foo bar baz

\n", + "example": 337, + "start_line": 5977, + "end_line": 5982, + "section": "Code spans" + }, + { + "markdown": "`foo\\`bar`\n", + "html": "

foo\\bar`

\n", + "example": 338, + "start_line": 5994, + "end_line": 5998, + "section": "Code spans" + }, + { + "markdown": "``foo`bar``\n", + "html": "

foo`bar

\n", + "example": 339, + "start_line": 6005, + "end_line": 6009, + "section": "Code spans" + }, + { + "markdown": "` foo `` bar `\n", + "html": "

foo `` bar

\n", + "example": 340, + "start_line": 6011, + "end_line": 6015, + "section": "Code spans" + }, + { + "markdown": "*foo`*`\n", + "html": "

*foo*

\n", + "example": 341, + "start_line": 6023, + "end_line": 6027, + "section": "Code spans" + }, + { + "markdown": "[not a `link](/foo`)\n", + "html": "

[not a link](/foo)

\n", + "example": 342, + "start_line": 6032, + "end_line": 6036, + "section": "Code spans" + }, + { + "markdown": "``\n", + "html": "

<a href="">`

\n", + "example": 343, + "start_line": 6042, + "end_line": 6046, + "section": "Code spans" + }, + { + "markdown": "
`\n", + "html": "

`

\n", + "example": 344, + "start_line": 6051, + "end_line": 6055, + "section": "Code spans" + }, + { + "markdown": "``\n", + "html": "

<http://foo.bar.baz>`

\n", + "example": 345, + "start_line": 6060, + "end_line": 6064, + "section": "Code spans" + }, + { + "markdown": "`\n", + "html": "

http://foo.bar.`baz`

\n", + "example": 346, + "start_line": 6069, + "end_line": 6073, + "section": "Code spans" + }, + { + "markdown": "```foo``\n", + "html": "

```foo``

\n", + "example": 347, + "start_line": 6079, + "end_line": 6083, + "section": "Code spans" + }, + { + "markdown": "`foo\n", + "html": "

`foo

\n", + "example": 348, + "start_line": 6086, + "end_line": 6090, + "section": "Code spans" + }, + { + "markdown": "`foo``bar``\n", + "html": "

`foobar

\n", + "example": 349, + "start_line": 6095, + "end_line": 6099, + "section": "Code spans" + }, + { + "markdown": "*foo bar*\n", + "html": "

foo bar

\n", + "example": 350, + "start_line": 6312, + "end_line": 6316, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "a * foo bar*\n", + "html": "

a * foo bar*

\n", + "example": 351, + "start_line": 6322, + "end_line": 6326, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "a*\"foo\"*\n", + "html": "

a*"foo"*

\n", + "example": 352, + "start_line": 6333, + "end_line": 6337, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "* a *\n", + "html": "

* a *

\n", + "example": 353, + "start_line": 6342, + "end_line": 6346, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo*bar*\n", + "html": "

foobar

\n", + "example": 354, + "start_line": 6351, + "end_line": 6355, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "5*6*78\n", + "html": "

5678

\n", + "example": 355, + "start_line": 6358, + "end_line": 6362, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo bar_\n", + "html": "

foo bar

\n", + "example": 356, + "start_line": 6367, + "end_line": 6371, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_ foo bar_\n", + "html": "

_ foo bar_

\n", + "example": 357, + "start_line": 6377, + "end_line": 6381, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "a_\"foo\"_\n", + "html": "

a_"foo"_

\n", + "example": 358, + "start_line": 6387, + "end_line": 6391, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo_bar_\n", + "html": "

foo_bar_

\n", + "example": 359, + "start_line": 6396, + "end_line": 6400, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "5_6_78\n", + "html": "

5_6_78

\n", + "example": 360, + "start_line": 6403, + "end_line": 6407, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "пристаням_стремятся_\n", + "html": "

пристаням_стремятся_

\n", + "example": 361, + "start_line": 6410, + "end_line": 6414, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "aa_\"bb\"_cc\n", + "html": "

aa_"bb"_cc

\n", + "example": 362, + "start_line": 6420, + "end_line": 6424, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo-_(bar)_\n", + "html": "

foo-(bar)

\n", + "example": 363, + "start_line": 6431, + "end_line": 6435, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo*\n", + "html": "

_foo*

\n", + "example": 364, + "start_line": 6443, + "end_line": 6447, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo bar *\n", + "html": "

*foo bar *

\n", + "example": 365, + "start_line": 6453, + "end_line": 6457, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo bar\n*\n", + "html": "

*foo bar\n*

\n", + "example": 366, + "start_line": 6462, + "end_line": 6468, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*(*foo)\n", + "html": "

*(*foo)

\n", + "example": 367, + "start_line": 6475, + "end_line": 6479, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*(*foo*)*\n", + "html": "

(foo)

\n", + "example": 368, + "start_line": 6485, + "end_line": 6489, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo*bar\n", + "html": "

foobar

\n", + "example": 369, + "start_line": 6494, + "end_line": 6498, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo bar _\n", + "html": "

_foo bar _

\n", + "example": 370, + "start_line": 6507, + "end_line": 6511, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_(_foo)\n", + "html": "

_(_foo)

\n", + "example": 371, + "start_line": 6517, + "end_line": 6521, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_(_foo_)_\n", + "html": "

(foo)

\n", + "example": 372, + "start_line": 6526, + "end_line": 6530, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo_bar\n", + "html": "

_foo_bar

\n", + "example": 373, + "start_line": 6535, + "end_line": 6539, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_пристаням_стремятся\n", + "html": "

_пристаням_стремятся

\n", + "example": 374, + "start_line": 6542, + "end_line": 6546, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo_bar_baz_\n", + "html": "

foo_bar_baz

\n", + "example": 375, + "start_line": 6549, + "end_line": 6553, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_(bar)_.\n", + "html": "

(bar).

\n", + "example": 376, + "start_line": 6560, + "end_line": 6564, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo bar**\n", + "html": "

foo bar

\n", + "example": 377, + "start_line": 6569, + "end_line": 6573, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "** foo bar**\n", + "html": "

** foo bar**

\n", + "example": 378, + "start_line": 6579, + "end_line": 6583, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "a**\"foo\"**\n", + "html": "

a**"foo"**

\n", + "example": 379, + "start_line": 6590, + "end_line": 6594, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo**bar**\n", + "html": "

foobar

\n", + "example": 380, + "start_line": 6599, + "end_line": 6603, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo bar__\n", + "html": "

foo bar

\n", + "example": 381, + "start_line": 6608, + "end_line": 6612, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__ foo bar__\n", + "html": "

__ foo bar__

\n", + "example": 382, + "start_line": 6618, + "end_line": 6622, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__\nfoo bar__\n", + "html": "

__\nfoo bar__

\n", + "example": 383, + "start_line": 6626, + "end_line": 6632, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "a__\"foo\"__\n", + "html": "

a__"foo"__

\n", + "example": 384, + "start_line": 6638, + "end_line": 6642, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo__bar__\n", + "html": "

foo__bar__

\n", + "example": 385, + "start_line": 6647, + "end_line": 6651, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "5__6__78\n", + "html": "

5__6__78

\n", + "example": 386, + "start_line": 6654, + "end_line": 6658, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "пристаням__стремятся__\n", + "html": "

пристаням__стремятся__

\n", + "example": 387, + "start_line": 6661, + "end_line": 6665, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo, __bar__, baz__\n", + "html": "

foo, bar, baz

\n", + "example": 388, + "start_line": 6668, + "end_line": 6672, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo-__(bar)__\n", + "html": "

foo-(bar)

\n", + "example": 389, + "start_line": 6679, + "end_line": 6683, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo bar **\n", + "html": "

**foo bar **

\n", + "example": 390, + "start_line": 6692, + "end_line": 6696, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**(**foo)\n", + "html": "

**(**foo)

\n", + "example": 391, + "start_line": 6705, + "end_line": 6709, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*(**foo**)*\n", + "html": "

(foo)

\n", + "example": 392, + "start_line": 6715, + "end_line": 6719, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n", + "html": "

Gomphocarpus (Gomphocarpus physocarpus, syn.\nAsclepias physocarpa)

\n", + "example": 393, + "start_line": 6722, + "end_line": 6728, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo \"*bar*\" foo**\n", + "html": "

foo "bar" foo

\n", + "example": 394, + "start_line": 6731, + "end_line": 6735, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo**bar\n", + "html": "

foobar

\n", + "example": 395, + "start_line": 6740, + "end_line": 6744, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo bar __\n", + "html": "

__foo bar __

\n", + "example": 396, + "start_line": 6752, + "end_line": 6756, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__(__foo)\n", + "html": "

__(__foo)

\n", + "example": 397, + "start_line": 6762, + "end_line": 6766, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_(__foo__)_\n", + "html": "

(foo)

\n", + "example": 398, + "start_line": 6772, + "end_line": 6776, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo__bar\n", + "html": "

__foo__bar

\n", + "example": 399, + "start_line": 6781, + "end_line": 6785, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__пристаням__стремятся\n", + "html": "

__пристаням__стремятся

\n", + "example": 400, + "start_line": 6788, + "end_line": 6792, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo__bar__baz__\n", + "html": "

foo__bar__baz

\n", + "example": 401, + "start_line": 6795, + "end_line": 6799, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__(bar)__.\n", + "html": "

(bar).

\n", + "example": 402, + "start_line": 6806, + "end_line": 6810, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo [bar](/url)*\n", + "html": "

foo bar

\n", + "example": 403, + "start_line": 6818, + "end_line": 6822, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo\nbar*\n", + "html": "

foo\nbar

\n", + "example": 404, + "start_line": 6825, + "end_line": 6831, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo __bar__ baz_\n", + "html": "

foo bar baz

\n", + "example": 405, + "start_line": 6837, + "end_line": 6841, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo _bar_ baz_\n", + "html": "

foo bar baz

\n", + "example": 406, + "start_line": 6844, + "end_line": 6848, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo_ bar_\n", + "html": "

foo bar

\n", + "example": 407, + "start_line": 6851, + "end_line": 6855, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo *bar**\n", + "html": "

foo bar

\n", + "example": 408, + "start_line": 6858, + "end_line": 6862, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo **bar** baz*\n", + "html": "

foo bar baz

\n", + "example": 409, + "start_line": 6865, + "end_line": 6869, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo**bar**baz*\n", + "html": "

foobarbaz

\n", + "example": 410, + "start_line": 6871, + "end_line": 6875, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo**bar*\n", + "html": "

foo**bar

\n", + "example": 411, + "start_line": 6895, + "end_line": 6899, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "***foo** bar*\n", + "html": "

foo bar

\n", + "example": 412, + "start_line": 6908, + "end_line": 6912, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo **bar***\n", + "html": "

foo bar

\n", + "example": 413, + "start_line": 6915, + "end_line": 6919, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo**bar***\n", + "html": "

foobar

\n", + "example": 414, + "start_line": 6922, + "end_line": 6926, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo***bar***baz\n", + "html": "

foobarbaz

\n", + "example": 415, + "start_line": 6933, + "end_line": 6937, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo******bar*********baz\n", + "html": "

foobar***baz

\n", + "example": 416, + "start_line": 6939, + "end_line": 6943, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo **bar *baz* bim** bop*\n", + "html": "

foo bar baz bim bop

\n", + "example": 417, + "start_line": 6948, + "end_line": 6952, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo [*bar*](/url)*\n", + "html": "

foo bar

\n", + "example": 418, + "start_line": 6955, + "end_line": 6959, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "** is not an empty emphasis\n", + "html": "

** is not an empty emphasis

\n", + "example": 419, + "start_line": 6964, + "end_line": 6968, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**** is not an empty strong emphasis\n", + "html": "

**** is not an empty strong emphasis

\n", + "example": 420, + "start_line": 6971, + "end_line": 6975, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo [bar](/url)**\n", + "html": "

foo bar

\n", + "example": 421, + "start_line": 6984, + "end_line": 6988, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo\nbar**\n", + "html": "

foo\nbar

\n", + "example": 422, + "start_line": 6991, + "end_line": 6997, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo _bar_ baz__\n", + "html": "

foo bar baz

\n", + "example": 423, + "start_line": 7003, + "end_line": 7007, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo __bar__ baz__\n", + "html": "

foo bar baz

\n", + "example": 424, + "start_line": 7010, + "end_line": 7014, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "____foo__ bar__\n", + "html": "

foo bar

\n", + "example": 425, + "start_line": 7017, + "end_line": 7021, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo **bar****\n", + "html": "

foo bar

\n", + "example": 426, + "start_line": 7024, + "end_line": 7028, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo *bar* baz**\n", + "html": "

foo bar baz

\n", + "example": 427, + "start_line": 7031, + "end_line": 7035, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo*bar*baz**\n", + "html": "

foobarbaz

\n", + "example": 428, + "start_line": 7038, + "end_line": 7042, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "***foo* bar**\n", + "html": "

foo bar

\n", + "example": 429, + "start_line": 7045, + "end_line": 7049, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo *bar***\n", + "html": "

foo bar

\n", + "example": 430, + "start_line": 7052, + "end_line": 7056, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo *bar **baz**\nbim* bop**\n", + "html": "

foo bar baz\nbim bop

\n", + "example": 431, + "start_line": 7061, + "end_line": 7067, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo [*bar*](/url)**\n", + "html": "

foo bar

\n", + "example": 432, + "start_line": 7070, + "end_line": 7074, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__ is not an empty emphasis\n", + "html": "

__ is not an empty emphasis

\n", + "example": 433, + "start_line": 7079, + "end_line": 7083, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "____ is not an empty strong emphasis\n", + "html": "

____ is not an empty strong emphasis

\n", + "example": 434, + "start_line": 7086, + "end_line": 7090, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo ***\n", + "html": "

foo ***

\n", + "example": 435, + "start_line": 7096, + "end_line": 7100, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo *\\**\n", + "html": "

foo *

\n", + "example": 436, + "start_line": 7103, + "end_line": 7107, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo *_*\n", + "html": "

foo _

\n", + "example": 437, + "start_line": 7110, + "end_line": 7114, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo *****\n", + "html": "

foo *****

\n", + "example": 438, + "start_line": 7117, + "end_line": 7121, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo **\\***\n", + "html": "

foo *

\n", + "example": 439, + "start_line": 7124, + "end_line": 7128, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo **_**\n", + "html": "

foo _

\n", + "example": 440, + "start_line": 7131, + "end_line": 7135, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo*\n", + "html": "

*foo

\n", + "example": 441, + "start_line": 7142, + "end_line": 7146, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo**\n", + "html": "

foo*

\n", + "example": 442, + "start_line": 7149, + "end_line": 7153, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "***foo**\n", + "html": "

*foo

\n", + "example": 443, + "start_line": 7156, + "end_line": 7160, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "****foo*\n", + "html": "

***foo

\n", + "example": 444, + "start_line": 7163, + "end_line": 7167, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo***\n", + "html": "

foo*

\n", + "example": 445, + "start_line": 7170, + "end_line": 7174, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo****\n", + "html": "

foo***

\n", + "example": 446, + "start_line": 7177, + "end_line": 7181, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo ___\n", + "html": "

foo ___

\n", + "example": 447, + "start_line": 7187, + "end_line": 7191, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo _\\__\n", + "html": "

foo _

\n", + "example": 448, + "start_line": 7194, + "end_line": 7198, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo _*_\n", + "html": "

foo *

\n", + "example": 449, + "start_line": 7201, + "end_line": 7205, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo _____\n", + "html": "

foo _____

\n", + "example": 450, + "start_line": 7208, + "end_line": 7212, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo __\\___\n", + "html": "

foo _

\n", + "example": 451, + "start_line": 7215, + "end_line": 7219, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "foo __*__\n", + "html": "

foo *

\n", + "example": 452, + "start_line": 7222, + "end_line": 7226, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo_\n", + "html": "

_foo

\n", + "example": 453, + "start_line": 7229, + "end_line": 7233, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo__\n", + "html": "

foo_

\n", + "example": 454, + "start_line": 7240, + "end_line": 7244, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "___foo__\n", + "html": "

_foo

\n", + "example": 455, + "start_line": 7247, + "end_line": 7251, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "____foo_\n", + "html": "

___foo

\n", + "example": 456, + "start_line": 7254, + "end_line": 7258, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo___\n", + "html": "

foo_

\n", + "example": 457, + "start_line": 7261, + "end_line": 7265, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo____\n", + "html": "

foo___

\n", + "example": 458, + "start_line": 7268, + "end_line": 7272, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo**\n", + "html": "

foo

\n", + "example": 459, + "start_line": 7278, + "end_line": 7282, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*_foo_*\n", + "html": "

foo

\n", + "example": 460, + "start_line": 7285, + "end_line": 7289, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__foo__\n", + "html": "

foo

\n", + "example": 461, + "start_line": 7292, + "end_line": 7296, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_*foo*_\n", + "html": "

foo

\n", + "example": 462, + "start_line": 7299, + "end_line": 7303, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "****foo****\n", + "html": "

foo

\n", + "example": 463, + "start_line": 7309, + "end_line": 7313, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "____foo____\n", + "html": "

foo

\n", + "example": 464, + "start_line": 7316, + "end_line": 7320, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "******foo******\n", + "html": "

foo

\n", + "example": 465, + "start_line": 7327, + "end_line": 7331, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "***foo***\n", + "html": "

foo

\n", + "example": 466, + "start_line": 7336, + "end_line": 7340, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_____foo_____\n", + "html": "

foo

\n", + "example": 467, + "start_line": 7343, + "end_line": 7347, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo _bar* baz_\n", + "html": "

foo _bar baz_

\n", + "example": 468, + "start_line": 7352, + "end_line": 7356, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo __bar *baz bim__ bam*\n", + "html": "

foo bar *baz bim bam

\n", + "example": 469, + "start_line": 7359, + "end_line": 7363, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**foo **bar baz**\n", + "html": "

**foo bar baz

\n", + "example": 470, + "start_line": 7368, + "end_line": 7372, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*foo *bar baz*\n", + "html": "

*foo bar baz

\n", + "example": 471, + "start_line": 7375, + "end_line": 7379, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*[bar*](/url)\n", + "html": "

*bar*

\n", + "example": 472, + "start_line": 7384, + "end_line": 7388, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_foo [bar_](/url)\n", + "html": "

_foo bar_

\n", + "example": 473, + "start_line": 7391, + "end_line": 7395, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*\n", + "html": "

*

\n", + "example": 474, + "start_line": 7398, + "end_line": 7402, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**\n", + "html": "

**

\n", + "example": 475, + "start_line": 7405, + "end_line": 7409, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__\n", + "html": "

__

\n", + "example": 476, + "start_line": 7412, + "end_line": 7416, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "*a `*`*\n", + "html": "

a *

\n", + "example": 477, + "start_line": 7419, + "end_line": 7423, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "_a `_`_\n", + "html": "

a _

\n", + "example": 478, + "start_line": 7426, + "end_line": 7430, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "**a\n", + "html": "

**ahttp://foo.bar/?q=**

\n", + "example": 479, + "start_line": 7433, + "end_line": 7437, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "__a\n", + "html": "

__ahttp://foo.bar/?q=__

\n", + "example": 480, + "start_line": 7440, + "end_line": 7444, + "section": "Emphasis and strong emphasis" + }, + { + "markdown": "[link](/uri \"title\")\n", + "html": "

link

\n", + "example": 481, + "start_line": 7528, + "end_line": 7532, + "section": "Links" + }, + { + "markdown": "[link](/uri)\n", + "html": "

link

\n", + "example": 482, + "start_line": 7538, + "end_line": 7542, + "section": "Links" + }, + { + "markdown": "[](./target.md)\n", + "html": "

\n", + "example": 483, + "start_line": 7544, + "end_line": 7548, + "section": "Links" + }, + { + "markdown": "[link]()\n", + "html": "

link

\n", + "example": 484, + "start_line": 7551, + "end_line": 7555, + "section": "Links" + }, + { + "markdown": "[link](<>)\n", + "html": "

link

\n", + "example": 485, + "start_line": 7558, + "end_line": 7562, + "section": "Links" + }, + { + "markdown": "[]()\n", + "html": "

\n", + "example": 486, + "start_line": 7565, + "end_line": 7569, + "section": "Links" + }, + { + "markdown": "[link](/my uri)\n", + "html": "

[link](/my uri)

\n", + "example": 487, + "start_line": 7574, + "end_line": 7578, + "section": "Links" + }, + { + "markdown": "[link](
)\n", + "html": "

link

\n", + "example": 488, + "start_line": 7580, + "end_line": 7584, + "section": "Links" + }, + { + "markdown": "[link](foo\nbar)\n", + "html": "

[link](foo\nbar)

\n", + "example": 489, + "start_line": 7589, + "end_line": 7595, + "section": "Links" + }, + { + "markdown": "[link]()\n", + "html": "

[link]()

\n", + "example": 490, + "start_line": 7597, + "end_line": 7603, + "section": "Links" + }, + { + "markdown": "[a]()\n", + "html": "

a

\n", + "example": 491, + "start_line": 7608, + "end_line": 7612, + "section": "Links" + }, + { + "markdown": "[link]()\n", + "html": "

[link](<foo>)

\n", + "example": 492, + "start_line": 7616, + "end_line": 7620, + "section": "Links" + }, + { + "markdown": "[a](\n[a](c)\n", + "html": "

[a](<b)c\n[a](<b)c>\n[a](c)

\n", + "example": 493, + "start_line": 7625, + "end_line": 7633, + "section": "Links" + }, + { + "markdown": "[link](\\(foo\\))\n", + "html": "

link

\n", + "example": 494, + "start_line": 7637, + "end_line": 7641, + "section": "Links" + }, + { + "markdown": "[link](foo(and(bar)))\n", + "html": "

link

\n", + "example": 495, + "start_line": 7646, + "end_line": 7650, + "section": "Links" + }, + { + "markdown": "[link](foo(and(bar))\n", + "html": "

[link](foo(and(bar))

\n", + "example": 496, + "start_line": 7655, + "end_line": 7659, + "section": "Links" + }, + { + "markdown": "[link](foo\\(and\\(bar\\))\n", + "html": "

link

\n", + "example": 497, + "start_line": 7662, + "end_line": 7666, + "section": "Links" + }, + { + "markdown": "[link]()\n", + "html": "

link

\n", + "example": 498, + "start_line": 7669, + "end_line": 7673, + "section": "Links" + }, + { + "markdown": "[link](foo\\)\\:)\n", + "html": "

link

\n", + "example": 499, + "start_line": 7679, + "end_line": 7683, + "section": "Links" + }, + { + "markdown": "[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n", + "html": "

link

\n

link

\n

link

\n", + "example": 500, + "start_line": 7688, + "end_line": 7698, + "section": "Links" + }, + { + "markdown": "[link](foo\\bar)\n", + "html": "

link

\n", + "example": 501, + "start_line": 7704, + "end_line": 7708, + "section": "Links" + }, + { + "markdown": "[link](foo%20bä)\n", + "html": "

link

\n", + "example": 502, + "start_line": 7720, + "end_line": 7724, + "section": "Links" + }, + { + "markdown": "[link](\"title\")\n", + "html": "

link

\n", + "example": 503, + "start_line": 7731, + "end_line": 7735, + "section": "Links" + }, + { + "markdown": "[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n", + "html": "

link\nlink\nlink

\n", + "example": 504, + "start_line": 7740, + "end_line": 7748, + "section": "Links" + }, + { + "markdown": "[link](/url \"title \\\""\")\n", + "html": "

link

\n", + "example": 505, + "start_line": 7754, + "end_line": 7758, + "section": "Links" + }, + { + "markdown": "[link](/url \"title\")\n", + "html": "

link

\n", + "example": 506, + "start_line": 7765, + "end_line": 7769, + "section": "Links" + }, + { + "markdown": "[link](/url \"title \"and\" title\")\n", + "html": "

[link](/url "title "and" title")

\n", + "example": 507, + "start_line": 7774, + "end_line": 7778, + "section": "Links" + }, + { + "markdown": "[link](/url 'title \"and\" title')\n", + "html": "

link

\n", + "example": 508, + "start_line": 7783, + "end_line": 7787, + "section": "Links" + }, + { + "markdown": "[link]( /uri\n \"title\" )\n", + "html": "

link

\n", + "example": 509, + "start_line": 7808, + "end_line": 7813, + "section": "Links" + }, + { + "markdown": "[link] (/uri)\n", + "html": "

[link] (/uri)

\n", + "example": 510, + "start_line": 7819, + "end_line": 7823, + "section": "Links" + }, + { + "markdown": "[link [foo [bar]]](/uri)\n", + "html": "

link [foo [bar]]

\n", + "example": 511, + "start_line": 7829, + "end_line": 7833, + "section": "Links" + }, + { + "markdown": "[link] bar](/uri)\n", + "html": "

[link] bar](/uri)

\n", + "example": 512, + "start_line": 7836, + "end_line": 7840, + "section": "Links" + }, + { + "markdown": "[link [bar](/uri)\n", + "html": "

[link bar

\n", + "example": 513, + "start_line": 7843, + "end_line": 7847, + "section": "Links" + }, + { + "markdown": "[link \\[bar](/uri)\n", + "html": "

link [bar

\n", + "example": 514, + "start_line": 7850, + "end_line": 7854, + "section": "Links" + }, + { + "markdown": "[link *foo **bar** `#`*](/uri)\n", + "html": "

link foo bar #

\n", + "example": 515, + "start_line": 7859, + "end_line": 7863, + "section": "Links" + }, + { + "markdown": "[![moon](moon.jpg)](/uri)\n", + "html": "

\"moon\"

\n", + "example": 516, + "start_line": 7866, + "end_line": 7870, + "section": "Links" + }, + { + "markdown": "[foo [bar](/uri)](/uri)\n", + "html": "

[foo bar](/uri)

\n", + "example": 517, + "start_line": 7875, + "end_line": 7879, + "section": "Links" + }, + { + "markdown": "[foo *[bar [baz](/uri)](/uri)*](/uri)\n", + "html": "

[foo [bar baz](/uri)](/uri)

\n", + "example": 518, + "start_line": 7882, + "end_line": 7886, + "section": "Links" + }, + { + "markdown": "![[[foo](uri1)](uri2)](uri3)\n", + "html": "

\"[foo](uri2)\"

\n", + "example": 519, + "start_line": 7889, + "end_line": 7893, + "section": "Links" + }, + { + "markdown": "*[foo*](/uri)\n", + "html": "

*foo*

\n", + "example": 520, + "start_line": 7899, + "end_line": 7903, + "section": "Links" + }, + { + "markdown": "[foo *bar](baz*)\n", + "html": "

foo *bar

\n", + "example": 521, + "start_line": 7906, + "end_line": 7910, + "section": "Links" + }, + { + "markdown": "*foo [bar* baz]\n", + "html": "

foo [bar baz]

\n", + "example": 522, + "start_line": 7916, + "end_line": 7920, + "section": "Links" + }, + { + "markdown": "[foo \n", + "html": "

[foo

\n", + "example": 523, + "start_line": 7926, + "end_line": 7930, + "section": "Links" + }, + { + "markdown": "[foo`](/uri)`\n", + "html": "

[foo](/uri)

\n", + "example": 524, + "start_line": 7933, + "end_line": 7937, + "section": "Links" + }, + { + "markdown": "[foo\n", + "html": "

[foohttp://example.com/?search=](uri)

\n", + "example": 525, + "start_line": 7940, + "end_line": 7944, + "section": "Links" + }, + { + "markdown": "[foo][bar]\n\n[bar]: /url \"title\"\n", + "html": "

foo

\n", + "example": 526, + "start_line": 7978, + "end_line": 7984, + "section": "Links" + }, + { + "markdown": "[link [foo [bar]]][ref]\n\n[ref]: /uri\n", + "html": "

link [foo [bar]]

\n", + "example": 527, + "start_line": 7993, + "end_line": 7999, + "section": "Links" + }, + { + "markdown": "[link \\[bar][ref]\n\n[ref]: /uri\n", + "html": "

link [bar

\n", + "example": 528, + "start_line": 8002, + "end_line": 8008, + "section": "Links" + }, + { + "markdown": "[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n", + "html": "

link foo bar #

\n", + "example": 529, + "start_line": 8013, + "end_line": 8019, + "section": "Links" + }, + { + "markdown": "[![moon](moon.jpg)][ref]\n\n[ref]: /uri\n", + "html": "

\"moon\"

\n", + "example": 530, + "start_line": 8022, + "end_line": 8028, + "section": "Links" + }, + { + "markdown": "[foo [bar](/uri)][ref]\n\n[ref]: /uri\n", + "html": "

[foo bar]ref

\n", + "example": 531, + "start_line": 8033, + "end_line": 8039, + "section": "Links" + }, + { + "markdown": "[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n", + "html": "

[foo bar baz]ref

\n", + "example": 532, + "start_line": 8042, + "end_line": 8048, + "section": "Links" + }, + { + "markdown": "*[foo*][ref]\n\n[ref]: /uri\n", + "html": "

*foo*

\n", + "example": 533, + "start_line": 8057, + "end_line": 8063, + "section": "Links" + }, + { + "markdown": "[foo *bar][ref]*\n\n[ref]: /uri\n", + "html": "

foo *bar*

\n", + "example": 534, + "start_line": 8066, + "end_line": 8072, + "section": "Links" + }, + { + "markdown": "[foo \n\n[ref]: /uri\n", + "html": "

[foo

\n", + "example": 535, + "start_line": 8078, + "end_line": 8084, + "section": "Links" + }, + { + "markdown": "[foo`][ref]`\n\n[ref]: /uri\n", + "html": "

[foo][ref]

\n", + "example": 536, + "start_line": 8087, + "end_line": 8093, + "section": "Links" + }, + { + "markdown": "[foo\n\n[ref]: /uri\n", + "html": "

[foohttp://example.com/?search=][ref]

\n", + "example": 537, + "start_line": 8096, + "end_line": 8102, + "section": "Links" + }, + { + "markdown": "[foo][BaR]\n\n[bar]: /url \"title\"\n", + "html": "

foo

\n", + "example": 538, + "start_line": 8107, + "end_line": 8113, + "section": "Links" + }, + { + "markdown": "[ẞ]\n\n[SS]: /url\n", + "html": "

\n", + "example": 539, + "start_line": 8118, + "end_line": 8124, + "section": "Links" + }, + { + "markdown": "[Foo\n bar]: /url\n\n[Baz][Foo bar]\n", + "html": "

Baz

\n", + "example": 540, + "start_line": 8130, + "end_line": 8137, + "section": "Links" + }, + { + "markdown": "[foo] [bar]\n\n[bar]: /url \"title\"\n", + "html": "

[foo] bar

\n", + "example": 541, + "start_line": 8143, + "end_line": 8149, + "section": "Links" + }, + { + "markdown": "[foo]\n[bar]\n\n[bar]: /url \"title\"\n", + "html": "

[foo]\nbar

\n", + "example": 542, + "start_line": 8152, + "end_line": 8160, + "section": "Links" + }, + { + "markdown": "[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n", + "html": "

bar

\n", + "example": 543, + "start_line": 8193, + "end_line": 8201, + "section": "Links" + }, + { + "markdown": "[bar][foo\\!]\n\n[foo!]: /url\n", + "html": "

[bar][foo!]

\n", + "example": 544, + "start_line": 8208, + "end_line": 8214, + "section": "Links" + }, + { + "markdown": "[foo][ref[]\n\n[ref[]: /uri\n", + "html": "

[foo][ref[]

\n

[ref[]: /uri

\n", + "example": 545, + "start_line": 8220, + "end_line": 8227, + "section": "Links" + }, + { + "markdown": "[foo][ref[bar]]\n\n[ref[bar]]: /uri\n", + "html": "

[foo][ref[bar]]

\n

[ref[bar]]: /uri

\n", + "example": 546, + "start_line": 8230, + "end_line": 8237, + "section": "Links" + }, + { + "markdown": "[[[foo]]]\n\n[[[foo]]]: /url\n", + "html": "

[[[foo]]]

\n

[[[foo]]]: /url

\n", + "example": 547, + "start_line": 8240, + "end_line": 8247, + "section": "Links" + }, + { + "markdown": "[foo][ref\\[]\n\n[ref\\[]: /uri\n", + "html": "

foo

\n", + "example": 548, + "start_line": 8250, + "end_line": 8256, + "section": "Links" + }, + { + "markdown": "[bar\\\\]: /uri\n\n[bar\\\\]\n", + "html": "

bar\\

\n", + "example": 549, + "start_line": 8261, + "end_line": 8267, + "section": "Links" + }, + { + "markdown": "[]\n\n[]: /uri\n", + "html": "

[]

\n

[]: /uri

\n", + "example": 550, + "start_line": 8273, + "end_line": 8280, + "section": "Links" + }, + { + "markdown": "[\n ]\n\n[\n ]: /uri\n", + "html": "

[\n]

\n

[\n]: /uri

\n", + "example": 551, + "start_line": 8283, + "end_line": 8294, + "section": "Links" + }, + { + "markdown": "[foo][]\n\n[foo]: /url \"title\"\n", + "html": "

foo

\n", + "example": 552, + "start_line": 8306, + "end_line": 8312, + "section": "Links" + }, + { + "markdown": "[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", + "html": "

foo bar

\n", + "example": 553, + "start_line": 8315, + "end_line": 8321, + "section": "Links" + }, + { + "markdown": "[Foo][]\n\n[foo]: /url \"title\"\n", + "html": "

Foo

\n", + "example": 554, + "start_line": 8326, + "end_line": 8332, + "section": "Links" + }, + { + "markdown": "[foo] \n[]\n\n[foo]: /url \"title\"\n", + "html": "

foo\n[]

\n", + "example": 555, + "start_line": 8339, + "end_line": 8347, + "section": "Links" + }, + { + "markdown": "[foo]\n\n[foo]: /url \"title\"\n", + "html": "

foo

\n", + "example": 556, + "start_line": 8359, + "end_line": 8365, + "section": "Links" + }, + { + "markdown": "[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", + "html": "

foo bar

\n", + "example": 557, + "start_line": 8368, + "end_line": 8374, + "section": "Links" + }, + { + "markdown": "[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n", + "html": "

[foo bar]

\n", + "example": 558, + "start_line": 8377, + "end_line": 8383, + "section": "Links" + }, + { + "markdown": "[[bar [foo]\n\n[foo]: /url\n", + "html": "

[[bar foo

\n", + "example": 559, + "start_line": 8386, + "end_line": 8392, + "section": "Links" + }, + { + "markdown": "[Foo]\n\n[foo]: /url \"title\"\n", + "html": "

Foo

\n", + "example": 560, + "start_line": 8397, + "end_line": 8403, + "section": "Links" + }, + { + "markdown": "[foo] bar\n\n[foo]: /url\n", + "html": "

foo bar

\n", + "example": 561, + "start_line": 8408, + "end_line": 8414, + "section": "Links" + }, + { + "markdown": "\\[foo]\n\n[foo]: /url \"title\"\n", + "html": "

[foo]

\n", + "example": 562, + "start_line": 8420, + "end_line": 8426, + "section": "Links" + }, + { + "markdown": "[foo*]: /url\n\n*[foo*]\n", + "html": "

*foo*

\n", + "example": 563, + "start_line": 8432, + "end_line": 8438, + "section": "Links" + }, + { + "markdown": "[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n", + "html": "

foo

\n", + "example": 564, + "start_line": 8444, + "end_line": 8451, + "section": "Links" + }, + { + "markdown": "[foo][]\n\n[foo]: /url1\n", + "html": "

foo

\n", + "example": 565, + "start_line": 8453, + "end_line": 8459, + "section": "Links" + }, + { + "markdown": "[foo]()\n\n[foo]: /url1\n", + "html": "

foo

\n", + "example": 566, + "start_line": 8463, + "end_line": 8469, + "section": "Links" + }, + { + "markdown": "[foo](not a link)\n\n[foo]: /url1\n", + "html": "

foo(not a link)

\n", + "example": 567, + "start_line": 8471, + "end_line": 8477, + "section": "Links" + }, + { + "markdown": "[foo][bar][baz]\n\n[baz]: /url\n", + "html": "

[foo]bar

\n", + "example": 568, + "start_line": 8482, + "end_line": 8488, + "section": "Links" + }, + { + "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n", + "html": "

foobaz

\n", + "example": 569, + "start_line": 8494, + "end_line": 8501, + "section": "Links" + }, + { + "markdown": "[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n", + "html": "

[foo]bar

\n", + "example": 570, + "start_line": 8507, + "end_line": 8514, + "section": "Links" + }, + { + "markdown": "![foo](/url \"title\")\n", + "html": "

\"foo\"

\n", + "example": 571, + "start_line": 8530, + "end_line": 8534, + "section": "Images" + }, + { + "markdown": "![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", + "html": "

\"foo

\n", + "example": 572, + "start_line": 8537, + "end_line": 8543, + "section": "Images" + }, + { + "markdown": "![foo ![bar](/url)](/url2)\n", + "html": "

\"foo

\n", + "example": 573, + "start_line": 8546, + "end_line": 8550, + "section": "Images" + }, + { + "markdown": "![foo [bar](/url)](/url2)\n", + "html": "

\"foo

\n", + "example": 574, + "start_line": 8553, + "end_line": 8557, + "section": "Images" + }, + { + "markdown": "![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n", + "html": "

\"foo

\n", + "example": 575, + "start_line": 8567, + "end_line": 8573, + "section": "Images" + }, + { + "markdown": "![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n", + "html": "

\"foo

\n", + "example": 576, + "start_line": 8576, + "end_line": 8582, + "section": "Images" + }, + { + "markdown": "![foo](train.jpg)\n", + "html": "

\"foo\"

\n", + "example": 577, + "start_line": 8585, + "end_line": 8589, + "section": "Images" + }, + { + "markdown": "My ![foo bar](/path/to/train.jpg \"title\" )\n", + "html": "

My \"foo

\n", + "example": 578, + "start_line": 8592, + "end_line": 8596, + "section": "Images" + }, + { + "markdown": "![foo]()\n", + "html": "

\"foo\"

\n", + "example": 579, + "start_line": 8599, + "end_line": 8603, + "section": "Images" + }, + { + "markdown": "![](/url)\n", + "html": "

\"\"

\n", + "example": 580, + "start_line": 8606, + "end_line": 8610, + "section": "Images" + }, + { + "markdown": "![foo][bar]\n\n[bar]: /url\n", + "html": "

\"foo\"

\n", + "example": 581, + "start_line": 8615, + "end_line": 8621, + "section": "Images" + }, + { + "markdown": "![foo][bar]\n\n[BAR]: /url\n", + "html": "

\"foo\"

\n", + "example": 582, + "start_line": 8624, + "end_line": 8630, + "section": "Images" + }, + { + "markdown": "![foo][]\n\n[foo]: /url \"title\"\n", + "html": "

\"foo\"

\n", + "example": 583, + "start_line": 8635, + "end_line": 8641, + "section": "Images" + }, + { + "markdown": "![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n", + "html": "

\"foo

\n", + "example": 584, + "start_line": 8644, + "end_line": 8650, + "section": "Images" + }, + { + "markdown": "![Foo][]\n\n[foo]: /url \"title\"\n", + "html": "

\"Foo\"

\n", + "example": 585, + "start_line": 8655, + "end_line": 8661, + "section": "Images" + }, + { + "markdown": "![foo] \n[]\n\n[foo]: /url \"title\"\n", + "html": "

\"foo\"\n[]

\n", + "example": 586, + "start_line": 8667, + "end_line": 8675, + "section": "Images" + }, + { + "markdown": "![foo]\n\n[foo]: /url \"title\"\n", + "html": "

\"foo\"

\n", + "example": 587, + "start_line": 8680, + "end_line": 8686, + "section": "Images" + }, + { + "markdown": "![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n", + "html": "

\"foo

\n", + "example": 588, + "start_line": 8689, + "end_line": 8695, + "section": "Images" + }, + { + "markdown": "![[foo]]\n\n[[foo]]: /url \"title\"\n", + "html": "

![[foo]]

\n

[[foo]]: /url "title"

\n", + "example": 589, + "start_line": 8700, + "end_line": 8707, + "section": "Images" + }, + { + "markdown": "![Foo]\n\n[foo]: /url \"title\"\n", + "html": "

\"Foo\"

\n", + "example": 590, + "start_line": 8712, + "end_line": 8718, + "section": "Images" + }, + { + "markdown": "!\\[foo]\n\n[foo]: /url \"title\"\n", + "html": "

![foo]

\n", + "example": 591, + "start_line": 8724, + "end_line": 8730, + "section": "Images" + }, + { + "markdown": "\\![foo]\n\n[foo]: /url \"title\"\n", + "html": "

!foo

\n", + "example": 592, + "start_line": 8736, + "end_line": 8742, + "section": "Images" + }, + { + "markdown": "\n", + "html": "

http://foo.bar.baz

\n", + "example": 593, + "start_line": 8769, + "end_line": 8773, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

http://foo.bar.baz/test?q=hello&id=22&boolean

\n", + "example": 594, + "start_line": 8776, + "end_line": 8780, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

irc://foo.bar:2233/baz

\n", + "example": 595, + "start_line": 8783, + "end_line": 8787, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

MAILTO:FOO@BAR.BAZ

\n", + "example": 596, + "start_line": 8792, + "end_line": 8796, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

a+b+c:d

\n", + "example": 597, + "start_line": 8804, + "end_line": 8808, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

made-up-scheme://foo,bar

\n", + "example": 598, + "start_line": 8811, + "end_line": 8815, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

http://../

\n", + "example": 599, + "start_line": 8818, + "end_line": 8822, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

localhost:5001/foo

\n", + "example": 600, + "start_line": 8825, + "end_line": 8829, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

<http://foo.bar/baz bim>

\n", + "example": 601, + "start_line": 8834, + "end_line": 8838, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

http://example.com/\\[\\

\n", + "example": 602, + "start_line": 8843, + "end_line": 8847, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

foo@bar.example.com

\n", + "example": 603, + "start_line": 8865, + "end_line": 8869, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

foo+special@Bar.baz-bar0.com

\n", + "example": 604, + "start_line": 8872, + "end_line": 8876, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

<foo+@bar.example.com>

\n", + "example": 605, + "start_line": 8881, + "end_line": 8885, + "section": "Autolinks" + }, + { + "markdown": "<>\n", + "html": "

<>

\n", + "example": 606, + "start_line": 8890, + "end_line": 8894, + "section": "Autolinks" + }, + { + "markdown": "< http://foo.bar >\n", + "html": "

< http://foo.bar >

\n", + "example": 607, + "start_line": 8897, + "end_line": 8901, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

<m:abc>

\n", + "example": 608, + "start_line": 8904, + "end_line": 8908, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

<foo.bar.baz>

\n", + "example": 609, + "start_line": 8911, + "end_line": 8915, + "section": "Autolinks" + }, + { + "markdown": "http://example.com\n", + "html": "

http://example.com

\n", + "example": 610, + "start_line": 8918, + "end_line": 8922, + "section": "Autolinks" + }, + { + "markdown": "foo@bar.example.com\n", + "html": "

foo@bar.example.com

\n", + "example": 611, + "start_line": 8925, + "end_line": 8929, + "section": "Autolinks" + }, + { + "markdown": "\n", + "html": "

\n", + "example": 612, + "start_line": 9006, + "end_line": 9010, + "section": "Raw HTML" + }, + { + "markdown": "\n", + "html": "

\n", + "example": 613, + "start_line": 9015, + "end_line": 9019, + "section": "Raw HTML" + }, + { + "markdown": "\n", + "html": "

\n", + "example": 614, + "start_line": 9024, + "end_line": 9030, + "section": "Raw HTML" + }, + { + "markdown": "\n", + "html": "

\n", + "example": 615, + "start_line": 9035, + "end_line": 9041, + "section": "Raw HTML" + }, + { + "markdown": "Foo \n", + "html": "

Foo

\n", + "example": 616, + "start_line": 9046, + "end_line": 9050, + "section": "Raw HTML" + }, + { + "markdown": "<33> <__>\n", + "html": "

<33> <__>

\n", + "example": 617, + "start_line": 9055, + "end_line": 9059, + "section": "Raw HTML" + }, + { + "markdown": "
\n", + "html": "

<a h*#ref="hi">

\n", + "example": 618, + "start_line": 9064, + "end_line": 9068, + "section": "Raw HTML" + }, + { + "markdown": "
\n", + "html": "

<a href="hi'> <a href=hi'>

\n", + "example": 619, + "start_line": 9073, + "end_line": 9077, + "section": "Raw HTML" + }, + { + "markdown": "< a><\nfoo>\n\n", + "html": "

< a><\nfoo><bar/ >\n<foo bar=baz\nbim!bop />

\n", + "example": 620, + "start_line": 9082, + "end_line": 9092, + "section": "Raw HTML" + }, + { + "markdown": "
\n", + "html": "

<a href='bar'title=title>

\n", + "example": 621, + "start_line": 9097, + "end_line": 9101, + "section": "Raw HTML" + }, + { + "markdown": "
\n", + "html": "

\n", + "example": 622, + "start_line": 9106, + "end_line": 9110, + "section": "Raw HTML" + }, + { + "markdown": "\n", + "html": "

</a href="foo">

\n", + "example": 623, + "start_line": 9115, + "end_line": 9119, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 624, + "start_line": 9124, + "end_line": 9130, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo <!-- not a comment -- two hyphens -->

\n", + "example": 625, + "start_line": 9133, + "end_line": 9137, + "section": "Raw HTML" + }, + { + "markdown": "foo foo -->\n\nfoo \n", + "html": "

foo <!--> foo -->

\n

foo <!-- foo--->

\n", + "example": 626, + "start_line": 9142, + "end_line": 9149, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 627, + "start_line": 9154, + "end_line": 9158, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 628, + "start_line": 9163, + "end_line": 9167, + "section": "Raw HTML" + }, + { + "markdown": "foo &<]]>\n", + "html": "

foo &<]]>

\n", + "example": 629, + "start_line": 9172, + "end_line": 9176, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 630, + "start_line": 9182, + "end_line": 9186, + "section": "Raw HTML" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 631, + "start_line": 9191, + "end_line": 9195, + "section": "Raw HTML" + }, + { + "markdown": "\n", + "html": "

<a href=""">

\n", + "example": 632, + "start_line": 9198, + "end_line": 9202, + "section": "Raw HTML" + }, + { + "markdown": "foo \nbaz\n", + "html": "

foo
\nbaz

\n", + "example": 633, + "start_line": 9212, + "end_line": 9218, + "section": "Hard line breaks" + }, + { + "markdown": "foo\\\nbaz\n", + "html": "

foo
\nbaz

\n", + "example": 634, + "start_line": 9224, + "end_line": 9230, + "section": "Hard line breaks" + }, + { + "markdown": "foo \nbaz\n", + "html": "

foo
\nbaz

\n", + "example": 635, + "start_line": 9235, + "end_line": 9241, + "section": "Hard line breaks" + }, + { + "markdown": "foo \n bar\n", + "html": "

foo
\nbar

\n", + "example": 636, + "start_line": 9246, + "end_line": 9252, + "section": "Hard line breaks" + }, + { + "markdown": "foo\\\n bar\n", + "html": "

foo
\nbar

\n", + "example": 637, + "start_line": 9255, + "end_line": 9261, + "section": "Hard line breaks" + }, + { + "markdown": "*foo \nbar*\n", + "html": "

foo
\nbar

\n", + "example": 638, + "start_line": 9267, + "end_line": 9273, + "section": "Hard line breaks" + }, + { + "markdown": "*foo\\\nbar*\n", + "html": "

foo
\nbar

\n", + "example": 639, + "start_line": 9276, + "end_line": 9282, + "section": "Hard line breaks" + }, + { + "markdown": "`code \nspan`\n", + "html": "

code span

\n", + "example": 640, + "start_line": 9287, + "end_line": 9292, + "section": "Hard line breaks" + }, + { + "markdown": "`code\\\nspan`\n", + "html": "

code\\ span

\n", + "example": 641, + "start_line": 9295, + "end_line": 9300, + "section": "Hard line breaks" + }, + { + "markdown": "
\n", + "html": "

\n", + "example": 642, + "start_line": 9305, + "end_line": 9311, + "section": "Hard line breaks" + }, + { + "markdown": "\n", + "html": "

\n", + "example": 643, + "start_line": 9314, + "end_line": 9320, + "section": "Hard line breaks" + }, + { + "markdown": "foo\\\n", + "html": "

foo\\

\n", + "example": 644, + "start_line": 9327, + "end_line": 9331, + "section": "Hard line breaks" + }, + { + "markdown": "foo \n", + "html": "

foo

\n", + "example": 645, + "start_line": 9334, + "end_line": 9338, + "section": "Hard line breaks" + }, + { + "markdown": "### foo\\\n", + "html": "

foo\\

\n", + "example": 646, + "start_line": 9341, + "end_line": 9345, + "section": "Hard line breaks" + }, + { + "markdown": "### foo \n", + "html": "

foo

\n", + "example": 647, + "start_line": 9348, + "end_line": 9352, + "section": "Hard line breaks" + }, + { + "markdown": "foo\nbaz\n", + "html": "

foo\nbaz

\n", + "example": 648, + "start_line": 9363, + "end_line": 9369, + "section": "Soft line breaks" + }, + { + "markdown": "foo \n baz\n", + "html": "

foo\nbaz

\n", + "example": 649, + "start_line": 9375, + "end_line": 9381, + "section": "Soft line breaks" + }, + { + "markdown": "hello $.;'there\n", + "html": "

hello $.;'there

\n", + "example": 650, + "start_line": 9395, + "end_line": 9399, + "section": "Textual content" + }, + { + "markdown": "Foo χρῆν\n", + "html": "

Foo χρῆν

\n", + "example": 651, + "start_line": 9402, + "end_line": 9406, + "section": "Textual content" + }, + { + "markdown": "Multiple spaces\n", + "html": "

Multiple spaces

\n", + "example": 652, + "start_line": 9411, + "end_line": 9415, + "section": "Textual content" + } +] \ No newline at end of file diff --git a/libs/markdown/src/test/resources/simplelogger.properties b/libs/markdown/src/test/resources/simplelogger.properties new file mode 100644 index 00000000000..2370c548f2a --- /dev/null +++ b/libs/markdown/src/test/resources/simplelogger.properties @@ -0,0 +1,2 @@ + +org.slf4j.simpleLogger.defaultLogLevel=info diff --git a/libs/shaded-directory-watcher/build.gradle b/libs/shaded-directory-watcher/build.gradle new file mode 100644 index 00000000000..d3ba8ee6ecb --- /dev/null +++ b/libs/shaded-directory-watcher/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "com.github.johnrengelman.shadow" version "7.0.0" + id "java" +} + +configurations { + relocated { + // Directory-Watcher depends on JNA and on SLF4j, + // both of which are part of MCs own dependencies already + transitive = false + } +} + +dependencies { + relocated "io.methvin:directory-watcher:${rootProject.directory_watcher_version}" +} + +shadowJar { + relocate "io.methvin", "appeng.shaded.directorywatcher" + + configurations = [project.configurations.relocated] + archiveClassifier = "shaded" +} diff --git a/libs/shaded-snakeyaml/build.gradle b/libs/shaded-snakeyaml/build.gradle new file mode 100644 index 00000000000..5d864348d46 --- /dev/null +++ b/libs/shaded-snakeyaml/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "com.github.johnrengelman.shadow" version "7.0.0" + id "java" +} + +configurations { + relocated { + transitive = false + } +} + +dependencies { + relocated "org.yaml:snakeyaml:${rootProject.snakeyaml_version}" +} + +shadowJar { + relocate "org.yaml.snakeyaml", "appeng.shaded.snakeyaml" + + configurations = [project.configurations.relocated] + archiveClassifier = "shaded" +} diff --git a/settings.gradle b/settings.gradle index 9c30789726b..56edb6027fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,7 @@ pluginManagement { gradlePluginPortal() } } + +include 'libs:markdown' +include 'libs:shaded-snakeyaml' +include 'libs:shaded-directory-watcher' diff --git a/src/main/java/appeng/api/ids/AEItemIds.java b/src/main/java/appeng/api/ids/AEItemIds.java index b21b4978bef..e527c8e0a5d 100644 --- a/src/main/java/appeng/api/ids/AEItemIds.java +++ b/src/main/java/appeng/api/ids/AEItemIds.java @@ -278,6 +278,7 @@ public final class AEItemIds { public static final ResourceLocation FORMATION_CORE = id("formation_core"); public static final ResourceLocation ANNIHILATION_CORE = id("annihilation_core"); public static final ResourceLocation SKY_DUST = id("sky_dust"); + public static final ResourceLocation GUIDE = id("guide"); public static final ResourceLocation ENDER_DUST = id("ender_dust"); public static final ResourceLocation SINGULARITY = id("singularity"); public static final ResourceLocation QUANTUM_ENTANGLED_SINGULARITY = id("quantum_entangled_singularity"); diff --git a/src/main/java/appeng/client/gui/DashPattern.java b/src/main/java/appeng/client/gui/DashPattern.java new file mode 100644 index 00000000000..f6afce755b5 --- /dev/null +++ b/src/main/java/appeng/client/gui/DashPattern.java @@ -0,0 +1,7 @@ +package appeng.client.gui; + +public record DashPattern(float width, float onLength, float offLength, int color, float animationCycleMs) { + float length() { + return onLength + offLength; + } +} diff --git a/src/main/java/appeng/client/gui/DashedRectangle.java b/src/main/java/appeng/client/gui/DashedRectangle.java new file mode 100644 index 00000000000..6cbe4a3ebd5 --- /dev/null +++ b/src/main/java/appeng/client/gui/DashedRectangle.java @@ -0,0 +1,90 @@ +package appeng.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.util.Mth; + +import appeng.client.guidebook.document.LytRect; + +/** + * Rendering helper for rendering a rectangle with a dashed outline. + */ +public final class DashedRectangle { + private DashedRectangle() { + } + + public static void render(PoseStack stack, LytRect bounds, DashPattern pattern, float z) { + + RenderSystem.disableTexture(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder builder = tesselator.getBuilder(); + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + + var t = 0f; + if (pattern.animationCycleMs() > 0) { + t = (System.currentTimeMillis() % (int) pattern.animationCycleMs()) / pattern.animationCycleMs(); + } + + buildHorizontalDashedLine(builder, stack, t, bounds.x(), bounds.right(), bounds.y(), z, pattern, false); + buildHorizontalDashedLine(builder, stack, t, bounds.x(), bounds.right(), bounds.bottom() - pattern.width(), z, + pattern, true); + + buildVerticalDashedLine(builder, stack, t, bounds.x(), bounds.y(), bounds.bottom(), z, pattern, true); + buildVerticalDashedLine(builder, stack, t, bounds.right() - pattern.width(), bounds.y(), bounds.bottom(), z, + pattern, false); + + tesselator.end(); + RenderSystem.disableBlend(); + RenderSystem.enableTexture(); + } + + private static void buildHorizontalDashedLine(BufferBuilder builder, PoseStack stack, + float t, float x1, float x2, float y, float z, + DashPattern pattern, boolean reverse) { + if (!reverse) { + t = 1 - t; + } + var phase = t * pattern.length(); + + var pose = stack.last().pose(); + var color = pattern.color(); + + for (float x = x1 - phase; x < x2; x += pattern.length()) { + builder.vertex(pose, Mth.clamp(x + pattern.onLength(), x1, x2), y, z).color(color).endVertex(); + builder.vertex(pose, Mth.clamp(x, x1, x2), y, z).color(color).endVertex(); + builder.vertex(pose, Mth.clamp(x, x1, x2), y + pattern.width(), z).color(color).endVertex(); + builder.vertex(pose, Mth.clamp(x + pattern.onLength(), x1, x2), y + pattern.width(), z).color(color) + .endVertex(); + } + } + + private static void buildVerticalDashedLine(BufferBuilder builder, PoseStack stack, + float t, float x, float y1, float y2, float z, + DashPattern pattern, boolean reverse) { + if (!reverse) { + t = 1 - t; + } + var phase = t * pattern.length(); + + var pose = stack.last().pose(); + var color = pattern.color(); + + for (float y = y1 - phase; y < y2; y += pattern.length()) { + builder.vertex(pose, x + pattern.width(), Mth.clamp(y, y1, y2), z).color(color).endVertex(); + builder.vertex(pose, x, Mth.clamp(y, y1, y2), z).color(color).endVertex(); + builder.vertex(pose, x, Mth.clamp(y + pattern.onLength(), y1, y2), z).color(color).endVertex(); + builder.vertex(pose, x + pattern.width(), Mth.clamp(y + pattern.onLength(), y1, y2), z).color(color) + .endVertex(); + } + } + +} diff --git a/src/main/java/appeng/client/guidebook/GuideManager.java b/src/main/java/appeng/client/guidebook/GuideManager.java new file mode 100644 index 00000000000..d3bbc59a75d --- /dev/null +++ b/src/main/java/appeng/client/guidebook/GuideManager.java @@ -0,0 +1,371 @@ +package appeng.client.guidebook; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackCreator; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.LoadingOverlay; +import net.minecraft.commands.Commands; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ReloadableServerResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.repository.ServerPacksSource; +import net.minecraft.server.packs.resources.MultiPackResourceManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimplePreparableReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.ParsedGuidePage; +import appeng.client.guidebook.indices.CategoryIndex; +import appeng.client.guidebook.indices.ItemIndex; +import appeng.client.guidebook.indices.PageIndex; +import appeng.client.guidebook.navigation.NavigationTree; +import appeng.client.guidebook.screen.GuideScreen; +import appeng.core.AppEng; +import appeng.util.Platform; + +public final class GuideManager { + private static final Logger LOGGER = LoggerFactory.getLogger(GuideManager.class); + + public static final GuideManager INSTANCE = new GuideManager(); + /** + * Folder for the guidebook assets. + */ + private static final String ASSETS_FOLDER = "ae2guide"; + /** + * System property to set a folder path to load additional resources from for guidebook development + */ + private static final String PROPERTY_DEV_SOURCES = "appeng.guide-dev.sources"; + /** + * System property to load the namespace from for the resources in {@link #PROPERTY_DEV_SOURCES}. + */ + private static final String PROPERTY_DEV_SOURCES_NAMESPACE = "appeng.guide-dev.sources.namespace"; + /** + * System property to specify a page id to load directly after the client starts. + */ + private static final String PROPERTY_DEV_STARTUP_PAGE = "appeng.guide-dev.startup-page"; + /** + * Validates all pages at startup. + */ + private static final String PROPERTY_DEV_VALIDATE = "appeng.guide-dev.validate"; + private final Map developmentPages = new HashMap<>(); + private final List indices = new ArrayList<>(); + private NavigationTree navigationTree = new NavigationTree(); + private Map pages; + + @Nullable + private final Path developmentSourceFolder; + @Nullable + private final String developmentSourceNamespace; + + private GuideManager() { + addIndex(ItemIndex.INSTANCE); + addIndex(CategoryIndex.INSTANCE); + + ResourceManagerHelper.get(PackType.CLIENT_RESOURCES).registerReloadListener(new ReloadListener()); + + var sourceFolder = System.getProperty(PROPERTY_DEV_SOURCES); + if (sourceFolder != null) { + developmentSourceFolder = Paths.get(sourceFolder); + // Allow overriding which Mod-ID is used for the sources in the given folder + developmentSourceNamespace = System.getProperty(PROPERTY_DEV_SOURCES_NAMESPACE, AppEng.MOD_ID); + watchDevelopmentSources(developmentSourceFolder, developmentSourceNamespace); + } else { + developmentSourceFolder = null; + developmentSourceNamespace = null; + } + } + + public void addIndex(PageIndex index) { + if (!indices.contains(index)) { + indices.add(index); + } + } + + public static void init() { + // Guaranteed init order + + openPageAtStartupOrValidate(); + } + + /** + * Opens the given page directly after the client started. + */ + private static void openPageAtStartupOrValidate() { + var validatePages = Boolean.TRUE.equals(Boolean.getBoolean(PROPERTY_DEV_VALIDATE)); + ResourceLocation startupPageId; + + var startupPage = System.getProperty(PROPERTY_DEV_STARTUP_PAGE); + if (startupPage != null) { + startupPageId = new ResourceLocation(startupPage); + } else { + startupPageId = null; + } + + if (startupPageId == null && !validatePages) { + return; // Nothing to do + } + + ClientLifecycleEvents.CLIENT_STARTED.register(client -> { + CompletableFuture reload; + + if (client.getOverlay() instanceof LoadingOverlay loadingOverlay) { + reload = loadingOverlay.reload.done(); + } else { + reload = CompletableFuture.completedFuture(null); + } + + reload.thenRunAsync(() -> { + runDatapackReload(); + if (validatePages) { + // Iterate and compile all pages to warn about errors on startup + for (var entry : GuideManager.INSTANCE.developmentPages.entrySet()) { + LOGGER.info("Compiling {}", entry.getKey()); + GuideManager.INSTANCE.getPage(entry.getKey()); + } + } + + if (startupPageId != null) { + client.setScreen(GuideScreen.openNew(PageAnchor.page(startupPageId))); + } + }, client) + .exceptionally(throwable -> { + LOGGER.error("Failed to open startup page / validate pages.", throwable); + return null; + }); + }); + } + + // Run a fake datapack reload to properly compile the page (Recipes, Tags, etc.) + // Only used when we try to compile pages before entering a world (validation, show on startup) + private static void runDatapackReload() { + try { + var access = RegistryAccess.BUILTIN.get(); + + PackRepository packRepository = new PackRepository( + PackType.SERVER_DATA, + new ServerPacksSource(), + new ModResourcePackCreator(PackType.SERVER_DATA)); + packRepository.reload(); + packRepository.setSelected(ModResourcePackUtil.createDefaultDataPackSettings().getEnabled()); + + var closeableResourceManager = new MultiPackResourceManager(PackType.SERVER_DATA, + packRepository.openAllSelected()); + var stuff = ReloadableServerResources.loadResources( + closeableResourceManager, + access, + Commands.CommandSelection.ALL, + 0, + Util.backgroundExecutor(), + Runnable::run).get(); + stuff.updateRegistryTags(access); + Platform.fallbackClientRecipeManager = stuff.getRecipeManager(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Nullable + public ParsedGuidePage getParsedPage(ResourceLocation id) { + if (pages == null) { + LOGGER.warn("Can't get page {}. Pages not loaded yet.", id); + return null; + } + + return developmentPages.getOrDefault(id, pages.get(id)); + } + + @Nullable + public GuidePage getPage(ResourceLocation id) { + var page = getParsedPage(id); + + return page != null ? PageCompiler.compile(this::getAsset, page) : null; + } + + private byte[] getAsset(ResourceLocation id) { + // Also load images from the development sources folder, if it exists and contains the asset namespace + if (developmentSourceFolder != null && id.getNamespace().equals(developmentSourceNamespace)) { + var path = developmentSourceFolder.resolve(id.getPath()); + try (var in = Files.newInputStream(path)) { + return in.readAllBytes(); + } catch (FileNotFoundException ignored) { + } catch (IOException e) { + LOGGER.error("Failed to open guidebook asset {}", path); + return null; + } + } + + // Transform id such that the path is prefixed with "ae2assets", the source folder for the guidebook assets + id = new ResourceLocation(id.getNamespace(), ASSETS_FOLDER + "/" + id.getPath()); + + var resource = Minecraft.getInstance().getResourceManager().getResource(id).orElse(null); + if (resource == null) { + return null; + } + try (var input = resource.open()) { + return input.readAllBytes(); + } catch (IOException e) { + LOGGER.error("Failed to open guidebook asset {}", id); + return null; + } + } + + public NavigationTree getNavigationTree() { + return navigationTree; + } + + public boolean pageExists(ResourceLocation pageId) { + return developmentPages.containsKey(pageId) || pages != null && pages.containsKey(pageId); + } + + @Nullable + public Path getDevelopmentSourcePath(ResourceLocation id) { + if (developmentSourceFolder != null && id.getNamespace().equals(developmentSourceNamespace)) { + var path = developmentSourceFolder.resolve(id.getPath()); + if (Files.exists(path)) { + return path; + } + } + return null; + } + + class ReloadListener extends SimplePreparableReloadListener> + implements IdentifiableResourceReloadListener { + @Override + public ResourceLocation getFabricId() { + return AppEng.makeId("guidebook"); + } + + @Override + protected Map prepare(ResourceManager resourceManager, + ProfilerFiller profiler) { + profiler.startTick(); + Map pages = new HashMap<>(); + + var resources = resourceManager.listResources(ASSETS_FOLDER, + location -> location.getPath().endsWith(".md")); + + for (var entry : resources.entrySet()) { + var pageId = new ResourceLocation( + entry.getKey().getNamespace(), + entry.getKey().getPath().substring((ASSETS_FOLDER + "/").length())); + + String sourcePackId = entry.getValue().sourcePackId(); + try (var in = entry.getValue().open()) { + pages.put(pageId, PageCompiler.parse(sourcePackId, pageId, in)); + } catch (IOException e) { + LOGGER.error("Failed to load guidebook page {} from pack {}", pageId, sourcePackId, e); + } + } + + profiler.endTick(); + return pages; + } + + @Override + protected void apply(Map pages, ResourceManager resourceManager, + ProfilerFiller profiler) { + profiler.startTick(); + GuideManager.this.pages = pages; + profiler.push("indices"); + var allPages = new ArrayList(); + allPages.addAll(pages.values()); + allPages.addAll(developmentPages.values()); + for (PageIndex index : indices) { + index.rebuild(allPages); + } + profiler.pop(); + profiler.push("navigation"); + navigationTree = buildNavigation(); + profiler.pop(); + profiler.endTick(); + } + + @Override + public String getName() { + return "AE2 Guidebook"; + } + } + + private void watchDevelopmentSources(Path developmentSources, String namespace) { + var watcher = new GuideSourceWatcher(namespace, developmentSources); + ClientTickEvents.START_CLIENT_TICK.register(client -> { + var changes = watcher.takeChanges(); + if (!changes.isEmpty()) { + applyChanges(changes); + } + }); + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> watcher.close()); + for (var page : watcher.loadAll()) { + developmentPages.put(page.getId(), page); + } + } + + private void applyChanges(List changes) { + // Enrich each change with the previous page data while we process them + for (int i = 0; i < changes.size(); i++) { + var change = changes.get(i); + var pageId = change.pageId(); + + var oldPage = change.newPage() != null ? developmentPages.put(pageId, change.newPage()) + : developmentPages.remove(pageId); + changes.set(i, new GuidePageChange(pageId, oldPage, change.newPage())); + } + + // Allow indices to rebuild + var allPages = new ArrayList(pages.size() + developmentPages.size()); + allPages.addAll(pages.values()); + allPages.addAll(developmentPages.values()); + for (var index : indices) { + if (index.supportsUpdate()) { + index.update(allPages, changes); + } else { + index.rebuild(allPages); + } + } + + // Rebuild navigation + this.navigationTree = buildNavigation(); + + // Reload the current page if it has been changed + if (Minecraft.getInstance().screen instanceof GuideScreen guideScreen) { + var currentPageId = guideScreen.getCurrentPageId(); + if (changes.stream().anyMatch(c -> c.pageId().equals(currentPageId))) { + guideScreen.reloadPage(); + } + } + } + + private NavigationTree buildNavigation() { + if (developmentPages.isEmpty()) { + return NavigationTree.build(pages.values()); + } else { + var allPages = new HashMap<>(pages); + allPages.putAll(developmentPages); + return NavigationTree.build(allPages.values()); + } + } + +} diff --git a/src/main/java/appeng/client/guidebook/GuidePage.java b/src/main/java/appeng/client/guidebook/GuidePage.java new file mode 100644 index 00000000000..a384baa4a20 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/GuidePage.java @@ -0,0 +1,30 @@ +package appeng.client.guidebook; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.document.block.LytDocument; + +public class GuidePage { + private final String sourcePack; + private final ResourceLocation id; + + private LytDocument document; + + public GuidePage(String sourcePack, ResourceLocation id, LytDocument document) { + this.sourcePack = sourcePack; + this.id = id; + this.document = document; + } + + public String getSourcePack() { + return sourcePack; + } + + public ResourceLocation getId() { + return id; + } + + public LytDocument getDocument() { + return document; + } +} diff --git a/src/main/java/appeng/client/guidebook/GuidePageChange.java b/src/main/java/appeng/client/guidebook/GuidePageChange.java new file mode 100644 index 00000000000..1734148cf9b --- /dev/null +++ b/src/main/java/appeng/client/guidebook/GuidePageChange.java @@ -0,0 +1,13 @@ +package appeng.client.guidebook; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.compiler.ParsedGuidePage; + +public record GuidePageChange( + ResourceLocation pageId, + @Nullable ParsedGuidePage oldPage, + @Nullable ParsedGuidePage newPage) { +} diff --git a/src/main/java/appeng/client/guidebook/GuideSourceWatcher.java b/src/main/java/appeng/client/guidebook/GuideSourceWatcher.java new file mode 100644 index 00000000000..b677f7b5b2e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/GuideSourceWatcher.java @@ -0,0 +1,250 @@ +package appeng.client.guidebook; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.ParsedGuidePage; +import appeng.core.AppEng; +import appeng.shaded.directorywatcher.watcher.DirectoryChangeEvent; +import appeng.shaded.directorywatcher.watcher.DirectoryChangeListener; +import appeng.shaded.directorywatcher.watcher.DirectoryWatcher; + +class GuideSourceWatcher { + private static final Logger LOGGER = LoggerFactory.getLogger(GuideSourceWatcher.class); + + /** + * The {@link ResourceLocation} namespace to use for files in the watched folder. + */ + private final String namespace; + + private final Path sourceFolder; + + // Recursive directory watcher for the guidebook sources. + @Nullable + private final DirectoryWatcher sourceWatcher; + + // Queued changes that come in from a separate thread + private final Map changedPages = new HashMap<>(); + private final Set deletedPages = new HashSet<>(); + + private final ExecutorService watchExecutor; + + public GuideSourceWatcher(String namespace, Path sourceFolder) { + this.namespace = namespace; + this.sourceFolder = sourceFolder; + if (!Files.isDirectory(sourceFolder)) { + throw new RuntimeException("Cannot find the specified folder for the AE2 guidebook sources: " + + sourceFolder); + } + LOGGER.info("Watching guidebook sources in {}", sourceFolder); + + watchExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("AE2GuidebookWatcher%d") + .build()); + + // Watch the folder recursively in a separate thread, queue up any changes and apply them + // in the client tick. + DirectoryWatcher watcher; + try { + watcher = DirectoryWatcher.builder() + .path(sourceFolder) + .fileHashing(false) + .listener(new Listener()) + .build(); + } catch (IOException e) { + LOGGER.error("Failed to watch for changes in the guidebook sources at {}", sourceFolder, e); + watcher = null; + } + sourceWatcher = watcher; + + // Actually process changes in the client tick to prevent race conditions and other crashes + if (sourceWatcher != null) { + sourceWatcher.watchAsync(watchExecutor); + } + } + + public List loadAll() { + var stopwatch = Stopwatch.createStarted(); + + // Find all potential pages + var pagesToLoad = new HashMap(); + try { + Files.walkFileTree(sourceFolder, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + var pageId = getPageId(file); + if (pageId != null) { + pagesToLoad.put(pageId, file); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + LOGGER.error("Failed to list page {}", file, exc); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + if (exc != null) { + LOGGER.error("Failed to list all pages in {}", dir, exc); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOGGER.error("Failed to list all pages in {}", sourceFolder, e); + } + + LOGGER.info("Loading {} guidebook pages", pagesToLoad.size()); + var loadedPages = pagesToLoad.entrySet() + .stream() + .map(entry -> { + var path = entry.getValue(); + try (var in = Files.newInputStream(path)) { + return PageCompiler.parse(AppEng.MOD_ID, entry.getKey(), in); + + } catch (Exception e) { + LOGGER.error("Failed to reload guidebook page {}", path, e); + return null; + } + }) + .filter(Objects::nonNull) + .toList(); + + LOGGER.info("Loaded {} pages from {} in {}", loadedPages.size(), sourceFolder, stopwatch); + + return loadedPages; + } + + public synchronized List takeChanges() { + + if (deletedPages.isEmpty() && changedPages.isEmpty()) { + return List.of(); + } + + var changes = new ArrayList(); + + for (var deletedPage : deletedPages) { + changes.add(new GuidePageChange(deletedPage, null, null)); + } + deletedPages.clear(); + + for (var changedPage : changedPages.values()) { + changes.add(new GuidePageChange(changedPage.getId(), null, changedPage)); + } + changedPages.clear(); + + return changes; + } + + public synchronized void close() { + changedPages.clear(); + deletedPages.clear(); + watchExecutor.shutdown(); + + if (sourceWatcher != null) { + try { + sourceWatcher.close(); + } catch (IOException e) { + LOGGER.error("Failed to close fileystem watcher for {}", sourceFolder); + } + } + } + + private class Listener implements DirectoryChangeListener { + @Override + public void onEvent(DirectoryChangeEvent event) { + if (event.isDirectory()) { + return; + } + switch (event.eventType()) { + case CREATE, MODIFY -> pageChanged(event.path()); + case DELETE -> pageDeleted(event.path()); + } + } + + @Override + public boolean isWatching() { + return sourceWatcher != null && !sourceWatcher.isClosed(); + } + + @Override + public void onException(Exception e) { + LOGGER.error("Failed watching for changes", e); + } + } + + // Only call while holding the lock! + private synchronized void pageChanged(Path path) { + var pageId = getPageId(path); + if (pageId == null) { + return; // Probably not a page + } + + // If it was previously deleted in the same change-set, undelete it + deletedPages.remove(pageId); + + try (var in = Files.newInputStream(path)) { + var page = PageCompiler.parse(AppEng.MOD_ID, pageId, in); + changedPages.put(pageId, page); + } catch (Exception e) { + LOGGER.error("Failed to reload guidebook page {}", path, e); + } + } + + // Only call while holding the lock! + private synchronized void pageDeleted(Path path) { + var pageId = getPageId(path); + if (pageId == null) { + return; // Probably not a page + } + + // If it was previously changed in the same change-set, remove the change + changedPages.remove(pageId); + deletedPages.add(pageId); + } + + @Nullable + private ResourceLocation getPageId(Path path) { + var relativePath = sourceFolder.relativize(path); + var relativePathStr = relativePath.toString().replace('\\', '/'); + if (!relativePathStr.endsWith(".md")) { + return null; + } + if (!ResourceLocation.isValidResourceLocation(relativePathStr)) { + return null; + } + return new ResourceLocation(namespace, relativePathStr); + } +} diff --git a/src/main/java/appeng/client/guidebook/PageAnchor.java b/src/main/java/appeng/client/guidebook/PageAnchor.java new file mode 100644 index 00000000000..7869bb7a8c8 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/PageAnchor.java @@ -0,0 +1,17 @@ +package appeng.client.guidebook; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.resources.ResourceLocation; + +/** + * Points to a guidebook page with an optional anchor within that page. + * + * @param pageId + * @param anchor ID of an anchor in the page. + */ +public record PageAnchor(ResourceLocation pageId, @Nullable String anchor) { + public static PageAnchor page(ResourceLocation pageId) { + return new PageAnchor(pageId, null); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/Frontmatter.java b/src/main/java/appeng/client/guidebook/compiler/Frontmatter.java new file mode 100644 index 00000000000..dc70494ad7b --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/Frontmatter.java @@ -0,0 +1,77 @@ +package appeng.client.guidebook.compiler; + +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +import appeng.shaded.snakeyaml.LoaderOptions; +import appeng.shaded.snakeyaml.Yaml; +import appeng.shaded.snakeyaml.constructor.SafeConstructor; + +public record Frontmatter( + FrontmatterNavigation navigationEntry, + Map additionalProperties) { + public static Frontmatter parse(ResourceLocation pageId, String yamlText) { + var yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + + FrontmatterNavigation navigation = null; + Map data = yaml.load(yamlText); + var navigationObj = data.remove("navigation"); + if (navigationObj != null) { + if (!(navigationObj instanceof MapnavigationMap)) { + throw new IllegalArgumentException("The navigation key in the frontmatter has to be a map"); + } + + var title = getString(navigationMap, "title"); + if (title == null) { + throw new IllegalArgumentException("title is missing in navigation frontmatter"); + } + var parentIdStr = getString(navigationMap, "parent"); + var position = 0; + if (navigationMap.containsKey("position")) { + position = getInt(navigationMap, "position"); + } + var iconIdStr = getString(navigationMap, "icon"); + CompoundTag iconNbt = null; // TODO Icon NBT + + ResourceLocation parentId = null; + if (parentIdStr != null) { + parentId = IdUtils.resolveId(parentIdStr, pageId.getNamespace()); + } + + ResourceLocation iconId = null; + if (iconIdStr != null) { + iconId = IdUtils.resolveId(iconIdStr, pageId.getNamespace()); + } + + navigation = new FrontmatterNavigation(title, parentId, position, iconId, iconNbt); + } + + return new Frontmatter( + navigation, + Map.copyOf(data)); + } + + @Nullable + private static String getString(Map map, String key) { + var value = map.get(key); + if (value != null && !(value instanceof String)) { + throw new IllegalArgumentException("Key " + key + " has to be a String!"); + } + return (String) value; + } + + private static int getInt(Map map, String key) { + var value = map.get(key); + if (value == null) { + throw new IllegalArgumentException("Key " + key + " is missing in navigation frontmatter"); + } + if (!(value instanceof Number number)) { + throw new IllegalArgumentException("Key " + key + " has to be a number!"); + } + return number.intValue(); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/FrontmatterNavigation.java b/src/main/java/appeng/client/guidebook/compiler/FrontmatterNavigation.java new file mode 100644 index 00000000000..b0cef969f94 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/FrontmatterNavigation.java @@ -0,0 +1,17 @@ +package appeng.client.guidebook.compiler; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; + +/** + * Inserts a page into the navigation tree. Null parent means top-level category. + */ +public record FrontmatterNavigation( + String title, + @Nullable ResourceLocation parent, + int position, + @Nullable ResourceLocation iconItemId, + @Nullable CompoundTag iconNbt) { +} diff --git a/src/main/java/appeng/client/guidebook/compiler/IdUtils.java b/src/main/java/appeng/client/guidebook/compiler/IdUtils.java new file mode 100644 index 00000000000..b380aefac2e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/IdUtils.java @@ -0,0 +1,41 @@ +package appeng.client.guidebook.compiler; + +import java.net.URI; + +import net.minecraft.resources.ResourceLocation; + +/** + * Helper to resolve shorthand and relative IDs found in markdown pages. + */ +public final class IdUtils { + + private IdUtils() { + } + + public static ResourceLocation resolveId(String idText, String defaultNamespace) { + if (!idText.contains(":")) { + return new ResourceLocation(defaultNamespace, idText); + } + return new ResourceLocation(idText); + } + + /** + * Supports relative resource locations such as: ./somepath, which would resolve relative to a given anchor + * location. Relative locations must not be namespaced since we would otherwise run into the problem if namespaced + * locations potentially having a different namespace than the anchor. + */ + public static ResourceLocation resolveLink(String idText, ResourceLocation anchor) { + if (!idText.contains(":")) { + URI uri = URI.create(anchor.getPath()); + uri = uri.resolve(idText); + + var relativeId = uri.toString(); + + return new ResourceLocation(anchor.getNamespace(), relativeId); + } + + // if it contains a ":" it's assumed to be absolute + return new ResourceLocation(idText); + } + +} diff --git a/src/main/java/appeng/client/guidebook/compiler/PageCompiler.java b/src/main/java/appeng/client/guidebook/compiler/PageCompiler.java new file mode 100644 index 00000000000..6b1586d8805 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/PageCompiler.java @@ -0,0 +1,452 @@ +package appeng.client.guidebook.compiler; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.ResourceLocationException; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmLinkScreen; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.GuideManager; +import appeng.client.guidebook.GuidePage; +import appeng.client.guidebook.PageAnchor; +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.block.LytDocument; +import appeng.client.guidebook.document.block.LytHeading; +import appeng.client.guidebook.document.block.LytImage; +import appeng.client.guidebook.document.block.LytList; +import appeng.client.guidebook.document.block.LytListItem; +import appeng.client.guidebook.document.block.LytParagraph; +import appeng.client.guidebook.document.block.LytThematicBreak; +import appeng.client.guidebook.document.block.table.LytTable; +import appeng.client.guidebook.document.flow.LytFlowBreak; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.document.flow.LytFlowInlineBlock; +import appeng.client.guidebook.document.flow.LytFlowLink; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.client.guidebook.document.flow.LytFlowSpan; +import appeng.client.guidebook.document.flow.LytFlowText; +import appeng.client.guidebook.document.interaction.TextTooltip; +import appeng.client.guidebook.render.ColorRef; +import appeng.client.guidebook.style.TextAlignment; +import appeng.client.guidebook.style.WhiteSpaceMode; +import appeng.libs.mdast.MdAst; +import appeng.libs.mdast.MdAstYamlFrontmatter; +import appeng.libs.mdast.MdastOptions; +import appeng.libs.mdast.YamlFrontmatterExtension; +import appeng.libs.mdast.gfm.GfmTableMdastExtension; +import appeng.libs.mdast.gfm.model.GfmTable; +import appeng.libs.mdast.mdx.MdxMdastExtension; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; +import appeng.libs.mdast.model.MdAstAnyContent; +import appeng.libs.mdast.model.MdAstBreak; +import appeng.libs.mdast.model.MdAstCode; +import appeng.libs.mdast.model.MdAstEmphasis; +import appeng.libs.mdast.model.MdAstHeading; +import appeng.libs.mdast.model.MdAstImage; +import appeng.libs.mdast.model.MdAstInlineCode; +import appeng.libs.mdast.model.MdAstLink; +import appeng.libs.mdast.model.MdAstList; +import appeng.libs.mdast.model.MdAstListItem; +import appeng.libs.mdast.model.MdAstNode; +import appeng.libs.mdast.model.MdAstParagraph; +import appeng.libs.mdast.model.MdAstParent; +import appeng.libs.mdast.model.MdAstPhrasingContent; +import appeng.libs.mdast.model.MdAstPosition; +import appeng.libs.mdast.model.MdAstRoot; +import appeng.libs.mdast.model.MdAstStrong; +import appeng.libs.mdast.model.MdAstText; +import appeng.libs.mdast.model.MdAstThematicBreak; +import appeng.libs.mdx.MdxSyntax; +import appeng.libs.micromark.extensions.YamlFrontmatterSyntax; +import appeng.libs.micromark.extensions.gfm.GfmTableSyntax; +import appeng.libs.unist.UnistNode; + +public final class PageCompiler { + private static final Logger LOGGER = LoggerFactory.getLogger(PageCompiler.class); + + /** + * Default gap between block-level elements. Set as margin. + */ + private static final int DEFAULT_ELEMENT_SPACING = 5; + + private final Function assetLoader; + private final String sourcePack; + private final ResourceLocation id; + private final String pageContent; + + public PageCompiler(Function assetLoader, + String sourcePack, + ResourceLocation id, + String pageContent) { + this.assetLoader = assetLoader; + this.sourcePack = sourcePack; + this.id = id; + this.pageContent = pageContent; + } + + public static ParsedGuidePage parse(String sourcePack, ResourceLocation id, InputStream in) throws IOException { + String pageContent = new String(in.readAllBytes(), StandardCharsets.UTF_8); + + var options = new MdastOptions() + .withSyntaxExtension(MdxSyntax.INSTANCE) + .withSyntaxExtension(YamlFrontmatterSyntax.INSTANCE) + .withSyntaxExtension(GfmTableSyntax.INSTANCE) + .withMdastExtension(MdxMdastExtension.INSTANCE) + .withMdastExtension(YamlFrontmatterExtension.INSTANCE) + .withMdastExtension(GfmTableMdastExtension.INSTANCE); + + var astRoot = MdAst.fromMarkdown(pageContent, options); + + // Find front-matter + var frontmatter = parseFrontmatter(id, astRoot); + + return new ParsedGuidePage(sourcePack, id, pageContent, astRoot, frontmatter); + } + + public static GuidePage compile(Function resourceLookup, ParsedGuidePage parsedPage) { + // Translate page tree over to layout pages + var document = new PageCompiler(resourceLookup, parsedPage.sourcePack, parsedPage.id, parsedPage.source) + .compile(parsedPage.astRoot); + + return new GuidePage(parsedPage.sourcePack, parsedPage.id, document); + } + + private LytDocument compile(MdAstRoot root) { + var document = new LytDocument(); + compileBlockContext(root, document); + return document; + } + + private static Frontmatter parseFrontmatter(ResourceLocation pageId, MdAstRoot root) { + Frontmatter result = null; + + for (var child : root.children()) { + if (child instanceof MdAstYamlFrontmatter frontmatter) { + if (result != null) { + LOGGER.error("Found more than one frontmatter!"); // TODO: proper debugging + continue; + } + try { + result = Frontmatter.parse(pageId, frontmatter.value); + } catch (Exception e) { + LOGGER.error("Failed to parse frontmatter for page {}", pageId, e); + break; + } + } + } + + return Objects.requireNonNullElse(result, new Frontmatter(null, Map.of())); + } + + public void compileBlockContext(MdAstParent markdownParent, LytBlockContainer layoutParent) { + LytBlock previousLayoutChild = null; + for (var child : markdownParent.children()) { + LytBlock layoutChild; + if (child instanceof MdAstThematicBreak) { + layoutChild = new LytThematicBreak(); + } else if (child instanceof MdAstList astList) { + layoutChild = compileList(astList); + } else if (child instanceof MdAstCode astCode) { + var paragraph = new LytParagraph(); + paragraph.modifyStyle(style -> style.italic(true).whiteSpace(WhiteSpaceMode.PRE)); + paragraph.setMarginLeft(5); + paragraph.appendText(astCode.value); + layoutChild = paragraph; + } else if (child instanceof MdAstHeading astHeading) { + var heading = new LytHeading(); + heading.setDepth(astHeading.depth); + compileFlowContext(astHeading, heading); + layoutChild = heading; + } else if (child instanceof MdAstParagraph astParagraph) { + var paragraph = new LytParagraph(); + compileFlowContext(astParagraph, paragraph); + paragraph.setMarginTop(DEFAULT_ELEMENT_SPACING); + paragraph.setMarginBottom(DEFAULT_ELEMENT_SPACING); + layoutChild = paragraph; + } else if (child instanceof MdAstYamlFrontmatter) { + // This is handled by compile directly + layoutChild = null; + } else if (child instanceof GfmTable astTable) { + layoutChild = compileTable(astTable); + } else if (child instanceof MdxJsxFlowElement el) { + var compiler = TagCompilers.get(el.name()); + if (compiler == null) { + layoutChild = createErrorBlock("Unhandled MDX element in block context", (MdAstNode) child); + } else { + layoutChild = null; + compiler.compileBlockContext(this, layoutParent, el); + } + } else if (child instanceof MdAstPhrasingContent phrasingContent) { + // Wrap in a paragraph with no margins, but try appending to an existing paragraph before this + if (previousLayoutChild instanceof LytParagraph paragraph) { + compileFlowContent(paragraph, phrasingContent); + continue; + } else { + var paragraph = new LytParagraph(); + compileFlowContent(paragraph, phrasingContent); + layoutChild = paragraph; + } + } else { + layoutChild = createErrorBlock("Unhandled Markdown node in block context", (MdAstNode) child); + } + + if (layoutChild != null) { + layoutParent.append(layoutChild); + } + previousLayoutChild = layoutChild; + } + } + + private LytList compileList(MdAstList astList) { + var list = new LytList(astList.ordered, astList.start); + for (var listContent : astList.children()) { + if (listContent instanceof MdAstListItem astListItem) { + var listItem = new LytListItem(); + compileBlockContext(astListItem, listItem); + + // Fix up top/bottom margin for list item children + var children = listItem.getChildren(); + if (!children.isEmpty()) { + var firstChild = children.get(0); + if (firstChild instanceof LytBlock firstBlock) { + firstBlock.setMarginTop(0); + firstBlock.setMarginBottom(0); + } + } + list.append(listItem); + } else { + list.append(createErrorBlock("Cannot handle list content", (MdAstNode) listContent)); + } + } + return list; + } + + private LytBlock compileTable(GfmTable astTable) { + var table = new LytTable(); + table.setMarginBottom(DEFAULT_ELEMENT_SPACING); + + boolean firstRow = true; + for (var astRow : astTable.children()) { + var row = table.appendRow(); + if (firstRow) { + row.modifyStyle(style -> style.bold(true)); + firstRow = false; + } + + var astCells = astRow.children(); + for (int i = 0; i < astCells.size(); i++) { + var cell = row.appendCell(); + // Apply alignment + if (astTable.align != null && i < astTable.align.size()) { + switch (astTable.align.get(i)) { + case CENTER -> cell.modifyStyle(style -> style.alignment(TextAlignment.CENTER)); + case RIGHT -> cell.modifyStyle(style -> style.alignment(TextAlignment.RIGHT)); + } + } + + compileBlockContext(astCells.get(i), cell); + } + } + + return table; + } + + /** + * Converts formatted Minecraft text into our flow content. + */ + public void compileComponentToFlow(FormattedText formattedText, LytFlowParent layoutParent) { + formattedText.visit((style, text) -> { + if (style.isEmpty()) { + layoutParent.appendText(text); + } else { + var span = new LytFlowSpan(); + // TODO: Convert style + span.appendText(text); + layoutParent.append(span); + } + return Optional.empty(); + }, Style.EMPTY); + } + + public void compileFlowContext(MdAstParent markdownParent, LytFlowParent layoutParent) { + for (var child : markdownParent.children()) { + compileFlowContent(layoutParent, child); + } + } + + private void compileFlowContent(LytFlowParent layoutParent, MdAstAnyContent content) { + LytFlowContent layoutChild; + if (content instanceof MdAstText astText) { + var text = new LytFlowText(); + text.setText(astText.value); + layoutChild = text; + } else if (content instanceof MdAstInlineCode astCode) { + var text = new LytFlowText(); + text.setText(astCode.value); + text.modifyStyle(style -> style.italic(true).whiteSpace(WhiteSpaceMode.PRE)); + layoutChild = text; + } else if (content instanceof MdAstStrong astStrong) { + var span = new LytFlowSpan(); + span.modifyStyle(style -> style.bold(true)); + compileFlowContext(astStrong, span); + layoutChild = span; + } else if (content instanceof MdAstEmphasis astEmphasis) { + var span = new LytFlowSpan(); + span.modifyStyle(style -> style.italic(true)); + compileFlowContext(astEmphasis, span); + layoutChild = span; + } else if (content instanceof MdAstBreak) { + layoutChild = new LytFlowBreak(); + } else if (content instanceof MdAstLink astLink) { + layoutChild = compileLink(astLink); + } else if (content instanceof MdAstImage astImage) { + var inlineBlock = new LytFlowInlineBlock(); + inlineBlock.setBlock(compileImage(astImage)); + layoutChild = inlineBlock; + } else if (content instanceof MdxJsxTextElement el) { + var compiler = TagCompilers.get(el.name()); + if (compiler == null) { + layoutChild = createErrorFlowContent("Unhandled MDX element in flow context", (MdAstNode) content); + } else { + layoutChild = null; + compiler.compileFlowContext(this, layoutParent, el); + } + } else { + layoutChild = createErrorFlowContent("Unhandled Markdown node in flow context", (MdAstNode) content); + } + + if (layoutChild != null) { + layoutParent.append(layoutChild); + } + } + + private LytFlowContent compileLink(MdAstLink astLink) { + var link = new LytFlowLink(); + if (astLink.title != null && !astLink.title.isEmpty()) { + link.setTooltip(new TextTooltip(astLink.title)); + } + + // Internal vs. external links + var uri = URI.create(astLink.url); + if (uri.isAbsolute()) { + link.setClickCallback(screen -> { + var mc = Minecraft.getInstance(); + mc.setScreen(new ConfirmLinkScreen(yes -> { + if (yes) { + Util.getPlatform().openUri(uri); + } + + mc.setScreen(screen); + }, astLink.url, false)); + }); + } else { + + // Determine the page id, account for relative paths + ResourceLocation pageId; + try { + pageId = IdUtils.resolveLink(uri.getPath(), id); + } catch (ResourceLocationException ignored) { + return createErrorFlowContent("Invalid page link", astLink); + } + + if (!GuideManager.INSTANCE.pageExists(pageId)) { + LOGGER.error("Broken link to page '{}' in page {}", astLink.url, id); + } else { + var anchor = new PageAnchor(pageId, uri.getFragment()); + link.setClickCallback(screen -> { + screen.navigateTo(anchor); + }); + } + } + + compileFlowContext(astLink, link); + return link; + } + + @NotNull + private LytImage compileImage(MdAstImage astImage) { + var image = new LytImage(); + image.setTitle(astImage.title); + image.setAlt(astImage.alt); + try { + var imageId = IdUtils.resolveLink(astImage.url, id); + var imageContent = assetLoader.apply(imageId); + if (imageContent == null) { + LOGGER.error("Couldn't find image {}", astImage.url); + image.setTitle("Missing image: " + astImage.url); + } + image.setImage(imageId, imageContent); + } catch (ResourceLocationException e) { + LOGGER.error("Invalid image id: {}", astImage.url); + image.setTitle("Invalid image URL: " + astImage.url); + } + return image; + } + + public LytBlock createErrorBlock(String text, UnistNode child) { + var paragraph = new LytParagraph(); + paragraph.append(createErrorFlowContent(text, child)); + return paragraph; + } + + public LytFlowContent createErrorFlowContent(String text, UnistNode child) { + LytFlowSpan span = new LytFlowSpan(); + span.modifyStyle(style -> { + style.color(new ColorRef(0xFFFF0000)) + .whiteSpace(WhiteSpaceMode.PRE); + }); + + // Find the position in the source + var pos = child.position().start(); + var startOfLine = pageContent.lastIndexOf('\n', pos.offset()) + 1; + var endOfLine = pageContent.indexOf('\n', pos.offset() + 1); + if (endOfLine == -1) { + endOfLine = pageContent.length(); + } + var line = pageContent.substring(startOfLine, endOfLine); + + text += " " + child.type() + " (" + MdAstPosition.stringify(pos) + ")"; + + span.appendText(text); + span.appendBreak(); + + span.appendText(line); + span.appendBreak(); + + span.appendText("~".repeat(pos.column() - 1) + "^"); + span.appendBreak(); + + LOGGER.warn("{}\n{}\n{}\n", text, line, "~".repeat(pos.column() - 1) + "^"); + + return span; + } + + public ResourceLocation resolveId(String idText) { + return IdUtils.resolveId(idText, id.getNamespace()); + } + + public ResourceLocation getId() { + return id; + } + + public byte[] loadAsset(ResourceLocation imageId) { + return assetLoader.apply(imageId); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/ParsedGuidePage.java b/src/main/java/appeng/client/guidebook/compiler/ParsedGuidePage.java new file mode 100644 index 00000000000..78e6a57d7a4 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/ParsedGuidePage.java @@ -0,0 +1,43 @@ +package appeng.client.guidebook.compiler; + +import net.minecraft.resources.ResourceLocation; + +import appeng.libs.mdast.model.MdAstRoot; + +public class ParsedGuidePage { + final String sourcePack; + final ResourceLocation id; + final String source; + final MdAstRoot astRoot; + final Frontmatter frontmatter; + + public ParsedGuidePage(String sourcePack, ResourceLocation id, String source, MdAstRoot astRoot, + Frontmatter frontmatter) { + this.sourcePack = sourcePack; + this.id = id; + this.source = source; + this.astRoot = astRoot; + this.frontmatter = frontmatter; + } + + public String getSourcePack() { + return sourcePack; + } + + public ResourceLocation getId() { + return id; + } + + public Frontmatter getFrontmatter() { + return frontmatter; + } + + @Override + public String toString() { + if (id.getNamespace().equals(sourcePack)) { + return id.toString(); + } else { + return id + " (from " + sourcePack + ")"; + } + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/TagCompiler.java b/src/main/java/appeng/client/guidebook/compiler/TagCompiler.java new file mode 100644 index 00000000000..39575681980 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/TagCompiler.java @@ -0,0 +1,16 @@ +package appeng.client.guidebook.compiler; + +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; + +public interface TagCompiler { + default void compileBlockContext(PageCompiler compiler, LytBlockContainer parent, MdxJsxFlowElement el) { + parent.append(compiler.createErrorBlock("Cannot use MDX tag " + el.name + " in block context", el)); + } + + default void compileFlowContext(PageCompiler compiler, LytFlowParent parent, MdxJsxTextElement el) { + parent.append(compiler.createErrorFlowContent("Cannot use MDX tag " + el.name() + " in flow context", el)); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/TagCompilers.java b/src/main/java/appeng/client/guidebook/compiler/TagCompilers.java new file mode 100644 index 00000000000..6c38a89f6c7 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/TagCompilers.java @@ -0,0 +1,52 @@ +package appeng.client.guidebook.compiler; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import appeng.client.guidebook.compiler.tags.ATagCompiler; +import appeng.client.guidebook.compiler.tags.BreakCompiler; +import appeng.client.guidebook.compiler.tags.CategoryIndexCompiler; +import appeng.client.guidebook.compiler.tags.DivTagCompiler; +import appeng.client.guidebook.compiler.tags.FloatingImageCompiler; +import appeng.client.guidebook.compiler.tags.ItemGridCompiler; +import appeng.client.guidebook.compiler.tags.ItemLinkCompiler; +import appeng.client.guidebook.compiler.tags.RecipeForCompiler; + +/** + * Maintains a mapping between MDX Tag-Names to handlers for compiling these tags. + */ +public final class TagCompilers { + private static final Map handlers = new HashMap<>(); + + static { + register("div", new DivTagCompiler()); + register("a", new ATagCompiler()); + register("ItemLink", new ItemLinkCompiler()); + register("FloatingImage", new FloatingImageCompiler()); + register("br", new BreakCompiler()); + register("RecipeFor", new RecipeForCompiler()); + register("ItemGrid", new ItemGridCompiler()); + register("CategoryIndex", new CategoryIndexCompiler()); + } + + public static void register(String tagName, TagCompiler handler) { + tagName = normalizeTagName(tagName); + if (handlers.containsKey(tagName)) { + throw new IllegalStateException("MDX handler for tag " + tagName + " is already registered"); + } + handlers.put(tagName, handler); + } + + public static TagCompiler get(String tagName) { + return handlers.get(normalizeTagName(tagName)); + } + + public static void remove(String tagName) { + handlers.remove(normalizeTagName(tagName)); + } + + private static String normalizeTagName(String tagName) { + return tagName.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/ATagCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/ATagCompiler.java new file mode 100644 index 00000000000..dbb496972db --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/ATagCompiler.java @@ -0,0 +1,17 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.TagCompiler; +import appeng.client.guidebook.document.flow.LytFlowLink; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; + +public class ATagCompiler implements TagCompiler { + @Override + public void compileFlowContext(PageCompiler compiler, LytFlowParent parent, MdxJsxTextElement el) { + var link = new LytFlowLink(); + // TODO: HREF, TITLE + compiler.compileFlowContext(el, link); + parent.append(link); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/BlockTagCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/BlockTagCompiler.java new file mode 100644 index 00000000000..9f988cdeab0 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/BlockTagCompiler.java @@ -0,0 +1,32 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.TagCompiler; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.flow.LytFlowInlineBlock; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; + +/** + * Compiler base-class for tag compilers that compile block content but allow the block content to be used in flow + * context by wrapping it in an inline block. + */ +public abstract class BlockTagCompiler implements TagCompiler { + protected abstract void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el); + + @Override + public final void compileFlowContext(PageCompiler compiler, LytFlowParent parent, MdxJsxTextElement el) { + compile(compiler, node -> { + var inlineBlock = new LytFlowInlineBlock(); + inlineBlock.setBlock(node); + parent.append(inlineBlock); + }, el); + } + + @Override + public final void compileBlockContext(PageCompiler compiler, LytBlockContainer parent, MdxJsxFlowElement el) { + compile(compiler, parent, el); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/BreakCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/BreakCompiler.java new file mode 100644 index 00000000000..86675d0303a --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/BreakCompiler.java @@ -0,0 +1,28 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.flow.LytFlowBreak; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.libs.mdast.model.MdAstNode; + +public class BreakCompiler extends FlowTagCompiler { + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var br = new LytFlowBreak(); + var clear = el.getAttributeString("clear", "none"); + switch (clear) { + case "left" -> br.setClearLeft(true); + case "right" -> br.setClearRight(true); + case "all" -> { + br.setClearLeft(true); + br.setClearRight(true); + } + case "none" -> { + } + default -> parent.append(compiler.createErrorFlowContent("Invalid 'clear' attribute", (MdAstNode) el)); + } + + parent.append(br); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/CategoryIndexCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/CategoryIndexCompiler.java new file mode 100644 index 00000000000..13e37a7ce85 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/CategoryIndexCompiler.java @@ -0,0 +1,44 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.GuideManager; +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.block.LytList; +import appeng.client.guidebook.document.block.LytListItem; +import appeng.client.guidebook.document.block.LytParagraph; +import appeng.client.guidebook.document.flow.LytFlowLink; +import appeng.client.guidebook.indices.CategoryIndex; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; + +public class CategoryIndexCompiler extends BlockTagCompiler { + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + + var category = el.getAttributeString("category", null); + if (category == null) { + parent.appendError(compiler, "Missing category", el); + return; + } + + var categories = CategoryIndex.INSTANCE.get(category); + + var list = new LytList(false, 0); + for (var pageAnchor : categories) { + var page = GuideManager.INSTANCE.getParsedPage(pageAnchor.pageId()); + + var listItem = new LytListItem(); + var listItemPar = new LytParagraph(); + if (page == null) { + listItemPar.appendText("Unknown page id: " + pageAnchor.pageId()); + } else { + var link = new LytFlowLink(); + link.setClickCallback(guideScreen -> guideScreen.navigateTo(pageAnchor)); + link.appendText(page.getFrontmatter().navigationEntry().title()); + listItemPar.append(link); + } + listItem.append(listItemPar); + list.append(listItem); + } + parent.append(list); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/DivTagCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/DivTagCompiler.java new file mode 100644 index 00000000000..904be7149a1 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/DivTagCompiler.java @@ -0,0 +1,13 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.TagCompiler; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; + +public class DivTagCompiler implements TagCompiler { + @Override + public void compileBlockContext(PageCompiler compiler, LytBlockContainer parent, MdxJsxFlowElement el) { + compiler.compileBlockContext(el, parent); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/FloatingImageCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/FloatingImageCompiler.java new file mode 100644 index 00000000000..005987089e8 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/FloatingImageCompiler.java @@ -0,0 +1,65 @@ +package appeng.client.guidebook.compiler.tags; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.ResourceLocationException; + +import appeng.client.guidebook.compiler.IdUtils; +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.block.LytImage; +import appeng.client.guidebook.document.flow.InlineBlockAlignment; +import appeng.client.guidebook.document.flow.LytFlowInlineBlock; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.libs.mdast.model.MdAstNode; + +public class FloatingImageCompiler extends FlowTagCompiler { + private static final Logger LOGGER = LoggerFactory.getLogger(FloatingImageCompiler.class); + + @Override + protected void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var src = el.getAttributeString("src", null); + var align = el.getAttributeString("align", "left"); + var title = el.getAttributeString("title", null); + + var image = new LytImage(); + if (title != null) { + image.setTitle(title); + } + try { + var imageId = IdUtils.resolveLink(src, compiler.getId()); + var imageContent = compiler.loadAsset(imageId); + if (imageContent == null) { + LOGGER.error("Couldn't find image {}", src); + image.setTitle("Missing image: " + src); + } + image.setImage(imageId, imageContent); + } catch (ResourceLocationException e) { + LOGGER.error("Invalid image id: {}", src); + image.setTitle("Invalid image URL: " + src); + } + + // Wrap it in a flow content inline block + var inlineBlock = new LytFlowInlineBlock(); + inlineBlock.setBlock(image); + switch (align) { + case "left" -> { + inlineBlock.setAlignment(InlineBlockAlignment.FLOAT_LEFT); + image.setMarginRight(5); + image.setMarginBottom(5); + } + case "right" -> { + inlineBlock.setAlignment(InlineBlockAlignment.FLOAT_RIGHT); + image.setMarginLeft(5); + image.setMarginBottom(5); + } + default -> { + parent.append(compiler.createErrorFlowContent("Invalid align. Must be left or right.", (MdAstNode) el)); + return; + } + } + + parent.append(inlineBlock); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/FlowTagCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/FlowTagCompiler.java new file mode 100644 index 00000000000..b396d31a852 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/FlowTagCompiler.java @@ -0,0 +1,30 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.compiler.TagCompiler; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.block.LytParagraph; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.libs.mdast.mdx.model.MdxJsxFlowElement; +import appeng.libs.mdast.mdx.model.MdxJsxTextElement; + +/** + * Compiler base-class for tag compilers that compile flow content but allow the flow content to be used in block + * context by wrapping it in a paragraph. + */ +public abstract class FlowTagCompiler implements TagCompiler { + protected abstract void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el); + + @Override + public void compileFlowContext(PageCompiler compiler, LytFlowParent parent, MdxJsxTextElement el) { + compile(compiler, parent, el); + } + + @Override + public final void compileBlockContext(PageCompiler compiler, LytBlockContainer parent, MdxJsxFlowElement el) { + var paragraph = new LytParagraph(); + compile(compiler, paragraph, el); + parent.append(paragraph); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/ItemGridCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/ItemGridCompiler.java new file mode 100644 index 00000000000..4013907a1f5 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/ItemGridCompiler.java @@ -0,0 +1,28 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.block.LytItemGrid; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; + +public class ItemGridCompiler extends BlockTagCompiler { + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + var itemGrid = new LytItemGrid(); + + // We expect children to only contain ItemIcon elements + for (var childNode : el.children()) { + if (childNode instanceof MdxJsxElementFields jsxChild && "ItemIcon".equals(jsxChild.name())) { + var item = MdxAttrs.getRequiredItem(compiler, parent, jsxChild, "id"); + if (item != null) { + itemGrid.addItem(item); + } + + continue; + } + parent.appendError(compiler, "Unsupported child-element in ItemGrid", childNode); + } + + parent.append(itemGrid); + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/ItemLinkCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/ItemLinkCompiler.java new file mode 100644 index 00000000000..1f89da3d067 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/ItemLinkCompiler.java @@ -0,0 +1,51 @@ +package appeng.client.guidebook.compiler.tags; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.flow.LytFlowLink; +import appeng.client.guidebook.document.flow.LytFlowParent; +import appeng.client.guidebook.document.flow.LytTooltipSpan; +import appeng.client.guidebook.document.interaction.ItemTooltip; +import appeng.client.guidebook.indices.ItemIndex; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.libs.mdast.model.MdAstNode; + +public class ItemLinkCompiler extends FlowTagCompiler { + @Override + public void compile(PageCompiler compiler, LytFlowParent parent, MdxJsxElementFields el) { + var itemAndId = MdxAttrs.getRequiredItemAndId(compiler, parent, el, "id"); + if (itemAndId == null) { + return; + } + var id = itemAndId.getLeft(); + var item = itemAndId.getRight(); + + var linksTo = ItemIndex.INSTANCE.get(id); + // We'll error out for item-links to our own mod because we expect them to have a page + // while we don't have pages for Vanilla items or items from other mods. + if (linksTo == null && id.getNamespace().equals(compiler.getId().getNamespace())) { + parent.append(compiler.createErrorFlowContent("No page found for item " + id, (MdAstNode) el)); + return; + } + + var stack = item.getDefaultInstance(); + + // If the item link is already on the page we're linking to, replace it with an underlined + // text that has a tooltip. + if (linksTo == null || linksTo.anchor() == null && compiler.getId().equals(linksTo.pageId())) { + var span = new LytTooltipSpan(); + span.modifyStyle(style -> style.italic(true)); + compiler.compileComponentToFlow(stack.getHoverName(), span); + span.setTooltip(new ItemTooltip(stack)); + parent.append(span); + } else { + var link = new LytFlowLink(); + link.setClickCallback(screen -> { + screen.navigateTo(linksTo); + }); + compiler.compileComponentToFlow(stack.getHoverName(), link); + link.setTooltip(new ItemTooltip(stack)); + parent.append(link); + } + } + +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/MdxAttrs.java b/src/main/java/appeng/client/guidebook/compiler/tags/MdxAttrs.java new file mode 100644 index 00000000000..c6e762f6827 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/MdxAttrs.java @@ -0,0 +1,59 @@ +package appeng.client.guidebook.compiler.tags; + +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.ResourceLocationException; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.LytErrorSink; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; + +/** + * utilities for dealing with attributes of {@link MdxJsxElementFields}. + */ +public final class MdxAttrs { + + private MdxAttrs() { + } + + @Nullable + public static Pair getRequiredItemAndId(PageCompiler compiler, LytErrorSink errorSink, + MdxJsxElementFields el, + String attribute) { + var id = el.getAttributeString(attribute, null); + if (id == null) { + errorSink.appendError(compiler, "Missing " + attribute + " attribute.", el); + return null; + } + + id = id.trim(); // Trim leading/trailing whitespace for easier use + + ResourceLocation itemId; + try { + itemId = compiler.resolveId(id); + } catch (ResourceLocationException e) { + errorSink.appendError(compiler, "Malformed item id " + id + ": " + e.getMessage(), el); + return null; + } + + var resultItem = Registry.ITEM.getOptional(itemId).orElse(null); + if (resultItem == null) { + errorSink.appendError(compiler, "Missing item: " + itemId, el); + return null; + } + return Pair.of(itemId, resultItem); + } + + public static Item getRequiredItem(PageCompiler compiler, LytErrorSink errorSink, MdxJsxElementFields el, + String attribute) { + var result = getRequiredItemAndId(compiler, errorSink, el, attribute); + if (result != null) { + return result.getRight(); + } + return null; + } +} diff --git a/src/main/java/appeng/client/guidebook/compiler/tags/RecipeForCompiler.java b/src/main/java/appeng/client/guidebook/compiler/tags/RecipeForCompiler.java new file mode 100644 index 00000000000..bf516b649a6 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/compiler/tags/RecipeForCompiler.java @@ -0,0 +1,78 @@ +package appeng.client.guidebook.compiler.tags; + +import java.util.List; +import java.util.function.Function; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.world.Container; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.document.block.LytBlockContainer; +import appeng.client.guidebook.document.block.recipes.LytCraftingRecipe; +import appeng.client.guidebook.document.block.recipes.LytInscriberRecipe; +import appeng.libs.mdast.mdx.model.MdxJsxElementFields; +import appeng.recipes.handlers.InscriberRecipe; +import appeng.util.Platform; + +/** + * Shows a Recipe-Book-Like representation of the recipe needed to craft a given item. + */ +public class RecipeForCompiler extends BlockTagCompiler { + + private final List> mappings = List.of( + new RecipeTypeMapping<>(RecipeType.CRAFTING, LytCraftingRecipe::new), + new RecipeTypeMapping<>(InscriberRecipe.TYPE, LytInscriberRecipe::new)); + + @Override + protected void compile(PageCompiler compiler, LytBlockContainer parent, MdxJsxElementFields el) { + var itemAndId = MdxAttrs.getRequiredItemAndId(compiler, parent, el, "id"); + if (itemAndId == null) { + return; + } + + var id = itemAndId.getLeft(); + var item = itemAndId.getRight(); + + // Find the recipe + var recipeManager = Platform.getClientRecipeManager(); + if (recipeManager == null) { + parent.appendError(compiler, "Cannot show recipe for " + id + " while not in-game", el); + return; + } + + for (var mapping : mappings) { + var block = mapping.tryCreate(recipeManager, item); + if (block != null) { + parent.append(block); + return; + } + } + + // TODO This *can* be legit if there's no recipe due to datapacks + parent.appendError(compiler, "Couldn't find recipe for " + id, el); + } + + /** + * Maps a recipe type to a factory that can create a layout block to display it. + */ + private record RecipeTypeMapping, C extends Container> ( + RecipeType recipeType, + Function factory) { + @Nullable + LytBlock tryCreate(RecipeManager recipeManager, Item resultItem) { + for (var recipe : recipeManager.byType(recipeType).values()) { + if (recipe.getResultItem().getItem() == resultItem) { + return factory.apply(recipe); + } + } + + return null; + } + } +} diff --git a/src/main/java/appeng/client/guidebook/document/DefaultStyles.java b/src/main/java/appeng/client/guidebook/document/DefaultStyles.java new file mode 100644 index 00000000000..fafedb2c791 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/DefaultStyles.java @@ -0,0 +1,35 @@ +package appeng.client.guidebook.document; + +import net.minecraft.client.Minecraft; + +import appeng.client.guidebook.render.SymbolicColor; +import appeng.client.guidebook.style.ResolvedTextStyle; +import appeng.client.guidebook.style.TextAlignment; +import appeng.client.guidebook.style.TextStyle; +import appeng.client.guidebook.style.WhiteSpaceMode; + +public class DefaultStyles { + + public static final ResolvedTextStyle BASE_STYLE = new ResolvedTextStyle( + 1, + false, + false, + false, + false, + false, + Minecraft.UNIFORM_FONT, + SymbolicColor.BODY_TEXT.ref(), + WhiteSpaceMode.NORMAL, + TextAlignment.LEFT); + + public static final TextStyle BODY_TEXT = TextStyle.builder() + .font(Minecraft.UNIFORM_FONT) + .color(SymbolicColor.BODY_TEXT.ref()) + .build(); + + public static final TextStyle CRAFTING_RECIPE_TYPE = TextStyle.builder() + .font(Minecraft.UNIFORM_FONT) + .color(SymbolicColor.CRAFTING_RECIPE_TYPE.ref()) + .build(); + +} diff --git a/src/main/java/appeng/client/guidebook/document/LytErrorSink.java b/src/main/java/appeng/client/guidebook/document/LytErrorSink.java new file mode 100644 index 00000000000..54e36c9ffdd --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/LytErrorSink.java @@ -0,0 +1,8 @@ +package appeng.client.guidebook.document; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.libs.unist.UnistNode; + +public interface LytErrorSink { + void appendError(PageCompiler compiler, String text, UnistNode node); +} diff --git a/src/main/java/appeng/client/guidebook/document/LytRect.java b/src/main/java/appeng/client/guidebook/document/LytRect.java new file mode 100644 index 00000000000..ab27b45b1cc --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/LytRect.java @@ -0,0 +1,94 @@ +package appeng.client.guidebook.document; + +public record LytRect(int x, int y, int width, int height) { + + private static final LytRect EMPTY = new LytRect(0, 0, 0, 0); + + public static LytRect empty() { + return EMPTY; + } + + public int right() { + return x + width; + } + + public int bottom() { + return y + height; + } + + public boolean isEmpty() { + return width == 0 || height == 0; + } + + public LytRect shrink(int left, int top, int right, int bottom) { + return new LytRect( + x + left, + y + top, + Math.max(width - left - right, 0), + Math.max(height - top - bottom, 0)); + } + + public LytRect expand(int amount) { + return expand(amount, amount, amount, amount); + } + + public LytRect expand(int left, int top, int right, int bottom) { + return new LytRect( + x - left, + y - top, + Math.max(width + left + right, 0), + Math.max(height + top + bottom, 0)); + } + + public LytRect withWidth(int width) { + return new LytRect(x, y, width, height); + } + + public LytRect withHeight(int height) { + return new LytRect(x, y, width, height); + } + + public LytRect move(int x, int y) { + return new LytRect(this.x + x, this.y + y, width, height); + } + + public LytRect centerVerticallyIn(LytRect other) { + var centerYOther = other.y + other.height / 2; + return new LytRect(x, centerYOther - height / 2, width, height); + } + + public static LytRect union(LytRect a, LytRect b) { + if (a.isEmpty()) { + return b; + } else if (b.isEmpty()) { + return a; + } + + int x = Math.min(a.x, b.x); + int y = Math.min(a.y, b.y); + int right = Math.max(a.right(), b.right()); + int bottom = Math.max(a.bottom(), b.bottom()); + + return new LytRect( + x, y, + right - x, + bottom - y); + } + + public boolean contains(int x, int y) { + return x >= this.x && x < right() && + y >= this.y && y < bottom(); + } + + public boolean intersects(LytRect other) { + return right() > other.x() && x < other.right() && bottom() > other.y && y < other.bottom(); + } + + public LytRect withX(int x) { + return new LytRect(x, y, width, height); + } + + public LytRect withY(int y) { + return new LytRect(x, y, width, height); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/LytSize.java b/src/main/java/appeng/client/guidebook/document/LytSize.java new file mode 100644 index 00000000000..4eaba276d05 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/LytSize.java @@ -0,0 +1,9 @@ +package appeng.client.guidebook.document; + +public record LytSize(int width, int height) { + + public static LytSize empty() { + return new LytSize(0, 0); + } + +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytBlock.java b/src/main/java/appeng/client/guidebook/document/block/LytBlock.java new file mode 100644 index 00000000000..e634d6c05cf --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytBlock.java @@ -0,0 +1,71 @@ +package appeng.client.guidebook.document.block; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; + +public abstract class LytBlock extends LytNode { + /** + * Content rectangle. + */ + protected LytRect bounds = LytRect.empty(); + + private int marginTop; + private int marginLeft; + private int marginRight; + private int marginBottom; + + @Override + public LytRect getBounds() { + return bounds; + } + + public boolean isCulled(LytRect viewport) { + return !viewport.intersects(bounds); + } + + public final LytRect layout(LayoutContext context, int x, int y, int availableWidth) { + bounds = computeLayout(context, x, y, availableWidth); + return bounds; + } + + public int getMarginTop() { + return marginTop; + } + + public void setMarginTop(int marginTop) { + this.marginTop = marginTop; + } + + public int getMarginLeft() { + return marginLeft; + } + + public void setMarginLeft(int marginLeft) { + this.marginLeft = marginLeft; + } + + public int getMarginRight() { + return marginRight; + } + + public void setMarginRight(int marginRight) { + this.marginRight = marginRight; + } + + public int getMarginBottom() { + return marginBottom; + } + + public void setMarginBottom(int marginBottom) { + this.marginBottom = marginBottom; + } + + protected abstract LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth); + + public abstract void renderBatch(RenderContext context, MultiBufferSource buffers); + + public abstract void render(RenderContext context); +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytBlockContainer.java b/src/main/java/appeng/client/guidebook/document/block/LytBlockContainer.java new file mode 100644 index 00000000000..7457f2ef719 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytBlockContainer.java @@ -0,0 +1,14 @@ +package appeng.client.guidebook.document.block; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.LytErrorSink; +import appeng.libs.unist.UnistNode; + +public interface LytBlockContainer extends LytErrorSink { + void append(LytBlock node); + + @Override + default void appendError(PageCompiler compiler, String text, UnistNode node) { + append(compiler.createErrorBlock(text, node)); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytBox.java b/src/main/java/appeng/client/guidebook/document/block/LytBox.java new file mode 100644 index 00000000000..1642b9b0335 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytBox.java @@ -0,0 +1,76 @@ +package appeng.client.guidebook.document.block; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; + +public abstract class LytBox extends LytBlock implements LytBlockContainer { + protected final List children = new ArrayList<>(); + + protected int paddingLeft; + protected int paddingTop; + protected int paddingRight; + protected int paddingBottom; + + @Override + public void removeChild(LytNode node) { + if (node instanceof LytBlock block && block.parent == this) { + children.remove(block); + block.parent = null; + } + } + + @Override + public void append(LytBlock block) { + if (block.parent != null) { + block.parent.removeChild(block); + } + block.parent = this; + children.add(block); + } + + protected abstract LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth); + + @Override + protected final LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + // Apply adding + var innerLayout = computeBoxLayout( + context, + x + paddingLeft, + y + paddingTop, + availableWidth - paddingLeft - paddingRight); + + return innerLayout.expand(paddingLeft, paddingTop, paddingRight, paddingBottom); + } + + protected final void setPadding(int padding) { + paddingLeft = padding; + paddingTop = padding; + paddingRight = padding; + paddingBottom = padding; + } + + @Override + public List getChildren() { + return children; + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + for (var child : children) { + child.renderBatch(context, buffers); + } + } + + @Override + public void render(RenderContext context) { + for (var child : children) { + child.render(context); + } + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytDocument.java b/src/main/java/appeng/client/guidebook/document/block/LytDocument.java new file mode 100644 index 00000000000..3b3b492bec9 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytDocument.java @@ -0,0 +1,156 @@ +package appeng.client.guidebook.document.block; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowContainer; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.document.flow.LytFlowInlineBlock; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.Layouts; +import appeng.client.guidebook.render.SimpleRenderContext; + +/** + * Layout document. Has a viewport and an overall size which may exceed the document size vertically, but not + * horizontally. + */ +public class LytDocument extends LytNode implements LytBlockContainer { + private final List blocks = new ArrayList<>(); + + @Nullable + private Layout layout; + + @Nullable + private HitTestResult hoveredElement; + + public int getAvailableWidth() { + return layout != null ? layout.availableWidth() : 0; + } + + public int getContentHeight() { + return layout != null ? layout.contentHeight() : 0; + } + + public List getBlocks() { + return blocks; + } + + @Override + public List getChildren() { + return blocks; + } + + @Override + public LytRect getBounds() { + return layout != null ? new LytRect(0, 0, layout.availableWidth, layout.contentHeight) : null; + } + + @Override + public void removeChild(LytNode node) { + if (node instanceof LytBlock block) { + blocks.remove(block); + } + } + + @Override + public void append(LytBlock block) { + if (block.parent != null) { + block.parent.removeChild(block); + } + block.parent = this; + blocks.add(block); + } + + public void updateLayout(LayoutContext context, int availableWidth) { + if (layout != null && layout.availableWidth == availableWidth) { + return; + } + + layout = createLayout(context, availableWidth); + } + + private Layout createLayout(LayoutContext context, int availableWidth) { + var bounds = Layouts.verticalLayout(context, + blocks, + 0, + 0, + availableWidth, + 5, + 5, + 5, + 5); + + return new Layout(availableWidth, bounds.height()); + } + + public void render(SimpleRenderContext context) { + for (var block : blocks) { + if (block.isCulled(context.viewport())) { + continue; + } + block.render(context); + } + } + + public void renderBatch(SimpleRenderContext context, MultiBufferSource buffers) { + for (var block : blocks) { + if (!block.getBounds().intersects(context.viewport())) { + continue; + } + block.renderBatch(context, buffers); + } + } + + public HitTestResult getHoveredElement() { + return hoveredElement; + } + + public void setHoveredElement(HitTestResult hoveredElement) { + if (hoveredElement != this.hoveredElement) { + if (this.hoveredElement != null) { + this.hoveredElement.node.onMouseLeave(); + } + this.hoveredElement = hoveredElement; + if (this.hoveredElement != null) { + this.hoveredElement.node.onMouseEnter(hoveredElement.content()); + } + } + } + + public HitTestResult pick(int x, int y) { + return pick(this, x, y); + } + + private static HitTestResult pick(LytNode root, int x, int y) { + var node = root.pickNode(x, y); + if (node != null) { + LytFlowContent content = null; + if (node instanceof LytFlowContainer container) { + content = container.pickContent(x, y); + + // If the content is an inline-block, we descend into it! (This can go on and on and on...) + if (content instanceof LytFlowInlineBlock inlineBlock && inlineBlock.getBlock() != null) { + return pick(inlineBlock.getBlock(), x, y); + } + } + return new HitTestResult(node, content); + } + + return null; + } + + @Override + public void onMouseEnter(@Nullable LytFlowContent hoveredContent) { + } + + public record Layout(int availableWidth, int contentHeight) { + } + + public record HitTestResult(LytNode node, @Nullable LytFlowContent content) { + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytHeading.java b/src/main/java/appeng/client/guidebook/document/block/LytHeading.java new file mode 100644 index 00000000000..95e2af9dfcb --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytHeading.java @@ -0,0 +1,22 @@ +package appeng.client.guidebook.document.block; + +import net.minecraft.client.Minecraft; + +public class LytHeading extends LytParagraph { + public LytHeading() { + setMarginTop(5); + setMarginBottom(5); + } + + private int depth; + + public int getDepth() { + return depth; + } + + public void setDepth(int depth) { + this.depth = depth; + var fontScale = Math.max(1, 1.75f - depth * 0.25f); + modifyStyle(builder -> builder.fontScale(fontScale).font(Minecraft.DEFAULT_FONT)); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytImage.java b/src/main/java/appeng/client/guidebook/document/block/LytImage.java new file mode 100644 index 00000000000..2ca6cb13f42 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytImage.java @@ -0,0 +1,99 @@ +package appeng.client.guidebook.document.block; + +import java.util.Optional; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.interaction.GuideTooltip; +import appeng.client.guidebook.document.interaction.InteractiveElement; +import appeng.client.guidebook.document.interaction.TextTooltip; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.GuidePageTexture; +import appeng.client.guidebook.render.RenderContext; + +public class LytImage extends LytBlock implements InteractiveElement { + + private ResourceLocation imageId; + private GuidePageTexture texture = GuidePageTexture.missing(); + private String title; + private String alt; + + public ResourceLocation getImageId() { + return imageId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAlt() { + return alt; + } + + public void setAlt(String alt) { + this.alt = alt; + } + + public void setImage(ResourceLocation id, byte @Nullable [] imageData) { + this.imageId = id; + if (imageData != null) { + this.texture = GuidePageTexture.load(id, imageData); + } else { + this.texture = GuidePageTexture.missing(); + } + } + + @Override + protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + if (texture == null) { + return new LytRect(x, y, 32, 32); + } + + var size = texture.getSize(); + var width = size.width(); + var height = size.height(); + + width /= 4; + height /= 4; + + if (width > availableWidth) { + var f = availableWidth / (float) width; + width *= f; + height *= f; + } + + return new LytRect(x, y, width, height); + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + } + + @Override + public void render(RenderContext context) { + if (texture == null) { + var texture = MissingTextureAtlasSprite.getTexture(); + context.fillTexturedRect(getBounds(), texture); + } else { + context.fillTexturedRect(getBounds(), texture); + } + } + + @Override + public Optional getTooltip() { + if (title != null) { + return Optional.of(new TextTooltip(Component.literal(title))); + } + return Optional.empty(); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytItemGrid.java b/src/main/java/appeng/client/guidebook/document/block/LytItemGrid.java new file mode 100644 index 00000000000..163bd9bfe50 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytItemGrid.java @@ -0,0 +1,48 @@ +package appeng.client.guidebook.document.block; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.world.item.Item; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; + +/** + * Shows items in a grid-like fashion, i.e. to show-case variants. + */ +public class LytItemGrid extends LytBox { + private final List slots = new ArrayList<>(); + + public LytItemGrid() { + setPadding(5); + } + + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + var cols = Math.max(1, availableWidth / LytSlot.OUTER_SIZE); + var rows = (slots.size() + cols - 1) / cols; + + for (int i = 0; i < slots.size(); i++) { + var slotX = i % cols; + var slotY = i / cols; + slots.get(i).layout( + context, + x + slotX * LytSlot.OUTER_SIZE, + y + slotY * LytSlot.OUTER_SIZE, + availableWidth); + } + + return new LytRect( + x, + y, + cols * LytSlot.OUTER_SIZE, + rows * LytSlot.OUTER_SIZE); + } + + public void addItem(Item item) { + var slot = new LytSlot(item.getDefaultInstance()); + slots.add(slot); + append(slot); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytList.java b/src/main/java/appeng/client/guidebook/document/block/LytList.java new file mode 100644 index 00000000000..110772e899b --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytList.java @@ -0,0 +1,28 @@ +package appeng.client.guidebook.document.block; + +public class LytList extends LytVBox { + private final boolean ordered; + private final int start; + + public LytList(boolean ordered, int start) { + this.ordered = ordered; + this.start = start; + } + + public int getDepth() { + for (var parent = getParent(); parent != null; parent = parent.getParent()) { + if (parent instanceof LytList parentList) { + return parentList.getDepth() + 1; + } + } + return 1; + } + + public boolean isOrdered() { + return ordered; + } + + public int getStart() { + return start; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytListItem.java b/src/main/java/appeng/client/guidebook/document/block/LytListItem.java new file mode 100644 index 00000000000..d6606a0eb73 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytListItem.java @@ -0,0 +1,85 @@ +package appeng.client.guidebook.document.block; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.DefaultStyles; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.client.guidebook.render.SymbolicColor; +import appeng.client.guidebook.style.ResolvedTextStyle; + +public class LytListItem extends LytVBox { + + private static final int LEVEL_MARGIN = 10; + + private final ResolvedTextStyle style = DefaultStyles.BODY_TEXT.mergeWith(DefaultStyles.BASE_STYLE); + + private boolean isOrdered() { + if (parent instanceof LytList list) { + return list.isOrdered(); + } + return false; + } + + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + // Constraint child layout + var margin = LEVEL_MARGIN; + var bounds = super.computeBoxLayout(context, x + margin, y, availableWidth - margin); + + // Include the space we need for our list bullet in our bounds + return bounds.expand(LEVEL_MARGIN, 0, 0, 0); + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + if (isOrdered()) { + int number = getOrderedItemNumber(); + String label = number + "."; + + var width = context.getWidth(label, style); + var bounds = getBounds(); + var x = bounds.x() + LEVEL_MARGIN - width - 2; + + context.renderTextInBatch(label, + style, + x, (float) bounds.y(), buffers); + } + + super.renderBatch(context, buffers); + } + + private int getOrderedItemNumber() { + var number = 1; + if (parent instanceof LytList list) { + number = list.getStart(); + // Count precending list items on the same level + for (var child : list.getChildren()) { + if (child == this) { + break; + } + if (child instanceof LytListItem) { + number++; + } + } + } + return number; + } + + @Override + public void render(RenderContext context) { + if (!isOrdered()) { + var bounds = getBounds(); + + context.fillRect( + bounds.x() + 5, + bounds.y() + 4, + 2, + 2, + SymbolicColor.BODY_TEXT.ref()); + } + + super.render(context); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytNode.java b/src/main/java/appeng/client/guidebook/document/block/LytNode.java new file mode 100644 index 00000000000..ce96cefef7e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytNode.java @@ -0,0 +1,80 @@ +package appeng.client.guidebook.document.block; + +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.style.Styleable; +import appeng.client.guidebook.style.TextStyle; + +public abstract class LytNode implements Styleable { + @Nullable + protected LytNode parent; + + private TextStyle style = TextStyle.EMPTY; + private TextStyle hoverStyle = TextStyle.EMPTY; + + public void removeChild(LytNode node) { + } + + public List getChildren() { + return Collections.emptyList(); + } + + @Nullable + public final LytNode getParent() { + return parent; + } + + public abstract LytRect getBounds(); + + public void onMouseEnter(@Nullable LytFlowContent hoveredContent) { + } + + public void onMouseLeave() { + } + + @Nullable + public LytNode pickNode(int x, int y) { + if (!getBounds().contains(x, y)) { + return null; + } + + for (var child : getChildren()) { + var node = child.pickNode(x, y); + if (node != null) { + return node; + } + } + + return this; + } + + @Override + public TextStyle getStyle() { + return style; + } + + @Override + public void setStyle(TextStyle style) { + this.style = style; + } + + @Override + public TextStyle getHoverStyle() { + return hoverStyle; + } + + @Override + public void setHoverStyle(TextStyle style) { + this.hoverStyle = style; + } + + @Override + public @Nullable Styleable getStylingParent() { + return parent; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytParagraph.java b/src/main/java/appeng/client/guidebook/document/block/LytParagraph.java new file mode 100644 index 00000000000..192103dd4a1 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytParagraph.java @@ -0,0 +1,144 @@ +package appeng.client.guidebook.document.block; + +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowContainer; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.flow.FlowBuilder; +import appeng.client.guidebook.render.RenderContext; + +public class LytParagraph extends LytBlock implements LytFlowContainer { + protected final FlowBuilder content = new FlowBuilder(); + + protected int paddingLeft; + protected int paddingTop; + protected int paddingRight; + protected int paddingBottom; + + @Nullable + protected LytFlowContent hoveredContent; + + @Override + public void append(LytFlowContent child) { + content.append(child); + child.setParent(this); + } + + @Override + public boolean isCulled(LytRect viewport) { + // If we have floating content, account for its bounding box exceeding our content box + if (content.floatsIntersect(viewport)) { + return false; + } + + return super.isCulled(viewport); + } + + @Override + public LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + // Apply padding to paragraph content + x += paddingLeft; + availableWidth -= paddingLeft + paddingRight; + y += paddingTop; + + var style = resolveStyle(); + + var bounds = content.computeLayout(context, x, y, availableWidth, style.alignment()); + if (paddingBottom != 0) { + return bounds.withHeight(bounds.height() + paddingBottom); + } + return bounds; + } + + @Override + public void onMouseEnter(@Nullable LytFlowContent hoveredContent) { + super.onMouseEnter(hoveredContent); + this.hoveredContent = hoveredContent; + } + + @Override + public void onMouseLeave() { + super.onMouseLeave(); + this.hoveredContent = null; + } + + @Override + public @Nullable LytNode pickNode(int x, int y) { + // If we are the host for any floating elements, those can exceed our own bounds + var fl = content.pickFloatingElement(x, y); + if (fl != null) { + return this; + } + + return super.pickNode(x, y); + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + // Since we overwrite isCulled, we render even if our actual line content is culled, for floats + if (bounds.intersects(context.viewport())) { + content.renderBatch(context, buffers, hoveredContent); + } + + content.renderFloatsBatch(context, buffers, hoveredContent); + } + + @Override + public void render(RenderContext context) { + // Since we overwrite isCulled, we render even if our actual line content is culled, for floats + if (bounds.intersects(context.viewport())) { + content.render(context, hoveredContent); + } + + content.renderFloats(context, hoveredContent); + } + + @Override + public @Nullable LytFlowContent pickContent(int x, int y) { + var lineEl = content.pick(x, y); + return lineEl != null ? lineEl.getFlowContent() : null; + } + + @Override + public Stream enumerateContentBounds(LytFlowContent content) { + return this.content.enumerateContentBounds(content); + } + + public int getPaddingLeft() { + return paddingLeft; + } + + public void setPaddingLeft(int paddingLeft) { + this.paddingLeft = paddingLeft; + } + + public int getPaddingTop() { + return paddingTop; + } + + public void setPaddingTop(int paddingTop) { + this.paddingTop = paddingTop; + } + + public int getPaddingRight() { + return paddingRight; + } + + public void setPaddingRight(int paddingRight) { + this.paddingRight = paddingRight; + } + + public int getPaddingBottom() { + return paddingBottom; + } + + public void setPaddingBottom(int paddingBottom) { + this.paddingBottom = paddingBottom; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytSlot.java b/src/main/java/appeng/client/guidebook/document/block/LytSlot.java new file mode 100644 index 00000000000..08e8c92ad39 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytSlot.java @@ -0,0 +1,107 @@ +package appeng.client.guidebook.document.block; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.interaction.GuideTooltip; +import appeng.client.guidebook.document.interaction.InteractiveElement; +import appeng.client.guidebook.document.interaction.ItemTooltip; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.core.AppEng; + +/** + * Renders a standard Minecraft GUI slot. + */ +public class LytSlot extends LytBlock implements InteractiveElement { + public static final ResourceLocation SLOT_LIGHT = AppEng.makeId("ae2guide/gui/slot_light.png"); + public static final ResourceLocation SLOT_DARK = AppEng.makeId("ae2guide/gui/slot_dark.png"); + public static final ResourceLocation LARGE_SLOT_LIGHT = AppEng.makeId("ae2guide/gui/large_slot_light.png"); + public static final ResourceLocation LARGE_SLOT_DARK = AppEng.makeId("ae2guide/gui/large_slot_dark.png"); + + private static final int ITEM_SIZE = 16; + private static final int PADDING = 1; + private static final int LARGE_PADDING = 5; + public static final int OUTER_SIZE = ITEM_SIZE + 2 * PADDING; + public static final int OUTER_SIZE_LARGE = ITEM_SIZE + 2 * LARGE_PADDING; + private static final int CYCLE_TIME = 2000; + + private boolean largeSlot; + + private final ItemStack[] stacks; + + public LytSlot(Ingredient ingredient) { + this.stacks = ingredient.getItems(); + } + + public LytSlot(ItemStack stack) { + this.stacks = new ItemStack[] { stack }; + } + + public boolean isLargeSlot() { + return largeSlot; + } + + public void setLargeSlot(boolean largeSlot) { + this.largeSlot = largeSlot; + } + + @Override + protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + if (largeSlot) { + return new LytRect(x, y, OUTER_SIZE_LARGE, OUTER_SIZE_LARGE); + } else { + return new LytRect(x, y, OUTER_SIZE, OUTER_SIZE); + } + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + + } + + @Override + public void render(RenderContext context) { + var x = bounds.x(); + var y = bounds.y(); + + ResourceLocation texture; + if (largeSlot) { + texture = context.isDarkMode() ? LARGE_SLOT_DARK : LARGE_SLOT_LIGHT; + } else { + texture = context.isDarkMode() ? SLOT_DARK : SLOT_LIGHT; + } + context.fillTexturedRect(bounds, texture); + + var padding = largeSlot ? LARGE_PADDING : PADDING; + + var stack = getDisplayedStack(); + if (!stack.isEmpty()) { + context.renderItem(stack, x + padding, y + padding, 1, ITEM_SIZE, ITEM_SIZE); + } + } + + @Override + public Optional getTooltip() { + var stack = getDisplayedStack(); + if (stack.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new ItemTooltip(stack)); + } + + private ItemStack getDisplayedStack() { + if (stacks.length == 0) { + return ItemStack.EMPTY; + } + + var cycle = System.nanoTime() / TimeUnit.MILLISECONDS.toNanos(CYCLE_TIME); + return stacks[(int) (cycle % stacks.length)]; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytSlotGrid.java b/src/main/java/appeng/client/guidebook/document/block/LytSlotGrid.java new file mode 100644 index 00000000000..3099aa15647 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytSlotGrid.java @@ -0,0 +1,96 @@ +package appeng.client.guidebook.document.block; + +import net.minecraft.world.item.crafting.Ingredient; + +import appeng.client.gui.Icon; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.ColorRef; +import appeng.client.guidebook.render.RenderContext; + +public class LytSlotGrid extends LytBox { + private final int width; + private final int height; + private final LytSlot[] slots; + private boolean renderEmptySlots = true; + + public LytSlotGrid(int width, int height) { + this.width = width; + this.height = height; + this.slots = new LytSlot[width * height]; + } + + public boolean isRenderEmptySlots() { + return renderEmptySlots; + } + + public void setRenderEmptySlots(boolean renderEmptySlots) { + this.renderEmptySlots = renderEmptySlots; + } + + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + // Lay out the slots left-to-right, top-to-bottom + for (var row = 0; row < height; row++) { + for (var col = 0; col < width; col++) { + var index = getSlotIndex(col, row); + if (index < slots.length) { + var slot = slots[index]; + if (slot != null) { + slot.layout( + context, + x + col * LytSlot.OUTER_SIZE, + y + row * LytSlot.OUTER_SIZE, + availableWidth); + } + } + } + } + + return new LytRect(x, y, LytSlot.OUTER_SIZE * width, LytSlot.OUTER_SIZE * height); + } + + public void setIngredient(int x, int y, Ingredient ingredient) { + if (x < 0 || x >= width) { + throw new IndexOutOfBoundsException("x: " + x); + } + if (y < 0 || y >= height) { + throw new IndexOutOfBoundsException("y: " + y); + } + + var slotIndex = getSlotIndex(x, y); + var slot = slots[slotIndex]; + if (slot != null) { + slot.removeChild(slot); + slots[slotIndex] = null; + } + + slot = slots[slotIndex] = new LytSlot(ingredient); + append(slot); + } + + @Override + public void render(RenderContext context) { + // Render empty slots if requested + if (renderEmptySlots) { + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { + var index = getSlotIndex(x, y); + if (index >= slots.length || slots[index] == null) { + context.drawIcon( + bounds.x() + LytSlot.OUTER_SIZE * x, + bounds.y() + LytSlot.OUTER_SIZE * y, + Icon.SLOT_BACKGROUND, + ColorRef.WHITE); + } + } + } + } + + super.render(context); + } + + private int getSlotIndex(int col, int row) { + return row * width + col; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytThematicBreak.java b/src/main/java/appeng/client/guidebook/document/block/LytThematicBreak.java new file mode 100644 index 00000000000..04780db24f3 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytThematicBreak.java @@ -0,0 +1,26 @@ +package appeng.client.guidebook.document.block; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.client.guidebook.render.SymbolicColor; + +public class LytThematicBreak extends LytBlock { + @Override + public LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + return new LytRect(x, y, availableWidth, 6); + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + } + + @Override + public void render(RenderContext context) { + var line = bounds.withHeight(2).centerVerticallyIn(bounds); + + context.fillRect(line, SymbolicColor.THEMATIC_BREAK.ref()); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/LytVBox.java b/src/main/java/appeng/client/guidebook/document/block/LytVBox.java new file mode 100644 index 00000000000..09c951fa1f3 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/LytVBox.java @@ -0,0 +1,24 @@ +package appeng.client.guidebook.document.block; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.Layouts; + +/** + * Lays out its children vertically. + */ +public class LytVBox extends LytBox { + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + // Padding is applied through the parent + return Layouts.verticalLayout(context, + children, + x, + y, + availableWidth, + 0, + 0, + 0, + 0); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/recipes/LytCraftingRecipe.java b/src/main/java/appeng/client/guidebook/document/block/recipes/LytCraftingRecipe.java new file mode 100644 index 00000000000..76d0f85f8bd --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/recipes/LytCraftingRecipe.java @@ -0,0 +1,97 @@ +package appeng.client.guidebook.document.block.recipes; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.level.block.Blocks; + +import appeng.client.guidebook.document.DefaultStyles; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytBox; +import appeng.client.guidebook.document.block.LytSlot; +import appeng.client.guidebook.document.block.LytSlotGrid; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.core.AppEng; + +public class LytCraftingRecipe extends LytBox { + private static final ResourceLocation ARROW_LIGHT = AppEng.makeId("ae2guide/gui/recipe_arrow_light.png"); + private static final ResourceLocation ARROW_DARK = AppEng.makeId("ae2guide/gui/recipe_arrow_dark.png"); + + private final CraftingRecipe recipe; + + private final LytSlotGrid grid; + + private final LytSlot resultSlot; + + public LytCraftingRecipe(CraftingRecipe recipe) { + this.recipe = recipe; + setPadding(5); + paddingTop = 15; + + var ingredients = recipe.getIngredients(); + if (recipe instanceof ShapedRecipe shapedRecipe) { + this.grid = new LytSlotGrid(shapedRecipe.getWidth(), shapedRecipe.getHeight()); + + for (var x = 0; x < shapedRecipe.getWidth(); x++) { + for (var y = 0; y < shapedRecipe.getHeight(); y++) { + var index = y * shapedRecipe.getWidth() + x; + if (index < ingredients.size()) { + var ingredient = ingredients.get(index); + if (!ingredient.isEmpty()) { + grid.setIngredient(x, y, ingredient); + } + } + } + } + } else { + // For shapeless -> layout 3 ingredients per row and break + var ingredientCount = ingredients.size(); + this.grid = new LytSlotGrid(Math.min(3, ingredientCount), (ingredientCount + 2) / 3); + for (int i = 0; i < ingredients.size(); i++) { + var col = i % 3; + var row = i / 3; + grid.setIngredient(col, row, ingredients.get(i)); + } + } + append(grid); + + append(resultSlot = new LytSlot(recipe.getResultItem())); + } + + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + var gridBounds = grid.layout(context, x, y, availableWidth); + var slotBounds = resultSlot.layout( + context, + gridBounds.right() + 28, + // Center the slot vertically in relation to the grid + Math.max(y, gridBounds.y() + (gridBounds.height() - 18) / 2), + availableWidth); + return LytRect.union(gridBounds, slotBounds); + } + + @Override + public void render(RenderContext context) { + context.renderPanel(getBounds()); + + context.renderItem( + Blocks.CRAFTING_TABLE.asItem().getDefaultInstance(), + bounds.x() + paddingLeft, + bounds.y() + 4, + 8, + 8); + context.renderText( + (recipe instanceof ShapelessRecipe) ? "Crafting (Shapeless)" : "Crafting", + DefaultStyles.CRAFTING_RECIPE_TYPE.mergeWith(DefaultStyles.BASE_STYLE), + bounds.x() + paddingLeft + 10, + bounds.y() + 4); + + context.fillTexturedRect( + new LytRect(bounds.right() - 25 - 24, bounds.y() + 10 + (bounds.height() - 27) / 2, 24, 17), + context.isDarkMode() ? ARROW_DARK : ARROW_LIGHT); + + super.render(context); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/recipes/LytInscriberRecipe.java b/src/main/java/appeng/client/guidebook/document/block/recipes/LytInscriberRecipe.java new file mode 100644 index 00000000000..e9ae342efad --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/recipes/LytInscriberRecipe.java @@ -0,0 +1,58 @@ + +package appeng.client.guidebook.document.block.recipes; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytBox; +import appeng.client.guidebook.document.block.LytSlot; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.core.AppEng; +import appeng.recipes.handlers.InscriberRecipe; + +public class LytInscriberRecipe extends LytBox { + + private static final ResourceLocation ARROWS_LIGHT = AppEng.makeId("ae2guide/gui/inscriber_arrows_bg_light.png"); + private static final ResourceLocation ARROWS_DARK = AppEng.makeId("ae2guide/gui/inscriber_arrows_bg_dark.png"); + + private final InscriberRecipe recipe; + + private final LytSlot topSlot; + private final LytSlot middleSlot; + private final LytSlot bottomSlot; + private final LytSlot resultSlot; + + public LytInscriberRecipe(InscriberRecipe recipe) { + this.recipe = recipe; + + append(topSlot = new LytSlot(recipe.getTopOptional())); + append(middleSlot = new LytSlot(recipe.getMiddleInput())); + append(bottomSlot = new LytSlot(recipe.getBottomOptional())); + append(resultSlot = new LytSlot(recipe.getResultItem())); + resultSlot.setLargeSlot(true); + + setPadding(5); + } + + @Override + protected LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth) { + topSlot.layout(context, x, y, availableWidth); + middleSlot.layout(context, x + 18, y + 23, availableWidth); + bottomSlot.layout(context, x, y + 46, availableWidth); + resultSlot.layout(context, x + 64, y + 20, availableWidth); + + return new LytRect(x, y, 90, 64); + } + + @Override + public void render(RenderContext context) { + var bounds = getBounds(); + context.renderPanel(bounds); + + context.fillTexturedRect(new LytRect( + bounds.x() + 23, bounds.y() + 12, 46, 50), context.isDarkMode() ? ARROWS_DARK : ARROWS_LIGHT); + + super.render(context); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/table/LytTable.java b/src/main/java/appeng/client/guidebook/document/block/table/LytTable.java new file mode 100644 index 00000000000..8e2e321ef91 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/table/LytTable.java @@ -0,0 +1,122 @@ +package appeng.client.guidebook.document.block.table; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.client.guidebook.render.SymbolicColor; + +public class LytTable extends LytBlock { + /** + * Width of border around cells. + */ + private static final int CELL_BORDER = 1; + private final List rows = new ArrayList<>(); + + private final List columns = new ArrayList<>(); + + @Override + protected LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth) { + if (columns.isEmpty()) { + return LytRect.empty(); + } + + // Distribute available width evenly between columns + var cellWidth = (availableWidth - (columns.size() + 1) * CELL_BORDER) / columns.size(); + var colX = x + CELL_BORDER; + for (var column : columns) { + column.x = colX; + column.width = cellWidth; + colX += column.width + CELL_BORDER; + } + + // Ensure the last column fills the entire width (fixes rounding off-by-one issues) + var lastCol = columns.get(columns.size() - 1); + lastCol.width = (x + availableWidth) - lastCol.x - CELL_BORDER; + + // Layout each row + var currentY = y + CELL_BORDER; + for (var row : rows) { + var rowTop = currentY; + var rowBottom = currentY; + for (var cell : row.getChildren()) { + var column = cell.column; + var cellBounds = cell.layout(context, column.x, currentY, column.width); + rowBottom = Math.max(rowBottom, cellBounds.bottom()); + } + row.bounds = new LytRect(x, rowTop, availableWidth, rowBottom - rowTop); + currentY = rowBottom + CELL_BORDER; + } + + return new LytRect( + x, y, + availableWidth, + currentY - y); + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + for (var row : getChildren()) { + for (var cell : row.getChildren()) { + cell.renderBatch(context, buffers); + } + } + } + + @Override + public void render(RenderContext context) { + // Render the table cell borders + var bounds = getBounds(); + for (int i = 0; i < columns.size() - 1; i++) { + var column = columns.get(i); + if (i == 0) { + // context.fillRect(column.x - 1, bounds.y(), 1, bounds.height(), SymbolicColor.TABLE_BORDER.ref()); + } + var colRight = column.x + column.width; + context.fillRect(colRight, bounds.y(), 1, bounds.height(), SymbolicColor.TABLE_BORDER.ref()); + } + + for (int i = 0; i < rows.size() - 1; i++) { + var row = rows.get(i); + + if (i == 0) { + // context.fillRect(bounds.x(), row.bounds.y() - 1, bounds.width(), 1, + // SymbolicColor.TABLE_BORDER.ref()); + } + context.fillRect(bounds.x(), row.bounds.bottom(), bounds.width(), 1, SymbolicColor.TABLE_BORDER.ref()); + } + + for (var row : rows) { + for (var cell : row.getChildren()) { + cell.render(context); + } + } + } + + public LytTableRow appendRow() { + var row = new LytTableRow(this); + rows.add(row); + return row; + } + + public List getColumns() { + return columns; + } + + public LytTableColumn getOrCreateColumn(int index) { + while (index >= columns.size()) { + columns.add(new LytTableColumn()); + } + return columns.get(index); + } + + @Override + public List getChildren() { + return rows; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/table/LytTableCell.java b/src/main/java/appeng/client/guidebook/document/block/table/LytTableCell.java new file mode 100644 index 00000000000..f7cdfcc2030 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/table/LytTableCell.java @@ -0,0 +1,24 @@ +package appeng.client.guidebook.document.block.table; + +import appeng.client.guidebook.document.block.LytVBox; + +/** + * A cell in a {@link LytTable}s {@link LytTableRow}. + */ +public class LytTableCell extends LytVBox { + final LytTable table; + final LytTableRow row; + final LytTableColumn column; + + public LytTableCell(LytTable table, LytTableRow row, LytTableColumn column) { + this.table = table; + this.row = row; + this.column = column; + this.parent = row; + + paddingLeft = 1; + paddingTop = 1; + paddingRight = 1; + paddingBottom = 1; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/block/table/LytTableColumn.java b/src/main/java/appeng/client/guidebook/document/block/table/LytTableColumn.java new file mode 100644 index 00000000000..a1827c80d9e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/table/LytTableColumn.java @@ -0,0 +1,6 @@ +package appeng.client.guidebook.document.block.table; + +public class LytTableColumn { + int x; + int width; +} diff --git a/src/main/java/appeng/client/guidebook/document/block/table/LytTableRow.java b/src/main/java/appeng/client/guidebook/document/block/table/LytTableRow.java new file mode 100644 index 00000000000..1cb08dc61ec --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/block/table/LytTableRow.java @@ -0,0 +1,37 @@ +package appeng.client.guidebook.document.block.table; + +import java.util.ArrayList; +import java.util.List; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytNode; + +/** + * A row in {@link LytTable}. Contains {@link LytTableCell}. + */ +public class LytTableRow extends LytNode { + private final LytTable table; + private final List cells = new ArrayList<>(); + LytRect bounds = LytRect.empty(); + + public LytTableRow(LytTable table) { + this.table = table; + this.parent = table; + } + + @Override + public LytRect getBounds() { + return bounds; + } + + public LytTableCell appendCell() { + var cell = new LytTableCell(table, this, table.getOrCreateColumn(cells.size())); + cells.add(cell); + return cell; + } + + @Override + public List getChildren() { + return cells; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/InlineBlockAlignment.java b/src/main/java/appeng/client/guidebook/document/flow/InlineBlockAlignment.java new file mode 100644 index 00000000000..c2a3c8ec12e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/InlineBlockAlignment.java @@ -0,0 +1,19 @@ +package appeng.client.guidebook.document.flow; + +/** + * How an inline block element is supposed to be aligned within the flow layout. + */ +public enum InlineBlockAlignment { + /** + * Place it in the line like any other line element. This means text will not wrap around it to fill the height. + */ + INLINE, + /** + * Float it to the left and wrap text around its right side. + */ + FLOAT_LEFT, + /** + * Float it to the right and wrap text around its left side. + */ + FLOAT_RIGHT +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowBreak.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowBreak.java new file mode 100644 index 00000000000..e21912db51e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowBreak.java @@ -0,0 +1,25 @@ +package appeng.client.guidebook.document.flow; + +/** + * Line-Break that also clears floats. + */ +public class LytFlowBreak extends LytFlowContent { + private boolean clearLeft; + private boolean clearRight; + + public boolean isClearLeft() { + return clearLeft; + } + + public void setClearLeft(boolean clearLeft) { + this.clearLeft = clearLeft; + } + + public boolean isClearRight() { + return clearRight; + } + + public void setClearRight(boolean clearRight) { + this.clearRight = clearRight; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowContainer.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowContainer.java new file mode 100644 index 00000000000..131c216da55 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowContainer.java @@ -0,0 +1,18 @@ +package appeng.client.guidebook.document.flow; + +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.document.LytRect; + +public interface LytFlowContainer extends LytFlowParent { + /** + * Gets a stream of all the bounding rectangles for given flow content. Since flow content may be wrapped, it may + * consist of several disjointed bounding boxes. + */ + Stream enumerateContentBounds(LytFlowContent content); + + @Nullable + LytFlowContent pickContent(int x, int y); +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowContent.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowContent.java new file mode 100644 index 00000000000..9419791800b --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowContent.java @@ -0,0 +1,63 @@ +package appeng.client.guidebook.document.flow; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.style.Styleable; +import appeng.client.guidebook.style.TextStyle; + +public class LytFlowContent implements Styleable { + private TextStyle style = TextStyle.EMPTY; + private TextStyle hoverStyle = TextStyle.EMPTY; + + private LytFlowParent parent; + + public LytFlowParent getParent() { + return parent; + } + + public void setParent(LytFlowParent parent) { + this.parent = parent; + } + + /** + * Gets the parent of this flow content that is itself flow content. Null if the parent is null or not flow content. + */ + @Nullable + public LytFlowContent getFlowParent() { + return parent instanceof LytFlowContent flowContent ? flowContent : null; + } + + public boolean isInclusiveAncestor(LytFlowContent flowContent) { + for (var content = flowContent; content != null; content = content.getFlowParent()) { + if (content == this) { + return true; + } + } + return false; + } + + @Override + public TextStyle getStyle() { + return style; + } + + @Override + public void setStyle(TextStyle style) { + this.style = style; + } + + @Override + public TextStyle getHoverStyle() { + return hoverStyle; + } + + @Override + public void setHoverStyle(TextStyle style) { + this.hoverStyle = style; + } + + @Override + public @Nullable Styleable getStylingParent() { + return getParent() instanceof Styleable stylingParent ? stylingParent : null; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowInlineBlock.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowInlineBlock.java new file mode 100644 index 00000000000..f685184bf90 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowInlineBlock.java @@ -0,0 +1,70 @@ +package appeng.client.guidebook.document.flow; + +import java.util.Optional; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.LytSize; +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.document.interaction.GuideTooltip; +import appeng.client.guidebook.document.interaction.InteractiveElement; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.MinecraftFontMetrics; +import appeng.client.guidebook.screen.GuideScreen; + +public class LytFlowInlineBlock extends LytFlowContent implements InteractiveElement { + + private LytBlock block; + + private InlineBlockAlignment alignment = InlineBlockAlignment.INLINE; + + public LytBlock getBlock() { + return block; + } + + public void setBlock(LytBlock block) { + this.block = block; + } + + public InlineBlockAlignment getAlignment() { + return alignment; + } + + public void setAlignment(InlineBlockAlignment alignment) { + this.alignment = alignment; + } + + public LytSize getPreferredSize(int lineWidth) { + if (block == null) { + return LytSize.empty(); + } + + // We need to compute the layout + var layoutContext = new LayoutContext(new MinecraftFontMetrics(), LytRect.empty()); + var bounds = block.layout(layoutContext, 0, 0, lineWidth); + return new LytSize(bounds.right(), bounds.bottom()); + } + + @Override + public boolean mouseClicked(GuideScreen screen, int x, int y, int button) { + if (block instanceof InteractiveElement interactiveElement) { + return interactiveElement.mouseClicked(screen, x, y, button); + } + return false; + } + + @Override + public boolean mouseReleased(GuideScreen screen, int x, int y, int button) { + if (block instanceof InteractiveElement interactiveElement) { + return interactiveElement.mouseReleased(screen, x, y, button); + } + return false; + } + + @Override + public Optional getTooltip() { + if (block instanceof InteractiveElement interactiveElement) { + return interactiveElement.getTooltip(); + } + return Optional.empty(); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowLink.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowLink.java new file mode 100644 index 00000000000..a0054ca85e2 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowLink.java @@ -0,0 +1,31 @@ +package appeng.client.guidebook.document.flow; + +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.render.SymbolicColor; +import appeng.client.guidebook.screen.GuideScreen; + +public class LytFlowLink extends LytTooltipSpan { + @Nullable + private Consumer clickCallback; + + public LytFlowLink() { + modifyStyle(style -> style.color(SymbolicColor.LINK.ref())); + modifyHoverStyle(style -> style.underlined(true)); + } + + public void setClickCallback(@Nullable Consumer clickCallback) { + this.clickCallback = clickCallback; + } + + @Override + public boolean mouseClicked(GuideScreen screen, int x, int y, int button) { + if (button == 0 && clickCallback != null) { + clickCallback.accept(screen); + return true; + } + return false; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowParent.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowParent.java new file mode 100644 index 00000000000..e5f830015fa --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowParent.java @@ -0,0 +1,26 @@ +package appeng.client.guidebook.document.flow; + +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.client.guidebook.document.LytErrorSink; +import appeng.libs.unist.UnistNode; + +public interface LytFlowParent extends LytErrorSink { + void append(LytFlowContent child); + + default LytFlowText appendText(String text) { + var node = new LytFlowText(); + node.setText(text); + append(node); + return node; + } + + default void appendBreak() { + var br = new LytFlowBreak(); + append(br); + } + + @Override + default void appendError(PageCompiler compiler, String text, UnistNode node) { + append(compiler.createErrorFlowContent(text, node)); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowSpan.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowSpan.java new file mode 100644 index 00000000000..97af228f31c --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowSpan.java @@ -0,0 +1,25 @@ +package appeng.client.guidebook.document.flow; + +import java.util.ArrayList; +import java.util.List; + +import appeng.client.guidebook.style.Styleable; + +/** + * Attaches properties to a span of {@link LytFlowContent}, such as links or formatting. + */ +public class LytFlowSpan extends LytFlowContent implements LytFlowParent, Styleable { + private final List children = new ArrayList<>(); + + public List getChildren() { + return children; + } + + public void append(LytFlowContent child) { + if (child.getParent() != null) { + throw new IllegalStateException("Child is already owned by other span"); + } + child.setParent(this); + children.add(child); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytFlowText.java b/src/main/java/appeng/client/guidebook/document/flow/LytFlowText.java new file mode 100644 index 00000000000..6fbecf86095 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytFlowText.java @@ -0,0 +1,19 @@ +package appeng.client.guidebook.document.flow; + +public class LytFlowText extends LytFlowContent { + private String text = ""; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public static LytFlowText of(String text) { + var node = new LytFlowText(); + node.setText(text); + return node; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/flow/LytTooltipSpan.java b/src/main/java/appeng/client/guidebook/document/flow/LytTooltipSpan.java new file mode 100644 index 00000000000..c984f6ff220 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/flow/LytTooltipSpan.java @@ -0,0 +1,25 @@ +package appeng.client.guidebook.document.flow; + +import java.util.Optional; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.document.interaction.GuideTooltip; +import appeng.client.guidebook.document.interaction.InteractiveElement; + +/** + * An inline span that allows a tooltip to be shown on hover. + */ +public class LytTooltipSpan extends LytFlowSpan implements InteractiveElement { + @Nullable + private GuideTooltip tooltip; + + @Override + public Optional getTooltip() { + return Optional.ofNullable(tooltip); + } + + public void setTooltip(@Nullable GuideTooltip tooltip) { + this.tooltip = tooltip; + } +} diff --git a/src/main/java/appeng/client/guidebook/document/interaction/GuideTooltip.java b/src/main/java/appeng/client/guidebook/document/interaction/GuideTooltip.java new file mode 100644 index 00000000000..e045ba64d08 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/interaction/GuideTooltip.java @@ -0,0 +1,17 @@ +package appeng.client.guidebook.document.interaction; + +import java.util.List; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.world.item.ItemStack; + +public interface GuideTooltip { + + default ItemStack getIcon() { + return ItemStack.EMPTY; + } + + List getLines(Screen screen); + +} diff --git a/src/main/java/appeng/client/guidebook/document/interaction/InteractiveElement.java b/src/main/java/appeng/client/guidebook/document/interaction/InteractiveElement.java new file mode 100644 index 00000000000..5a6c2814ea8 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/interaction/InteractiveElement.java @@ -0,0 +1,19 @@ +package appeng.client.guidebook.document.interaction; + +import java.util.Optional; + +import appeng.client.guidebook.screen.GuideScreen; + +public interface InteractiveElement { + default boolean mouseClicked(GuideScreen screen, int x, int y, int button) { + return false; + } + + default boolean mouseReleased(GuideScreen screen, int x, int y, int button) { + return false; + } + + default Optional getTooltip() { + return Optional.empty(); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/interaction/ItemTooltip.java b/src/main/java/appeng/client/guidebook/document/interaction/ItemTooltip.java new file mode 100644 index 00000000000..2bcf87beee4 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/interaction/ItemTooltip.java @@ -0,0 +1,30 @@ +package appeng.client.guidebook.document.interaction; + +import java.util.List; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; + +public class ItemTooltip implements GuideTooltip { + private final ItemStack stack; + + public ItemTooltip(ItemStack stack) { + this.stack = stack; + } + + @Override + public ItemStack getIcon() { + return stack; + } + + @Override + public List getLines(Screen screen) { + var lines = screen.getTooltipFromItem(stack); + return lines.stream() + .map(Component::getVisualOrderText) + .map(ClientTooltipComponent::create) + .toList(); + } +} diff --git a/src/main/java/appeng/client/guidebook/document/interaction/TextTooltip.java b/src/main/java/appeng/client/guidebook/document/interaction/TextTooltip.java new file mode 100644 index 00000000000..877dadd6f18 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/document/interaction/TextTooltip.java @@ -0,0 +1,40 @@ +package appeng.client.guidebook.document.interaction; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTextTooltip; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.network.chat.Component; + +public class TextTooltip implements GuideTooltip { + private final List lines; + + public TextTooltip(String text) { + this(Component.literal(text)); + } + + public TextTooltip(List lines) { + this.lines = lines.stream() + .map(line -> new ClientTextTooltip(line.getVisualOrderText())) + .toList(); + } + + public TextTooltip(Component firstLine, Component... additionalLines) { + this(makeLineList(firstLine, additionalLines)); + } + + private static List makeLineList(Component firstLine, Component[] additionalLines) { + var lines = new ArrayList(1 + additionalLines.length); + lines.add(firstLine); + Collections.addAll(lines, additionalLines); + return lines; + } + + @Override + public List getLines(Screen screen) { + return lines; + } +} diff --git a/src/main/java/appeng/client/guidebook/indices/CategoryIndex.java b/src/main/java/appeng/client/guidebook/indices/CategoryIndex.java new file mode 100644 index 00000000000..92bebf5b3fc --- /dev/null +++ b/src/main/java/appeng/client/guidebook/indices/CategoryIndex.java @@ -0,0 +1,51 @@ +package appeng.client.guidebook.indices; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import appeng.client.guidebook.PageAnchor; +import appeng.client.guidebook.compiler.ParsedGuidePage; + +/** + * Pages can declare to be part of multiple categories using the categories frontmatter. + */ +public class CategoryIndex extends MultiValuedIndex { + private static final Logger LOGGER = LoggerFactory.getLogger(CategoryIndex.class); + + public static final CategoryIndex INSTANCE = new CategoryIndex(); + + public CategoryIndex() { + super("Categories", CategoryIndex::getCategories); + } + + private static List> getCategories(ParsedGuidePage page) { + var categoriesNode = page.getFrontmatter().additionalProperties().get("categories"); + if (categoriesNode == null) { + return List.of(); + } + + if (!(categoriesNode instanceof ListcategoryList)) { + LOGGER.warn("Page {} contains malformed categories frontmatter", page.getId()); + return List.of(); + } + + // The anchor to the current page + var anchor = new PageAnchor(page.getId(), null); + + var categories = new ArrayList>(); + + for (var listEntry : categoryList) { + if (listEntry instanceof String categoryString) { + categories.add(Pair.of(categoryString, anchor)); + } else { + LOGGER.warn("Page {} contains a malformed categories frontmatter entry: {}", page.getId(), listEntry); + } + } + + return categories; + } +} diff --git a/src/main/java/appeng/client/guidebook/indices/ItemIndex.java b/src/main/java/appeng/client/guidebook/indices/ItemIndex.java new file mode 100644 index 00000000000..4390f19b9cb --- /dev/null +++ b/src/main/java/appeng/client/guidebook/indices/ItemIndex.java @@ -0,0 +1,69 @@ +package appeng.client.guidebook.indices; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.ResourceLocationException; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.PageAnchor; +import appeng.client.guidebook.compiler.IdUtils; +import appeng.client.guidebook.compiler.ParsedGuidePage; + +/** + * An index of Minecraft items to the main guidebook page describing it. + */ +public class ItemIndex extends UniqueIndex { + private static final Logger LOGGER = LoggerFactory.getLogger(ItemIndex.class); + + public static final ItemIndex INSTANCE = new ItemIndex(); + + public ItemIndex() { + super("Item Index", ItemIndex::getItemAnchors); + } + + private static List> getItemAnchors(ParsedGuidePage page) { + var itemIdsNode = page.getFrontmatter().additionalProperties().get("item_ids"); + if (itemIdsNode == null) { + return List.of(); + } + + if (!(itemIdsNode instanceof ListitemIdList)) { + LOGGER.warn("Page {} contains malformed item_ids frontmatter", page.getId()); + return List.of(); + } + + var itemAnchors = new ArrayList>(); + + for (var listEntry : itemIdList) { + if (listEntry instanceof String itemIdStr) { + ResourceLocation itemId; + try { + itemId = IdUtils.resolveId(itemIdStr, page.getId().getNamespace()); + } catch (ResourceLocationException e) { + LOGGER.warn("Page {} contains a malformed item_ids frontmatter entry: {}", page.getId(), + listEntry); + continue; + } + + if (Registry.ITEM.containsKey(itemId)) { + // add a link to the top of the page + itemAnchors.add(Pair.of( + itemId, new PageAnchor(page.getId(), null))); + } else { + LOGGER.warn("Page {} references an unknown item {} in its item_ids frontmatter", + page.getId(), itemId); + } + } else { + LOGGER.warn("Page {} contains a malformed item_ids frontmatter entry: {}", page.getId(), listEntry); + } + } + + return itemAnchors; + } +} diff --git a/src/main/java/appeng/client/guidebook/indices/MultiValuedIndex.java b/src/main/java/appeng/client/guidebook/indices/MultiValuedIndex.java new file mode 100644 index 00000000000..9f2f7f15cd5 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/indices/MultiValuedIndex.java @@ -0,0 +1,97 @@ +package appeng.client.guidebook.indices; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.GuidePageChange; +import appeng.client.guidebook.compiler.ParsedGuidePage; + +/** + * A convenient index base-class for indices that map keys to multiple pages. + */ +public class MultiValuedIndex implements PageIndex { + private final Map>> index = new HashMap<>(); + + private final String name; + private final EntryFunction entryFunction; + + public MultiValuedIndex(String name, EntryFunction entryFunction) { + this.name = name; + this.entryFunction = entryFunction; + } + + @Override + public String getName() { + return name; + } + + public List get(K key) { + var entries = index.get(key); + if (entries != null) { + return entries.stream().map(Record::value).toList(); + } + return List.of(); + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public void rebuild(List pages) { + index.clear(); + + for (var page : pages) { + addToIndex(page); + } + } + + @Override + public void update(List allPages, List changes) { + // Clean up all index entries associated with changed pages + var idsToRemove = changes.stream() + .map(GuidePageChange::pageId) + .collect(Collectors.toSet()); + var it = index.values().iterator(); + while (it.hasNext()) { + var entries = it.next(); + entries.removeIf(p -> idsToRemove.contains(p.pageId)); + if (entries.isEmpty()) { + it.remove(); + } + } + + // Then re-add new or changed pages + for (var change : changes) { + var newPage = change.newPage(); + if (newPage != null) { + addToIndex(newPage); + } + } + } + + private void addToIndex(ParsedGuidePage page) { + for (var entry : entryFunction.getEntry(page)) { + var key = entry.getKey(); + var value = entry.getValue(); + var entries = index.computeIfAbsent(key, k -> new ArrayList<>()); + entries.add(new Record<>(page.getId(), value)); + } + } + + @FunctionalInterface + public interface EntryFunction { + Iterable> getEntry(ParsedGuidePage page); + } + + private record Record (ResourceLocation pageId, V value) { + } +} diff --git a/src/main/java/appeng/client/guidebook/indices/PageIndex.java b/src/main/java/appeng/client/guidebook/indices/PageIndex.java new file mode 100644 index 00000000000..4967a8a6c72 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/indices/PageIndex.java @@ -0,0 +1,30 @@ +package appeng.client.guidebook.indices; + +import java.util.List; + +import appeng.client.guidebook.GuidePageChange; +import appeng.client.guidebook.compiler.ParsedGuidePage; + +/** + * A page index is an index over all guidebook pages that will be automatically built when the guidebook is reloaded, + * and when individual pages change. + */ +public interface PageIndex { + String getName(); + + /** + * @return True if this index supports incremental updates via the {@link #update} method. + */ + boolean supportsUpdate(); + + /** + * Fully rebuilds this index. + */ + void rebuild(List pages); + + /** + * Applies an incremental update to this index. + */ + void update(List allPages, + List changes); +} diff --git a/src/main/java/appeng/client/guidebook/indices/UniqueIndex.java b/src/main/java/appeng/client/guidebook/indices/UniqueIndex.java new file mode 100644 index 00000000000..126e9e348aa --- /dev/null +++ b/src/main/java/appeng/client/guidebook/indices/UniqueIndex.java @@ -0,0 +1,109 @@ +package appeng.client.guidebook.indices; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.GuidePageChange; +import appeng.client.guidebook.compiler.ParsedGuidePage; + +/** + * Maintains an index for any given page using a mapping function for keys and values of the index. + */ +public class UniqueIndex implements PageIndex { + private static final Logger LOGGER = LoggerFactory.getLogger(UniqueIndex.class); + + private final Map> index = new HashMap<>(); + + private final String name; + private final EntryFunction entryFunction; + + // We need to track this to fully rebuild on incremental changes if we had duplicates + private boolean hadDuplicates; + + public UniqueIndex(String name, EntryFunction entryFunction) { + this.name = name; + this.entryFunction = entryFunction; + } + + @Override + public String getName() { + return name; + } + + @Nullable + public V get(K key) { + var entry = index.get(key); + if (entry != null) { + return entry.value(); + } + return null; + } + + @Override + public boolean supportsUpdate() { + return true; + } + + @Override + public void rebuild(List pages) { + index.clear(); + hadDuplicates = false; + + for (var page : pages) { + addToIndex(page); + } + } + + @Override + public void update(List allPages, List changes) { + if (hadDuplicates) { + rebuild(allPages); + return; + } + + // Clean up all index entries associated with changed pages + var idsToRemove = changes.stream() + .map(GuidePageChange::pageId) + .collect(Collectors.toSet()); + index.values().removeIf(p -> idsToRemove.contains(p.pageId)); + + // Then re-add new or changed pages + for (var change : changes) { + var newPage = change.newPage(); + if (newPage != null) { + addToIndex(newPage); + } + } + } + + private void addToIndex(ParsedGuidePage page) { + for (var entry : entryFunction.getEntry(page)) { + var key = entry.getKey(); + var value = entry.getValue(); + var previousPage = index.put(key, new Record<>(page.getId(), value)); + if (previousPage != null) { + LOGGER.warn("Key conflict in index {}: {} is used by pages {} and {}", + name, key, page, previousPage); + hadDuplicates = true; + } + } + } + + @FunctionalInterface + public interface EntryFunction { + Iterable> getEntry(ParsedGuidePage page); + } + + private record Record (ResourceLocation pageId, V value) { + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/FontMetrics.java b/src/main/java/appeng/client/guidebook/layout/FontMetrics.java new file mode 100644 index 00000000000..e8ed8119033 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/FontMetrics.java @@ -0,0 +1,9 @@ +package appeng.client.guidebook.layout; + +import appeng.client.guidebook.style.ResolvedTextStyle; + +public interface FontMetrics { + float getAdvance(int codePoint, ResolvedTextStyle style); + + int getLineHeight(ResolvedTextStyle style); +} diff --git a/src/main/java/appeng/client/guidebook/layout/LayoutContext.java b/src/main/java/appeng/client/guidebook/layout/LayoutContext.java new file mode 100644 index 00000000000..2e7a2d31e7d --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/LayoutContext.java @@ -0,0 +1,103 @@ +package appeng.client.guidebook.layout; + +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalInt; + +import com.google.common.collect.Streams; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.style.ResolvedTextStyle; + +public class LayoutContext implements FontMetrics { + private final FontMetrics fontMetrics; + + private final LytRect viewport; + + private final List leftFloats = new ArrayList<>(); + private final List rightFloats = new ArrayList<>(); + + public LayoutContext(FontMetrics fontMetrics, LytRect viewport) { + this.fontMetrics = fontMetrics; + this.viewport = viewport; + } + + public LytRect viewport() { + return this.viewport; + } + + public int viewportWidth() { + return viewport().width(); + } + + public int viewportHeight() { + return viewport().height(); + } + + public void addLeftFloat(LytRect bounds) { + leftFloats.add(bounds); + } + + public void addRightFloat(LytRect bounds) { + rightFloats.add(bounds); + } + + public OptionalInt getLeftFloatRightEdge() { + return leftFloats.stream() + .mapToInt(LytRect::right) + .max(); + } + + public OptionalInt getRightFloatLeftEdge() { + return rightFloats.stream() + .mapToInt(LytRect::x) + .min(); + } + + // Clears all pending floats and returns the lowest y level below the cleared floats + public OptionalInt clearFloats(boolean left, boolean right) { + if (left && right) { + var result = Streams.concat(leftFloats.stream(), rightFloats.stream()) + .mapToInt(LytRect::bottom).max(); + leftFloats.clear(); + rightFloats.clear(); + return result; + } else if (left) { + var result = leftFloats.stream().mapToInt(LytRect::bottom).max(); + leftFloats.clear(); + return result; + } else if (right) { + var result = rightFloats.stream().mapToInt(LytRect::bottom).max(); + rightFloats.clear(); + return result; + } else { + return OptionalInt.empty(); + } + } + + // Close out all floats above the given y position + public void clearFloatsAbove(int y) { + leftFloats.removeIf(f -> f.bottom() <= y); + rightFloats.removeIf(f -> f.bottom() <= y); + } + + @Override + public float getAdvance(int codePoint, ResolvedTextStyle style) { + return fontMetrics.getAdvance(codePoint, style); + } + + @Override + public int getLineHeight(ResolvedTextStyle style) { + return fontMetrics.getLineHeight(style); + } + + /** + * If there's a float whose bottom edge is below the given y coordinate, return that bottom edge. + */ + public OptionalInt getNextFloatBottomEdge(int y) { + return Streams.concat(leftFloats.stream(), rightFloats.stream()) + .mapToInt(LytRect::bottom) + .filter(bottom -> bottom > y) + .min(); + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/Layouts.java b/src/main/java/appeng/client/guidebook/layout/Layouts.java new file mode 100644 index 00000000000..ac62ba9147d --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/Layouts.java @@ -0,0 +1,50 @@ +package appeng.client.guidebook.layout; + +import java.util.List; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytBlock; + +public final class Layouts { + private Layouts() { + } + + /** + * Lays out all children along the vertical axis, and returns the bounding box of the content area. + */ + public static LytRect verticalLayout( + LayoutContext context, + List children, + int x, int y, int availableWidth, + int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { + // Margins have been applied outside + // Paddings need to be considered here + var innerX = x + paddingLeft; + var innerY = y + paddingTop; + var innerWidth = availableWidth - paddingLeft - paddingRight; + + // Layout children vertically, without padding + LytBlock previousBlock = null; + var contentHeight = paddingTop; + for (var child : children) { + // Account for margins of the child, and margin collapsing + if (previousBlock != null && previousBlock.getMarginBottom() > 0) { + innerY += Math.max(previousBlock.getMarginBottom(), child.getMarginTop()) + - previousBlock.getMarginBottom(); + } else { + innerY += child.getMarginTop(); + } + var blockWidth = Math.max(1, innerWidth - child.getMarginLeft() - child.getMarginRight()); + var childBounds = child.layout(context, innerX + child.getMarginLeft(), innerY, blockWidth); + innerY += childBounds.height() + child.getMarginBottom(); + contentHeight = Math.max(contentHeight, childBounds.bottom() - y); + previousBlock = child; + } + + return new LytRect( + x, y, + availableWidth, + contentHeight + paddingBottom); + } + +} diff --git a/src/main/java/appeng/client/guidebook/layout/MinecraftFontMetrics.java b/src/main/java/appeng/client/guidebook/layout/MinecraftFontMetrics.java new file mode 100644 index 00000000000..1e206b877e6 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/MinecraftFontMetrics.java @@ -0,0 +1,27 @@ +package appeng.client.guidebook.layout; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; + +import appeng.client.guidebook.style.ResolvedTextStyle; + +public class MinecraftFontMetrics implements FontMetrics { + private final Font font; + + public MinecraftFontMetrics() { + this(Minecraft.getInstance().font); + } + + public MinecraftFontMetrics(Font font) { + this.font = font; + } + + public float getAdvance(int codePoint, ResolvedTextStyle style) { + return font.getFontSet(style.font()).getGlyphInfo(codePoint, false) + .getAdvance(Boolean.TRUE.equals(style.bold())); + } + + public int getLineHeight(ResolvedTextStyle style) { + return (int) Math.ceil(font.lineHeight * style.fontScale()); + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/FlowBuilder.java b/src/main/java/appeng/client/guidebook/layout/flow/FlowBuilder.java new file mode 100644 index 00000000000..5279763c1f0 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/FlowBuilder.java @@ -0,0 +1,135 @@ +package appeng.client.guidebook.layout.flow; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.document.flow.LytFlowSpan; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.render.RenderContext; +import appeng.client.guidebook.style.TextAlignment; + +public class FlowBuilder { + private final List lines = new ArrayList<>(); + + private final List rootContent = new ArrayList<>(); + + // Bounding rectangles for any floats in this flow + private final List floats = new ArrayList<>(); + + public void append(LytFlowContent content) { + this.rootContent.add(content); + } + + public LytRect computeLayout(LayoutContext context, int x, int y, int availableWidth, TextAlignment alignment) { + lines.clear(); + floats.clear(); + var lineBuilder = new LineBuilder(context, x, y, availableWidth, lines, floats, alignment); + for (var content : rootContent) { + visitInDocumentOrder(content, lineBuilder); + } + lineBuilder.end(); + + // Build bounding box around all lines + return lineBuilder.getBounds(); + } + + public void renderBatch(RenderContext context, MultiBufferSource buffers, @Nullable LytFlowContent hoveredContent) { + for (var line : lines) { + for (var el = line.firstElement(); el != null; el = el.next) { + el.containsMouse = hoveredContent != null && hoveredContent.isInclusiveAncestor(el.getFlowContent()); + el.renderBatch(context, buffers); + } + } + } + + public void renderFloatsBatch(RenderContext context, MultiBufferSource buffers, + @Nullable LytFlowContent hoveredContent) { + for (var line : lines) { + for (var el = line.firstElement(); el != null; el = el.next) { + el.containsMouse = hoveredContent != null && hoveredContent.isInclusiveAncestor(el.getFlowContent()); + el.renderBatch(context, buffers); + } + } + } + + public void render(RenderContext context, @Nullable LytFlowContent hoveredContent) { + for (var line : lines) { + for (var el = line.firstElement(); el != null; el = el.next) { + el.containsMouse = hoveredContent != null && hoveredContent.isInclusiveAncestor(el.getFlowContent()); + el.render(context); + } + } + } + + public void renderFloats(RenderContext context, @Nullable LytFlowContent hoveredContent) { + for (var el : floats) { + el.containsMouse = hoveredContent != null && hoveredContent.isInclusiveAncestor(el.getFlowContent()); + el.render(context); + } + } + + private void visitInDocumentOrder(LytFlowContent content, Consumer visitor) { + if (content instanceof LytFlowSpan flowSpan) { + for (var child : flowSpan.getChildren()) { + visitInDocumentOrder(child, visitor); + } + } else { + visitor.accept(content); + } + } + + @Nullable + public LineElement pick(int x, int y) { + var floatEl = pickFloatingElement(x, y); + if (floatEl != null) { + return floatEl; + } + + for (var line : lines) { + // Floating content overflows the line-box, but still belongs to the line + // otherwise only hit-test line-elements if the line itself is hit + if (line.bounds().contains(x, y)) { + for (var el = line.firstElement(); el != null; el = el.next) { + if (el.bounds.contains(x, y)) { + return el; + } + } + } + } + + return null; + } + + public Stream enumerateContentBounds(LytFlowContent content) { + return Stream.concat(lines.stream().flatMap(Line::elements), floats.stream()) + .filter(el -> el.getFlowContent() == content) + .map(el -> el.bounds); + } + + @Nullable + public LineBlock pickFloatingElement(int x, int y) { + for (var el : floats) { + if (el.bounds.contains(x, y)) { + return el; + } + } + return null; + } + + public boolean floatsIntersect(LytRect bounds) { + for (var el : floats) { + if (el.bounds.intersects(bounds)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/Line.java b/src/main/java/appeng/client/guidebook/layout/flow/Line.java new file mode 100644 index 00000000000..7c79ebac399 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/Line.java @@ -0,0 +1,12 @@ +package appeng.client.guidebook.layout.flow; + +import java.util.Objects; +import java.util.stream.Stream; + +import appeng.client.guidebook.document.LytRect; + +record Line(LytRect bounds, LineElement firstElement) { + Stream elements() { + return Stream.iterate(firstElement, Objects::nonNull, el -> el.next); + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/LineBlock.java b/src/main/java/appeng/client/guidebook/layout/flow/LineBlock.java new file mode 100644 index 00000000000..46e46c23a76 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/LineBlock.java @@ -0,0 +1,32 @@ +package appeng.client.guidebook.layout.flow; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.render.RenderContext; + +/** + * Standalone block in-line with other content. + */ +public class LineBlock extends LineElement { + + private final LytBlock block; + + public LineBlock(LytBlock block) { + this.block = block; + } + + public LytBlock getBlock() { + return block; + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + block.renderBatch(context, buffers); + } + + @Override + public void render(RenderContext context) { + block.render(context); + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/LineBuilder.java b/src/main/java/appeng/client/guidebook/layout/flow/LineBuilder.java new file mode 100644 index 00000000000..42cfb7059da --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/LineBuilder.java @@ -0,0 +1,387 @@ +package appeng.client.guidebook.layout.flow; + +import java.util.List; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.document.DefaultStyles; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.InlineBlockAlignment; +import appeng.client.guidebook.document.flow.LytFlowBreak; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.document.flow.LytFlowInlineBlock; +import appeng.client.guidebook.document.flow.LytFlowText; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.style.ResolvedTextStyle; +import appeng.client.guidebook.style.TextAlignment; + +/** + * Does inline-flow layout similar to how it is described here: + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flow_Layout/Block_and_Inline_Layout_in_Normal_Flow + */ +class LineBuilder implements Consumer { + private final LayoutContext context; + private final List lines; + // Contains any floating elements we construct as part of processing flow content + private final List floats; + private final int lineBoxX; + private final int startY; + private int innerX; + private int lineBoxY; + private final int lineBoxWidth; + private int remainingLineWidth; + @Nullable + private LineElement openLineElement; + private final TextAlignment alignment; + + public LineBuilder(LayoutContext context, + int x, + int y, + int availableWidth, + List lines, + List floats, + TextAlignment alignment) { + this.floats = floats; + this.alignment = alignment; + this.context = context; + this.startY = y; + lineBoxX = x; + lineBoxY = y; + lineBoxWidth = availableWidth; + remainingLineWidth = getAvailableHorizontalSpace(); + this.lines = lines; + } + + @Override + public void accept(LytFlowContent content) { + if (content instanceof LytFlowText text) { + appendText(text.getText(), content); + } else if (content instanceof LytFlowBreak) { + appendBreak(content); + } else if (content instanceof LytFlowInlineBlock inlineBlock) { + appendInlineBlock(inlineBlock); + } else { + throw new IllegalArgumentException("Don't know how to layout flow content: " + content); + } + } + + private void appendBreak(@Nullable LytFlowContent flowContent) { + // Append an empty line with the default style + if (openLineElement == null) { + openLineElement = new LineTextRun("", DefaultStyles.BASE_STYLE, DefaultStyles.BASE_STYLE); + openLineElement.flowContent = flowContent; + } + endLine(); + + // Clear floats, if requested + if (flowContent instanceof LytFlowBreak flowBreak) { + if (flowBreak.isClearLeft() || flowBreak.isClearRight()) { + context.clearFloats(flowBreak.isClearLeft(), flowBreak.isClearRight()) + .ifPresent(floatBottom -> lineBoxY = Math.max(lineBoxY, floatBottom)); + } + } + } + + private void appendInlineBlock(LytFlowInlineBlock inlineBlock) { + var size = inlineBlock.getPreferredSize(lineBoxWidth); + var block = inlineBlock.getBlock(); + var marginLeft = block.getMarginLeft(); + var marginRight = block.getMarginRight(); + var marginTop = block.getMarginTop(); + var marginBottom = block.getMarginBottom(); + + // Is there enough space to have this element here? + var outerWidth = size.width() + marginLeft + marginRight; + ensureSpaceIsAvailable(outerWidth); + + var el = new LineBlock(block); + el.bounds = new LytRect(innerX + marginLeft, marginTop, size.width(), size.height()); + el.flowContent = inlineBlock; + + if (inlineBlock.getAlignment() == InlineBlockAlignment.FLOAT_LEFT) { + // Float it to the left of the actual text content. + // endLine will take care of moving any existing text in the line + el.bounds = el.bounds.withX(getInnerLeftEdge() + marginLeft).withY(lineBoxY + marginTop); + // Update the layout of the contained block to update its absolute position + block.layout(context, el.bounds.x(), el.bounds.y(), size.width()); + el.floating = true; + context.addLeftFloat(el.bounds.expand(0, 0, marginRight, marginBottom)); + floats.add(el); + remainingLineWidth -= outerWidth; + } else if (inlineBlock.getAlignment() == InlineBlockAlignment.FLOAT_RIGHT) { + // Float it to the right the actual text content. + el.bounds = el.bounds.withX(getInnerRightEdge() - el.bounds.width() + marginRight) + .withY(lineBoxY + marginTop); + // Update the layout of the contained block to update its absolute position + block.layout(context, el.bounds.x(), el.bounds.y(), size.width()); + el.floating = true; + context.addRightFloat(el.bounds.expand(marginLeft, 0, 0, marginBottom)); + floats.add(el); + remainingLineWidth -= outerWidth; + } else { + // Treat as a normal inline element for positioning + innerX += size.width(); + appendToOpenLine(el); + + // Since no margin is actually accounted for here, the remaining line width should just + // be reduced + remainingLineWidth -= size.width(); + } + } + + private void ensureSpaceIsAvailable(int width) { + if (width <= remainingLineWidth) { + return; // Got enough + } + + // First, try closing out any open line if we don't have enough space + endLine(); + + if (width <= remainingLineWidth) { + return; // We got enough by ending the current line and advancing to the next + } + + // If we *still* don't have enough room, we need to advance down to clear floats + // as long as any float is still open + var nextFloatEdge = context.getNextFloatBottomEdge(lineBoxY); + while (nextFloatEdge.isPresent()) { + lineBoxY = nextFloatEdge.getAsInt(); + context.clearFloatsAbove(lineBoxY); + remainingLineWidth = getAvailableHorizontalSpace(); + if (width <= remainingLineWidth) { + break; // Finally, we're good! + } + nextFloatEdge = context.getNextFloatBottomEdge(lineBoxY); + } + } + + @Nullable + private LineElement getEndOfOpenLine() { + var el = openLineElement; + if (el != null) { + while (el.next != null) { + el = el.next; + } + } + return el; + } + + private void appendText(String text, LytFlowContent flowContent) { + var style = flowContent.resolveStyle(); + var hoverStyle = flowContent.resolveHoverStyle(style); + + char lastChar = '\0'; + var endOfOpenLine = getEndOfOpenLine(); + if (endOfOpenLine instanceof LineTextRun textRun && !textRun.text.isEmpty()) { + lastChar = textRun.text.charAt(textRun.text.length() - 1); + } else if (endOfOpenLine == null || endOfOpenLine.floating) { + // Treat the first text in a line or text directly after a float as if it was after a line-break. + lastChar = '\n'; + } + + iterateRuns(text, style, lastChar, (run, width, endLine) -> { + if (!run.isEmpty()) { + var el = new LineTextRun(run.toString(), style, hoverStyle); + el.flowContent = flowContent; + el.bounds = new LytRect( + innerX, + 0, + Math.round(width), + context.getLineHeight(style)); + appendToOpenLine(el); + innerX += el.bounds.width(); + remainingLineWidth -= el.bounds.width(); + } + if (endLine) { + endLine(); + } + }); + } + + private void iterateRuns(CharSequence text, ResolvedTextStyle style, char lastChar, LineConsumer consumer) { + int lastBreakOpportunity = -1; + float widthAtBreakOpportunity = 0; + float curLineWidth = 0; + + var lineBuffer = new StringBuilder(); + + boolean lastCharWasWhitespace = Character.isWhitespace(lastChar); + // When starting after a whitespace on an existing line, we have a break opportunity at the start + if (lastCharWasWhitespace) { + lastBreakOpportunity = 0; + } + + for (var i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + int codePoint = ch; + + // UTF-16 surrogate handling + if (Character.isHighSurrogate(ch) && i + 1 < text.length()) { + // Always consume the next char if it's a low surrogate + char low = text.charAt(i + 1); + if (Character.isLowSurrogate(low)) { + i++; // Skip the low surrogate + codePoint = Character.toCodePoint(ch, low); + } + } + + // Handle explicit line breaks + if (codePoint == '\n') { + if (style.whiteSpace().isCollapseSegmentBreaks()) { + codePoint = ' '; + } else { + consumer.visitRun(lineBuffer, curLineWidth, true); + lineBuffer.setLength(0); + widthAtBreakOpportunity = curLineWidth = 0; + lastBreakOpportunity = 0; + lastCharWasWhitespace = true; + remainingLineWidth = getAvailableHorizontalSpace(); + continue; + } + } + + if (Character.isWhitespace(codePoint)) { + // Skip if the last one was a space already + if (lastCharWasWhitespace && style.whiteSpace().isCollapseWhitespace()) { + continue; // White space collapsing + } + // Treat spaces as a safe-point for going back to when needing to line-break later + lastBreakOpportunity = lineBuffer.length(); + widthAtBreakOpportunity = curLineWidth; + lastCharWasWhitespace = true; + } else { + lastCharWasWhitespace = false; + } + + var advance = context.getAdvance(codePoint, style); + // Break line if necessary + if (curLineWidth + advance > remainingLineWidth) { + // If we had a break opportunity, use it + // In this scenario, the space itself is discarded + if (lastBreakOpportunity != -1) { + consumer.visitRun(lineBuffer.subSequence(0, lastBreakOpportunity), widthAtBreakOpportunity, true); + curLineWidth -= widthAtBreakOpportunity; + lineBuffer.delete(0, lastBreakOpportunity); + if (!lineBuffer.isEmpty() && Character.isWhitespace(lineBuffer.charAt(0))) { + var firstChar = lineBuffer.charAt(0); + lineBuffer.deleteCharAt(0); + curLineWidth -= context.getAdvance(firstChar, style); + } + } else { + // We exceeded the line length, but did not find a break opportunity + // this causes a forced break mid-word + consumer.visitRun(lineBuffer, curLineWidth, true); + lineBuffer.setLength(0); + curLineWidth = 0; + } + lastBreakOpportunity = 0; + widthAtBreakOpportunity = curLineWidth; + remainingLineWidth = getAvailableHorizontalSpace(); + // If a white-space character broke the line, ignore it as it + // would otherwise be at the start of the next line + if (lastCharWasWhitespace) { + continue; + } + } + curLineWidth += advance; + lineBuffer.appendCodePoint(codePoint); + } + + if (!lineBuffer.isEmpty()) { + consumer.visitRun(lineBuffer, curLineWidth, false); + } + } + + private void endLine() { + if (openLineElement == null) { + return; + } + + var lineHeight = 1; + var lineWidth = 0; + for (var el = openLineElement; el != null; el = el.next) { + lineHeight = Math.max(lineHeight, el.bounds.bottom()); + lineWidth = Math.max(lineWidth, el.bounds.right()); + } + + var textAreaStart = getInnerLeftEdge(); + var textAreaEnd = getInnerRightEdge(); + + // Apply alignment + int xTranslation = textAreaStart; + if (alignment == TextAlignment.RIGHT) { + xTranslation = textAreaEnd - lineWidth; + } else if (alignment == TextAlignment.CENTER) { + xTranslation = textAreaStart + ((textAreaEnd - textAreaStart) - lineWidth) / 2; + } + + // reposition all line elements + for (var el = openLineElement; el != null; el = el.next) { + el.bounds = el.bounds.move(xTranslation, lineBoxY); + // Ensure that inline blocks update their blocks absolute position + if (el instanceof LineBlock lineBlock) { + lineBlock.getBlock().layout(context, el.bounds.x(), el.bounds.y(), el.bounds.width()); + } + } + + var lineBounds = new LytRect(lineBoxX, lineBoxY, lineBoxWidth, lineHeight); + var line = new Line(lineBounds, openLineElement); + lines.add(line); + + // Advance vertically + lineBoxY += line.bounds().height(); + + // Close out any floats that are above the fold + context.clearFloatsAbove(lineBoxY); + + // Reset horizontal position + openLineElement = null; + innerX = 0; + + // Recompute now that floats may have been closed, what the horizontal space really is + remainingLineWidth = getInnerRightEdge() - getInnerLeftEdge(); + } + + // How much horizontal space is available in a new line, accounting for active floats that take up space + private int getAvailableHorizontalSpace() { + return Math.max(0, getInnerRightEdge() - getInnerLeftEdge()); + } + + // Absolute X coord of the beginning of the text area of the current line box + private int getInnerLeftEdge() { + return context.getLeftFloatRightEdge().orElse(lineBoxX); + } + + // Absolute X coord of the end of the text area of the current line box + private int getInnerRightEdge() { + return context.getRightFloatLeftEdge().orElse(this.lineBoxX + lineBoxWidth); + } + + private void appendToOpenLine(LineElement el) { + if (openLineElement != null) { + var l = openLineElement; + while (l.next != null) { + l = l.next; + } + l.next = el; + } else { + openLineElement = el; + } + } + + public void end() { + endLine(); + } + + public LytRect getBounds() { + return new LytRect( + lineBoxX, startY, + lineBoxWidth, lineBoxY - startY); + } + + @FunctionalInterface + interface LineConsumer { + void visitRun(CharSequence run, float width, boolean end); + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/LineElement.java b/src/main/java/appeng/client/guidebook/layout/flow/LineElement.java new file mode 100644 index 00000000000..65911211b06 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/LineElement.java @@ -0,0 +1,46 @@ +package appeng.client.guidebook.layout.flow; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowContent; +import appeng.client.guidebook.render.RenderContext; + +public abstract class LineElement { + /** + * Next Element in flow direction. + */ + @Nullable + LineElement next; + + LytRect bounds = LytRect.empty(); + + /** + * The original flow content this line element is associated with. + */ + @Nullable + LytFlowContent flowContent; + + boolean containsMouse; + + boolean floating; + + @Nullable + public LytFlowContent getFlowContent() { + return flowContent; + } + + /** + * Render text content as part of batch rendering. + */ + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + } + + /** + * Render any other content individually. + */ + public void render(RenderContext context) { + } +} diff --git a/src/main/java/appeng/client/guidebook/layout/flow/LineTextRun.java b/src/main/java/appeng/client/guidebook/layout/flow/LineTextRun.java new file mode 100644 index 00000000000..553efc39488 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/layout/flow/LineTextRun.java @@ -0,0 +1,25 @@ +package appeng.client.guidebook.layout.flow; + +import net.minecraft.client.renderer.MultiBufferSource; + +import appeng.client.guidebook.render.RenderContext; +import appeng.client.guidebook.style.ResolvedTextStyle; + +public class LineTextRun extends LineElement { + final String text; + final ResolvedTextStyle style; + final ResolvedTextStyle hoverStyle; + + public LineTextRun(String text, ResolvedTextStyle style, ResolvedTextStyle hoverStyle) { + this.text = text; + this.style = style; + this.hoverStyle = hoverStyle; + } + + @Override + public void renderBatch(RenderContext context, MultiBufferSource buffers) { + var style = containsMouse ? this.hoverStyle : this.style; + + context.renderTextInBatch(text, style, (float) bounds.x(), (float) bounds.y(), buffers); + } +} diff --git a/src/main/java/appeng/client/guidebook/navigation/NavigationNode.java b/src/main/java/appeng/client/guidebook/navigation/NavigationNode.java new file mode 100644 index 00000000000..675b00eab5d --- /dev/null +++ b/src/main/java/appeng/client/guidebook/navigation/NavigationNode.java @@ -0,0 +1,15 @@ +package appeng.client.guidebook.navigation; + +import java.util.List; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +public record NavigationNode( + ResourceLocation pageId, + String title, + ItemStack icon, + List children, + int position, + boolean hasPage) { +} diff --git a/src/main/java/appeng/client/guidebook/navigation/NavigationTree.java b/src/main/java/appeng/client/guidebook/navigation/NavigationTree.java new file mode 100644 index 00000000000..23dda64c714 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/navigation/NavigationTree.java @@ -0,0 +1,150 @@ +package appeng.client.guidebook.navigation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.annotation.Nullable; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +import appeng.client.guidebook.compiler.ParsedGuidePage; + +public class NavigationTree { + + private static final Logger LOGGER = LoggerFactory.getLogger(NavigationTree.class); + + private final Map nodeIndex; + + private final List rootNodes; + + public NavigationTree(Map nodeIndex, List rootNodes) { + this.nodeIndex = nodeIndex; + this.rootNodes = rootNodes; + } + + public NavigationTree() { + this.nodeIndex = Map.of(); + this.rootNodes = List.of(); + } + + public List getRootNodes() { + return rootNodes; + } + + @Nullable + public NavigationNode getNodeById(ResourceLocation pageId) { + return nodeIndex.get(pageId); + } + + public static NavigationTree build(Collection pages) { + var pagesWithChildren = new HashMap>>(); + + // First pass, build a map of pages and their children + for (var page : pages) { + var navigationEntry = page.getFrontmatter().navigationEntry(); + if (navigationEntry == null) { + continue; + } + + // Create an entry for this page to collect any children it might have + pagesWithChildren.compute( + page.getId(), + (resourceLocation, previousPair) -> { + return previousPair != null ? Pair.of(page, previousPair.getRight()) + : Pair.of(page, new ArrayList<>()); + }); + + // Add this page to the collected children of the parent page (if any) + var parentId = navigationEntry.parent(); + if (parentId != null) { + pagesWithChildren.compute( + parentId, (resourceLocation, prevPage) -> { + if (prevPage != null) { + prevPage.getRight().add(page); + return prevPage; + } else { + var children = new ArrayList(); + children.add(page); + return Pair.of(null, children); + } + }); + } + } + + var nodeIndex = new HashMap(pages.size()); + var rootNodes = new ArrayList(); + + for (var entry : pagesWithChildren.entrySet()) { + createNode(nodeIndex, rootNodes, pagesWithChildren, entry.getKey(), entry.getValue()); + } + + // Sort root nodes + rootNodes.sort(NODE_COMPARATOR); + + return new NavigationTree(Map.copyOf(nodeIndex), List.copyOf(rootNodes)); + } + + private static NavigationNode createNode(HashMap nodeIndex, + ArrayList rootNodes, + Map>> pagesWithChildren, + ResourceLocation pageId, + Pair> entry) { + var page = entry.getKey(); + var children = entry.getRight(); + + if (page == null) { + // These children had a parent that doesn't exist + LOGGER.error("Pages {} had unknown navigation parent {}", children, pageId); + return null; + } + + var navigationEntry = Objects.requireNonNull(page.getFrontmatter().navigationEntry(), "navigation frontmatter"); + + // Construct the icon if set + var icon = ItemStack.EMPTY; + if (navigationEntry.iconItemId() != null) { + var iconItem = Registry.ITEM.get(navigationEntry.iconItemId()); + icon = new ItemStack(iconItem); + icon.setTag(navigationEntry.iconNbt()); + if (icon.isEmpty()) { + LOGGER.error("Couldn't find icon {} for icon of page {}", navigationEntry.iconItemId(), page); + } + } + + var childNodes = new ArrayList(children.size()); + for (var childPage : children) { + var childPageEntry = pagesWithChildren.get(childPage.getId()); + + childNodes.add(createNode(nodeIndex, rootNodes, pagesWithChildren, childPage.getId(), childPageEntry)); + } + childNodes.sort(NODE_COMPARATOR); + + var node = new NavigationNode( + page.getId(), + navigationEntry.title(), + icon, + childNodes, + navigationEntry.position(), + true); + nodeIndex.put(page.getId(), node); + if (navigationEntry.parent() == null) { + rootNodes.add(node); + } + return node; + } + + private static final Comparator NODE_COMPARATOR = Comparator.comparingInt(NavigationNode::position) + .thenComparing(NavigationNode::title); + +} diff --git a/src/main/java/appeng/client/guidebook/render/ColorRef.java b/src/main/java/appeng/client/guidebook/render/ColorRef.java new file mode 100644 index 00000000000..8d3b5546615 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/ColorRef.java @@ -0,0 +1,18 @@ +package appeng.client.guidebook.render; + +public class ColorRef { + public static final ColorRef WHITE = new ColorRef(-1); + + final SymbolicColor symbolic; + final int concrete; + + ColorRef(SymbolicColor color) { + this.symbolic = color; + this.concrete = 0; + } + + public ColorRef(int concrete) { + this.concrete = concrete; + this.symbolic = null; + } +} diff --git a/src/main/java/appeng/client/guidebook/render/Colors.java b/src/main/java/appeng/client/guidebook/render/Colors.java new file mode 100644 index 00000000000..d66324f595d --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/Colors.java @@ -0,0 +1,9 @@ +package appeng.client.guidebook.render; + +import net.minecraft.util.FastColor; + +final class Colors { + public static int argb(int a, int r, int g, int b) { + return FastColor.ARGB32.color(a, r, g, b); + } +} diff --git a/src/main/java/appeng/client/guidebook/render/GuidePageTexture.java b/src/main/java/appeng/client/guidebook/render/GuidePageTexture.java new file mode 100644 index 00000000000..56ba1944cb1 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/GuidePageTexture.java @@ -0,0 +1,108 @@ +package appeng.client.guidebook.render; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; + +import com.mojang.blaze3d.platform.NativeImage; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.stb.STBImage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.document.LytSize; +import appeng.core.AppEng; + +/** + * A texture that is used in the context of a single guide page and is automatically cleared from texture memory when + * the guide page it was last used on is closed. + */ +public class GuidePageTexture { + + public static GuidePageTexture missing() { + return new GuidePageTexture(AppEng.makeId("missing"), null); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(GuidePageTexture.class); + + // Textures in use by the current page + private static final Map usedTextures = new IdentityHashMap<>(); + + private final ResourceLocation id; + + private final ByteBuffer imageContent; + + private final LytSize size; + + private GuidePageTexture(ResourceLocation id, byte @Nullable [] imageContent) { + this.id = Objects.requireNonNull(id, "id"); + if (imageContent == null) { + this.imageContent = null; + this.size = new LytSize(32, 32); + } else { + this.imageContent = ByteBuffer.allocateDirect(imageContent.length); + this.imageContent.put(imageContent).flip(); + + var xOut = new int[1]; + var yOut = new int[1]; + var compOut = new int[1]; + if (!STBImage.stbi_info_from_memory(this.imageContent, xOut, yOut, compOut)) { + throw new IllegalArgumentException( + "Couldn't determine size of image " + id + ": " + STBImage.stbi_failure_reason()); + } + + // Try to determine the size + this.size = new LytSize(xOut[0], yOut[0]); + } + } + + public ResourceLocation getId() { + return id; + } + + public LytSize getSize() { + return size; + } + + public static GuidePageTexture load(ResourceLocation id, byte[] imageContent) { + try { + return new GuidePageTexture(id, imageContent); + } catch (Exception e) { + LOGGER.error("Failed to get image {}: {}", id, e.toString()); + return missing(); + } + } + + public AbstractTexture use() { + return usedTextures.computeIfAbsent(this, guidePageTexture -> { + if (guidePageTexture.imageContent == null) { + return MissingTextureAtlasSprite.getTexture(); + } + + try { + var nativeImage = NativeImage.read(guidePageTexture.imageContent); + return new DynamicTexture(nativeImage); + } catch (IOException e) { + LOGGER.error("Failed to read image {}: {}", guidePageTexture.id, e.toString()); + return MissingTextureAtlasSprite.getTexture(); + } + }); + } + + public static void releaseUsedTextures() { + for (DynamicTexture texture : usedTextures.values()) { + if (texture != MissingTextureAtlasSprite.getTexture()) { + texture.close(); + } + } + usedTextures.clear(); + } +} diff --git a/src/main/java/appeng/client/guidebook/render/LightDarkMode.java b/src/main/java/appeng/client/guidebook/render/LightDarkMode.java new file mode 100644 index 00000000000..087e82aea6a --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/LightDarkMode.java @@ -0,0 +1,6 @@ +package appeng.client.guidebook.render; + +public enum LightDarkMode { + LIGHT_MODE, + DARK_MODE +} diff --git a/src/main/java/appeng/client/guidebook/render/RenderContext.java b/src/main/java/appeng/client/guidebook/render/RenderContext.java new file mode 100644 index 00000000000..1d7cb763764 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/RenderContext.java @@ -0,0 +1,179 @@ +package appeng.client.guidebook.render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.math.Vector3f; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec2; + +import appeng.client.gui.Icon; +import appeng.client.gui.style.BackgroundGenerator; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.screen.GuideScreen; +import appeng.client.guidebook.style.ResolvedTextStyle; + +public interface RenderContext { + + LightDarkMode lightDarkMode(); + + default boolean isDarkMode() { + return lightDarkMode() == LightDarkMode.DARK_MODE; + } + + GuideScreen screen(); + + PoseStack poseStack(); + + LytRect viewport(); + + int resolveColor(ColorRef ref); + + void fillRect(LytRect rect, ColorRef topLeft, ColorRef topRight, ColorRef bottomRight, ColorRef bottomLeft); + + default void fillTexturedRect(LytRect rect, AbstractTexture texture, ColorRef topLeft, ColorRef topRight, + ColorRef bottomRight, ColorRef bottomLeft) { + // Just use the entire texture by default + fillTexturedRect(rect, texture, topLeft, topRight, bottomRight, bottomLeft, 0, 0, 1, 1); + } + + void fillTexturedRect(LytRect rect, AbstractTexture texture, ColorRef topLeft, ColorRef topRight, + ColorRef bottomRight, ColorRef bottomLeft, float u0, float v0, float u1, float v1); + + default void fillTexturedRect(LytRect rect, GuidePageTexture texture) { + fillTexturedRect(rect, texture.use(), ColorRef.WHITE); + } + + default void fillTexturedRect(LytRect rect, AbstractTexture texture) { + fillTexturedRect(rect, texture, ColorRef.WHITE); + } + + default void fillTexturedRect(LytRect rect, AbstractTexture texture, ColorRef color) { + fillTexturedRect(rect, texture, color, color, color, color); + } + + default void fillTexturedRect(LytRect rect, GuidePageTexture texture, ColorRef color) { + fillTexturedRect(rect, texture.use(), color, color, color, color); + } + + default void fillTexturedRect(LytRect rect, TextureAtlasSprite sprite, ColorRef color) { + fillTexturedRect(rect, sprite.atlas(), color, color, color, color, + sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1()); + } + + default void drawIcon(int x, int y, Icon icon, ColorRef color) { + var u0 = icon.x / (float) Icon.TEXTURE_WIDTH; + var v0 = icon.y / (float) Icon.TEXTURE_HEIGHT; + var u1 = (icon.x + icon.width) / (float) Icon.TEXTURE_WIDTH; + var v1 = (icon.y + icon.height) / (float) Icon.TEXTURE_HEIGHT; + + var texture = Minecraft.getInstance().getTextureManager().getTexture(Icon.TEXTURE); + fillTexturedRect(new LytRect(x, y, icon.width, icon.height), texture, color, color, color, color, + u0, v0, u1, v1); + } + + default void fillTexturedRect(LytRect rect, ResourceLocation textureId) { + fillTexturedRect(rect, textureId, ColorRef.WHITE); + } + + default void fillTexturedRect(LytRect rect, ResourceLocation textureId, ColorRef color) { + var texture = Minecraft.getInstance().getTextureManager().getTexture(textureId); + fillTexturedRect(rect, texture, color); + } + + void fillTriangle(Vec2 p1, Vec2 p2, Vec2 p3, ColorRef color); + + default Font font() { + return Minecraft.getInstance().font; + } + + default float getAdvance(int codePoint, ResolvedTextStyle style) { + return font().getFontSet(style.font()).getGlyphInfo(codePoint, false) + .getAdvance(Boolean.TRUE.equals(style.bold())); + } + + default float getWidth(String text, ResolvedTextStyle style) { + return (float) text.codePoints() + .mapToDouble(cp -> getAdvance(cp, style)) + .sum(); + } + + default void renderText(String text, ResolvedTextStyle style, float x, float y) { + var bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + renderTextInBatch(text, style, x, y, bufferSource); + bufferSource.endBatch(); + } + + default void renderTextInBatch(String text, ResolvedTextStyle style, float x, float y, MultiBufferSource buffers) { + var effectiveStyle = Style.EMPTY + .withBold(style.bold()) + .withItalic(style.italic()) + .withUnderlined(style.underlined()) + .withStrikethrough(style.strikethrough()) + .withFont(style.font()); + + var matrix = poseStack().last().pose(); + if (style.fontScale() != 1) { + matrix = matrix.copy(); + + matrix.multiplyWithTranslation(style.fontScale(), style.fontScale(), 1); + matrix.translate(new Vector3f(x, y, 0)); + x = 0; + y = 0; + } + + font().drawInBatch(Component.literal(text).withStyle(effectiveStyle), x, y, resolveColor(style.color()), false, + matrix, buffers, false, 0, LightTexture.FULL_BRIGHT); + } + + default void fillRect(int x, int y, int width, int height, ColorRef color) { + fillRect(new LytRect(x, y, width, height), color); + } + + default void fillRect(LytRect rect, ColorRef color) { + fillRect(rect, color, color, color, color); + } + + default void fillGradientVertical(LytRect rect, ColorRef top, ColorRef bottom) { + fillRect(rect, top, top, bottom, bottom); + } + + default void fillGradientVertical(int x, int y, int width, int height, ColorRef top, ColorRef bottom) { + fillGradientVertical(new LytRect(x, y, width, height), top, bottom); + } + + default void fillGradientHorizontal(LytRect rect, ColorRef left, ColorRef right) { + fillRect(rect, left, right, right, left); + } + + default void fillGradientHorizontal(int x, int y, int width, int height, ColorRef left, ColorRef right) { + fillGradientHorizontal(new LytRect(x, y, width, height), left, right); + } + + default MultiBufferSource.BufferSource beginBatch() { + return MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + } + + default void endBatch(MultiBufferSource.BufferSource batch) { + batch.endBatch(); + } + + default void renderItem(ItemStack stack, int x, int y, float width, float height) { + renderItem(stack, x, y, 0, width, height); + } + + void renderItem(ItemStack stack, int x, int y, int z, float width, float height); + + default void renderPanel(LytRect bounds) { + BackgroundGenerator.draw(bounds.width(), bounds.height(), poseStack(), 0, bounds.x(), bounds.y()); + } +} diff --git a/src/main/java/appeng/client/guidebook/render/SimpleRenderContext.java b/src/main/java/appeng/client/guidebook/render/SimpleRenderContext.java new file mode 100644 index 00000000000..43a29c240f5 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/SimpleRenderContext.java @@ -0,0 +1,142 @@ +package appeng.client.guidebook.render; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.block.model.ItemTransforms; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec2; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.screen.GuideScreen; + +public record SimpleRenderContext(@Override GuideScreen screen, + @Override LytRect viewport, + @Override PoseStack poseStack, + @Override LightDarkMode lightDarkMode) implements RenderContext { + + @Override + public int resolveColor(ColorRef ref) { + if (ref.symbolic != null) { + return ref.symbolic.resolve(lightDarkMode); + } else { + return ref.concrete; + } + } + + @Override + public void fillRect(LytRect rect, ColorRef topLeft, ColorRef topRight, ColorRef bottomRight, ColorRef bottomLeft) { + RenderSystem.disableTexture(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + var tesselator = Tesselator.getInstance(); + var builder = tesselator.getBuilder(); + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + var matrix = poseStack.last().pose(); + final int z = 0; + builder.vertex(matrix, rect.right(), rect.y(), z).color(resolveColor(topRight)).endVertex(); + builder.vertex(matrix, rect.x(), rect.y(), z).color(resolveColor(topLeft)).endVertex(); + builder.vertex(matrix, rect.x(), rect.bottom(), z).color(resolveColor(bottomLeft)).endVertex(); + builder.vertex(matrix, rect.right(), rect.bottom(), z).color(resolveColor(bottomRight)).endVertex(); + tesselator.end(); + RenderSystem.disableBlend(); + RenderSystem.enableTexture(); + } + + @Override + public void fillTexturedRect(LytRect rect, AbstractTexture texture, ColorRef topLeft, ColorRef topRight, + ColorRef bottomRight, ColorRef bottomLeft, float u0, float v0, float u1, float v1) { + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + RenderSystem.setShaderTexture(0, texture.getId()); + var tesselator = Tesselator.getInstance(); + var builder = tesselator.getBuilder(); + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + var matrix = poseStack.last().pose(); + final int z = 0; + builder.vertex(matrix, rect.right(), rect.y(), z).uv(u1, v0).color(resolveColor(topRight)).endVertex(); + builder.vertex(matrix, rect.x(), rect.y(), z).uv(u0, v0).color(resolveColor(topLeft)).endVertex(); + builder.vertex(matrix, rect.x(), rect.bottom(), z).uv(u0, v1).color(resolveColor(bottomLeft)).endVertex(); + builder.vertex(matrix, rect.right(), rect.bottom(), z).uv(u1, v1).color(resolveColor(bottomRight)).endVertex(); + tesselator.end(); + RenderSystem.disableBlend(); + } + + @Override + public void fillTriangle(Vec2 p1, Vec2 p2, Vec2 p3, ColorRef color) { + var resolvedColor = resolveColor(color); + + RenderSystem.disableTexture(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + var tesselator = Tesselator.getInstance(); + var builder = tesselator.getBuilder(); + builder.begin(VertexFormat.Mode.TRIANGLES, DefaultVertexFormat.POSITION_COLOR); + var matrix = poseStack.last().pose(); + final int z = 0; + builder.vertex(matrix, p1.x, p1.y, z).color(resolvedColor).endVertex(); + builder.vertex(matrix, p2.x, p2.y, z).color(resolvedColor).endVertex(); + builder.vertex(matrix, p3.x, p3.y, z).color(resolvedColor).endVertex(); + tesselator.end(); + RenderSystem.disableBlend(); + RenderSystem.enableTexture(); + } + + @Override + public void renderItem(ItemStack stack, int x, int y, int z, float width, float height) { + var itemRenderer = Minecraft.getInstance().getItemRenderer(); + var textureManager = Minecraft.getInstance().getTextureManager(); + + var model = itemRenderer.getModel(stack, null, null, 0); + + // Essentially the same code as in itemrenderer renderInGui, but we're passing our own posestack + textureManager.getTexture(TextureAtlas.LOCATION_BLOCKS).setFilter(false, false); + RenderSystem.setShaderTexture(0, TextureAtlas.LOCATION_BLOCKS); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + + poseStack.pushPose(); + poseStack.translate(x, y, z + 1); + poseStack.translate(width / 2, height / 2, 0.0); + poseStack.scale(1.0F, -1.0F, 1.0F); + poseStack.scale(width, height, 1f); + var buffers = Minecraft.getInstance().renderBuffers().bufferSource(); + boolean flatLighting = !model.usesBlockLight(); + if (flatLighting) { + Lighting.setupForFlatItems(); + } else { + Lighting.setupForEntityInInventory(); + } + + itemRenderer.render(stack, + ItemTransforms.TransformType.GUI, + false, + poseStack, + buffers, + LightTexture.FULL_BRIGHT, + OverlayTexture.NO_OVERLAY, + model); + buffers.endBatch(); + RenderSystem.enableDepthTest(); + if (flatLighting) { + Lighting.setupFor3DItems(); + } + + poseStack.popPose(); + } +} diff --git a/src/main/java/appeng/client/guidebook/render/SymbolicColor.java b/src/main/java/appeng/client/guidebook/render/SymbolicColor.java new file mode 100644 index 00000000000..012ce975924 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/render/SymbolicColor.java @@ -0,0 +1,39 @@ +package appeng.client.guidebook.render; + +/** + * TODO Colors from WIP patchouli book "nameplate_color": "8A5BA4", "link_color": "8A5BA4", "link_hover_color": + * "D7BBEC", "macros": { "$(item)": "$(#582E70)", "$(thing)": "$(#582E70)", "$(todo)": "$(#FF0000)" }, + */ +public enum SymbolicColor { + LINK(Colors.argb(255, 0, 213, 255), Colors.argb(255, 0, 213, 255)), + BODY_TEXT(Colors.argb(255, 174, 174, 174), Colors.argb(255, 174, 174, 174)), + /** + * Color used for the type of crafting shown in recipe blocks. + */ + CRAFTING_RECIPE_TYPE(Colors.argb(255, 64, 64, 64), Colors.argb(255, 64, 64, 64)), + THEMATIC_BREAK(Colors.argb(255, 55, 55, 55), Colors.argb(255, 155, 155, 155)), + + NAVBAR_BG_TOP(Colors.argb(255, 0, 0, 0), Colors.argb(255, 0, 0, 0)), + NAVBAR_BG_BOTTOM(Colors.argb(127, 0, 0, 0), Colors.argb(127, 0, 0, 0)), + NAVBAR_ROW_HOVER(Colors.argb(255, 33, 33, 33), Colors.argb(255, 33, 33, 33)), + NAVBAR_EXPAND_ARROW(Colors.argb(255, 238, 238, 238), Colors.argb(255, 238, 238, 238)), + TABLE_BORDER(Colors.argb(255, 124, 124, 124), Colors.argb(255, 124, 124, 124)); + + final int lightMode; + final int darkMode; + + SymbolicColor(int lightMode, int darkMode) { + this.lightMode = lightMode; + this.darkMode = darkMode; + } + + private final ColorRef ref = new ColorRef(this); + + public ColorRef ref() { + return ref; + } + + public int resolve(LightDarkMode lightDarkMode) { + return lightDarkMode == LightDarkMode.LIGHT_MODE ? lightMode : darkMode; + } +} diff --git a/src/main/java/appeng/client/guidebook/screen/GuideNavBar.java b/src/main/java/appeng/client/guidebook/screen/GuideNavBar.java new file mode 100644 index 00000000000..59291c5a8be --- /dev/null +++ b/src/main/java/appeng/client/guidebook/screen/GuideNavBar.java @@ -0,0 +1,361 @@ +package appeng.client.guidebook.screen; + +import java.util.ArrayList; +import java.util.List; + +import com.mojang.blaze3d.vertex.PoseStack; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec2; + +import appeng.client.Point; +import appeng.client.guidebook.GuideManager; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytParagraph; +import appeng.client.guidebook.document.flow.LytFlowSpan; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.MinecraftFontMetrics; +import appeng.client.guidebook.navigation.NavigationNode; +import appeng.client.guidebook.navigation.NavigationTree; +import appeng.client.guidebook.render.LightDarkMode; +import appeng.client.guidebook.render.SimpleRenderContext; +import appeng.client.guidebook.render.SymbolicColor; + +public class GuideNavBar extends AbstractWidget { + private static final int WIDTH_CLOSED = 15; + private static final int WIDTH_OPEN = 150; + private static final int CHILD_ROW_INDENT = 10; + private static final int PARENT_ROW_INDENT = 7; + + private NavigationTree navTree; + + private final List rows = new ArrayList<>(); + + private final GuideScreen screen; + + private int scrollOffset; + + private State state = State.CLOSED; + + public GuideNavBar(GuideScreen screen) { + super(0, 0, WIDTH_CLOSED, screen.height, Component.literal("Navigation Tree")); + this.screen = screen; + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) { + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (state != State.OPENING && state != State.OPEN) { + return; + } + + var row = pickRow(mouseX, mouseY); + if (row != null) { + row.expanded = !row.expanded; + updateLayout(); + + screen.navigateTo(row.node.pageId()); + } + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (state != State.OPENING && state != State.OPEN) { + return false; + } + + setScrollOffset((int) Math.round(scrollOffset - dragY)); + return true; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (state != State.OPENING && state != State.OPEN) { + return false; + } + + setScrollOffset((int) Math.round(scrollOffset - delta * 20)); + return true; + } + + private void setScrollOffset(int offset) { + var maxScrollOffset = 0; + if (!rows.isEmpty()) { + var contentHeight = rows.get(rows.size() - 1).bottom - rows.get(0).top; + maxScrollOffset = Math.max(0, contentHeight - height); + } + scrollOffset = Mth.clamp(offset, 0, maxScrollOffset); + } + + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + var viewport = new LytRect(0, scrollOffset, width, height); + var renderContext = new SimpleRenderContext(screen, viewport, poseStack, LightDarkMode.LIGHT_MODE); + + boolean containsMouse = (mouseX >= x && mouseY >= y && mouseX < x + width && mouseY <= y + height); + switch (state) { + case CLOSED -> { + if (containsMouse) { + state = State.OPENING; + } + } + case OPENING -> { + width = Math.round(width + Math.max(1, partialTick * (WIDTH_OPEN - WIDTH_CLOSED))); + if (width >= WIDTH_OPEN) { + width = WIDTH_OPEN; + state = State.OPEN; + } + } + case OPEN -> { + if (!containsMouse) { + state = State.CLOSING; + } + } + case CLOSING -> { + width = Math.round(width - Math.max(1, partialTick * (WIDTH_OPEN - WIDTH_CLOSED))); + if (width <= WIDTH_CLOSED) { + width = WIDTH_CLOSED; + state = State.CLOSED; + } + } + } + + updateMousePos(mouseX, mouseY); + + // Check if we need to re-layout + var currentNavTree = GuideManager.INSTANCE.getNavigationTree(); + if (currentNavTree != this.navTree) { + recreateRows(); + } + + if (state == State.CLOSED) { + renderContext.fillGradientHorizontal(x, y, width, height, SymbolicColor.NAVBAR_BG_TOP.ref(), + SymbolicColor.NAVBAR_BG_BOTTOM.ref()); + + var p1 = new Vec2(width - 4, height / 2f); + var p2 = new Vec2(4, height / 2f - 5); + var p3 = new Vec2(4, height / 2f + 5); + + renderContext.fillTriangle(p1, p2, p3, SymbolicColor.NAVBAR_EXPAND_ARROW.ref()); + } else { + renderContext.fillGradientVertical(x, y, width, height, SymbolicColor.NAVBAR_BG_TOP.ref(), + SymbolicColor.NAVBAR_BG_BOTTOM.ref()); + } + + if (state != State.CLOSED) { + enableScissor(x, y, width, height); + + poseStack.pushPose(); + poseStack.translate(x, y - scrollOffset, 0); + + // Draw a backdrop on the hovered row before starting batch rendering + var hoveredRow = pickRow(mouseX, mouseY); + if (hoveredRow != null) { + renderContext.fillRect(hoveredRow.getBounds(), SymbolicColor.NAVBAR_ROW_HOVER.ref()); + } + + // Render Text in batch + var buffers = renderContext.beginBatch(); + for (var row : rows) { + if (!row.isVisible(viewport)) { + continue; // Cull this row, it's not in the viewport + } + row.paragraph.renderBatch(renderContext, buffers); + } + renderContext.endBatch(buffers); + + // Render decorations, icons, etc. + for (var row : rows) { + if (!row.isVisible(viewport)) { + continue; // Cull this row, it's not in the viewport + } + if (row.hasChildren) { + float x = row.getBounds().x(); + x += 5; + float y = row.getBounds().y(); + y += 2f; + Vec2 p1, p2, p3; + if (row.expanded) { + // Triangle points down + p1 = new Vec2(x + 5, y); + p2 = new Vec2(x, y); + p3 = new Vec2(x + 2.5f, y + 5); + } else { + // Triangle points right + p1 = new Vec2(x + 5, y + 2.5f); + p2 = new Vec2(x, y); + p3 = new Vec2(x, y + 5); + } + + var color = row == hoveredRow ? SymbolicColor.LINK : SymbolicColor.BODY_TEXT; + renderContext.fillTriangle(p1, p2, p3, color.ref()); + } + + var icon = row.node.icon(); + if (!icon.isEmpty()) { + renderContext.renderItem(icon, row.paragraph.getBounds().x() - 9, row.paragraph.getBounds().y(), 1, + 8, + 8); + } + } + + poseStack.popPose(); + + disableScissor(); + } + } + + @Nullable + private Row pickRow(double x, double y) { + var vpPos = getViewportPoint(x, y); + if (vpPos != null) { + for (var row : rows) { + if (row.isVisible() && vpPos.getY() >= row.top && vpPos.getY() < row.bottom) { + return row; + } + } + } + return null; + } + + private void updateMousePos(double x, double y) { + var vpPos = getViewportPoint(x, y); + for (Row row : rows) { + if (!row.isVisible()) { + continue; + } + + if (vpPos != null && row.contains(vpPos.getX(), vpPos.getY())) { + row.paragraph.onMouseEnter(row.span); + } else { + row.paragraph.onMouseLeave(); + } + } + } + + private void recreateRows() { + this.navTree = GuideManager.INSTANCE.getNavigationTree(); + // Save Freeze expanded / scroll position + this.rows.clear(); + + for (var rootNode : this.navTree.getRootNodes()) { + var row = new Row(rootNode, null); + rows.add(row); + + // Add second level + for (var child : row.node.children()) { + row.hasChildren = true; + var childRow = new Row(child, row); + rows.add(childRow); + } + } + + updateLayout(); + } + + private void updateLayout() { + var context = new LayoutContext(new MinecraftFontMetrics(), new LytRect(0, 0, WIDTH_OPEN, height)); + + var currentY = 0; + for (var row : this.rows) { + if (!row.isVisible()) { + continue; + } + + // Child-Rows should be indented and their parents + // need an indent too for the expand/collapse indicator + int indent; + if (row.hasChildren) { + indent = PARENT_ROW_INDENT; + } else if (row.parent != null) { + indent = CHILD_ROW_INDENT; + } else { + indent = 0; + } + + if (!row.node.icon().isEmpty()) { + indent += 8; // Indent for icon; + } + + var x = indent; + var width = WIDTH_OPEN - indent; + var bounds = row.paragraph.layout(context, x, currentY, width); + row.top = bounds.y(); + row.bottom = bounds.bottom(); + currentY = bounds.bottom(); + } + } + + /** + * Gets a point in the coordinate space of the scrollable viewport. + */ + @Nullable + private Point getViewportPoint(double screenX, double screenY) { + if (state != State.OPENING && state != State.OPEN) { + return null; + } + + if (screenX >= x && screenX < x + width + && screenY >= y && screenY < y + height) { + var vpX = (int) Math.round(screenX - x); + var vpY = (int) Math.round(screenY + scrollOffset - y); + return new Point(vpX, vpY); + } + + return null; // Outside the viewport + } + + private class Row { + private final NavigationNode node; + private final LytParagraph paragraph = new LytParagraph(); + public final LytFlowSpan span; + private boolean expanded; + private final Row parent; + private boolean hasChildren; + public int top; + public int bottom; + + public Row(NavigationNode node, Row parent) { + this.node = node; + this.parent = parent; + + span = new LytFlowSpan(); + span.appendText(node.title()); + span.modifyHoverStyle(style -> style.color(SymbolicColor.LINK.ref())); + this.paragraph.setPaddingLeft(5); + this.paragraph.append(span); + } + + public LytRect getBounds() { + return new LytRect(0, top, width, bottom - top); + } + + public boolean contains(int x, int y) { + return x >= 0 && x < width && y >= top && y < bottom; + } + + public boolean isVisible() { + return parent == null || parent.expanded; + } + + // Is this row visible in the given viewport? + public boolean isVisible(LytRect viewport) { + return isVisible() && bottom > viewport.y() && top < viewport.bottom(); + } + } + + enum State { + CLOSED, + OPENING, + OPEN, + CLOSING + } +} diff --git a/src/main/java/appeng/client/guidebook/screen/GuideScreen.java b/src/main/java/appeng/client/guidebook/screen/GuideScreen.java new file mode 100644 index 00000000000..8ec5018101c --- /dev/null +++ b/src/main/java/appeng/client/guidebook/screen/GuideScreen.java @@ -0,0 +1,538 @@ +package appeng.client.guidebook.screen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import appeng.client.Point; +import appeng.client.gui.DashPattern; +import appeng.client.gui.DashedRectangle; +import appeng.client.guidebook.GuideManager; +import appeng.client.guidebook.GuidePage; +import appeng.client.guidebook.PageAnchor; +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.block.LytBlock; +import appeng.client.guidebook.document.block.LytDocument; +import appeng.client.guidebook.document.block.LytHeading; +import appeng.client.guidebook.document.block.LytParagraph; +import appeng.client.guidebook.document.flow.LytFlowContainer; +import appeng.client.guidebook.document.interaction.GuideTooltip; +import appeng.client.guidebook.document.interaction.InteractiveElement; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.layout.MinecraftFontMetrics; +import appeng.client.guidebook.render.ColorRef; +import appeng.client.guidebook.render.GuidePageTexture; +import appeng.client.guidebook.render.LightDarkMode; +import appeng.client.guidebook.render.SimpleRenderContext; +import appeng.core.AppEng; + +public class GuideScreen extends Screen { + private static final Logger LOGGER = LoggerFactory.getLogger(GuideScreen.class); + + private static final int HISTORY_SIZE = 100; + private static final DashPattern DEBUG_NODE_OUTLINE = new DashPattern(1f, 4, 3, 0xFFFFFFFF, 500); + private static final DashPattern DEBUG_CONTENT_OUTLINE = new DashPattern(0.5f, 2, 1, 0x7FFFFFFF, 500); + + private GuidePage currentPage; + private final GuideScrollbar scrollbar; + private GuideNavBar navbar; + + private static final List history = new ArrayList<>(); + private static int historyPosition; + + private Button backButton; + private Button forwardButton; + + private GuideScreen(PageAnchor anchor) { + super(Component.literal("AE2 Guidebook")); + this.scrollbar = new GuideScrollbar(); + loadPage(anchor); + } + + /** + * Opens and resets history. + */ + public static GuideScreen openNew(PageAnchor anchor) { + // Append to history if it's not already appended + if (history.lastIndexOf(anchor) != history.size()) { + historyPosition = history.size(); + history.add(anchor); + } + + return new GuideScreen(anchor); + } + + /** + * Opens at current history position and only falls back to the index if the history is empty. + */ + public static GuideScreen openAtPreviousPage(PageAnchor anchor) { + if (historyPosition < history.size()) { + anchor = history.get(historyPosition); + } else { + return openNew(anchor); + } + + return new GuideScreen(anchor); + } + + @Override + protected void init() { + super.init(); + + updatePageLayout(); + + // Add and re-position scrollbar + var docRect = getDocumentRect(); + addRenderableWidget(scrollbar); + scrollbar.move( + docRect.right(), + docRect.y(), + docRect.height()); + + this.navbar = new GuideNavBar(this); + addRenderableWidget(this.navbar); + + backButton = new Button(docRect.right() - 40, docRect.y() - 15, 20, 15, Component.literal("<"), + button -> navigateBack()); + addRenderableWidget(backButton); + forwardButton = new Button(docRect.right() - 20, docRect.y() - 15, 20, 15, Component.literal(">"), + button -> navigateForward()); + addRenderableWidget(forwardButton); + updateNavigationButtons(); + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + updateNavigationButtons(); + + renderBackground(poseStack); + + // Set scissor rectangle to rect that we show the document in + var documentRect = getDocumentRect(); + + fill(poseStack, documentRect.x(), documentRect.y(), documentRect.right(), documentRect.bottom(), 0x80333333); + + // Move rendering to anchor @ 0,0 in the document rect + var documentViewport = getDocumentViewport(); + poseStack.pushPose(); + poseStack.translate(documentRect.x() - documentViewport.x(), documentRect.y() - documentViewport.y(), 0); + + var document = currentPage.getDocument(); + var context = new SimpleRenderContext(this, + documentViewport, + poseStack, + LightDarkMode.LIGHT_MODE); + + enableScissor(documentRect.x(), documentRect.y(), documentRect.right(), documentRect.bottom()); + + // Render all text content in one large batch to improve performance + var buffers = context.beginBatch(); + document.renderBatch(context, buffers); + context.endBatch(buffers); + + document.render(context); + + disableScissor(); + + // renderHoverOutline(document, context); + + poseStack.popPose(); + + poseStack.pushPose(); + poseStack.translate(0, 0, 100); + + super.render(poseStack, mouseX, mouseY, partialTick); + + poseStack.popPose(); + + // Render tooltip + if (document.getHoveredElement() != null) { + renderTooltip(poseStack, mouseX, mouseY); + } + + } + + private void renderTooltip(PoseStack poseStack, int x, int y) { + var docPos = getDocumentPoint(x, y); + if (docPos == null) { + return; + } + + var tooltip = dispatchInteraction(docPos.getX(), docPos.getY(), InteractiveElement::getTooltip) + .orElse(null); + if (tooltip != null) { + renderTooltip(poseStack, tooltip, x, y); + } + } + + private static void renderHoverOutline(LytDocument document, SimpleRenderContext context) { + var hoveredElement = document.getHoveredElement(); + if (hoveredElement != null) { + // Fill a rectangle highlighting margins + if (hoveredElement.node() instanceof LytBlock block) { + var bounds = block.getBounds(); + if (block.getMarginTop() > 0) { + context.fillRect( + bounds.withHeight(block.getMarginTop()).move(0, -block.getMarginTop()), + new ColorRef(0x7FFFFF00)); + } + if (block.getMarginBottom() > 0) { + context.fillRect( + bounds.withHeight(block.getMarginBottom()).move(0, bounds.height()), + new ColorRef(0x7FFFFF00)); + } + if (block.getMarginLeft() > 0) { + context.fillRect( + bounds.withWidth(block.getMarginLeft()).move(-block.getMarginLeft(), 0), + new ColorRef(0x7FFFFF00)); + } + if (block.getMarginRight() > 0) { + context.fillRect( + bounds.withWidth(block.getMarginRight()).move(bounds.width(), 0), + new ColorRef(0x7FFFFF00)); + } + } + + // Fill the content rectangle + DashedRectangle.render(context.poseStack(), hoveredElement.node().getBounds(), DEBUG_NODE_OUTLINE, 0); + + // Also outline any inline-elements in the block + if (hoveredElement.content() != null) { + if (hoveredElement.node() instanceof LytFlowContainer flowContainer) { + flowContainer.enumerateContentBounds(hoveredElement.content()) + .forEach(bound -> { + DashedRectangle.render(context.poseStack(), bound, DEBUG_CONTENT_OUTLINE, 0); + }); + } + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)) { + return true; + } + + var docPoint = getDocumentPoint(mouseX, mouseY); + if (docPoint != null) { + if (button == 3) { + navigateBack(); + } else if (button == 4) { + navigateForward(); + } + + return dispatchEvent(docPoint.getX(), docPoint.getY(), el -> { + return el.mouseClicked(this, docPoint.getX(), docPoint.getY(), button); + }); + } else { + return false; + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (super.mouseReleased(mouseX, mouseY, button)) { + return true; + } + + var docPoint = getDocumentPoint(mouseX, mouseY); + if (docPoint != null) { + return dispatchEvent(docPoint.getX(), docPoint.getY(), el -> { + return el.mouseReleased(this, docPoint.getX(), docPoint.getY(), button); + }); + } else { + return false; + } + } + + public void navigateTo(ResourceLocation pageId) { + navigateTo(new PageAnchor(pageId, null)); + } + + public void navigateTo(PageAnchor anchor) { + if (currentPage.getId().equals(anchor.pageId())) { + // TODO -> scroll up (?) + return; + } + + loadPage(anchor); + + // Remove anything from the history after the current page when we navigate to a new one + if (historyPosition + 1 < history.size()) { + history.subList(historyPosition + 1, history.size()).clear(); + } + // Clamp history length + if (history.size() >= HISTORY_SIZE) { + history.subList(0, history.size() - HISTORY_SIZE).clear(); + } + // Append to history + historyPosition = history.size(); + history.add(anchor); + } + + // Navigate to next page in history (only possible if we've navigated back previously) + private void navigateForward() { + if (historyPosition + 1 < history.size()) { + loadPage(history.get(++historyPosition)); + } + } + + // Navigate to previous page in history + private void navigateBack() { + if (historyPosition > 0) { + loadPage(history.get(--historyPosition)); + } + } + + private void loadPage(PageAnchor anchor) { + GuidePageTexture.releaseUsedTextures(); + currentPage = GuideManager.INSTANCE.getPage(anchor.pageId()); + + if (currentPage == null) { + // Build a "not found" page dynamically + currentPage = buildNotFoundPage(anchor); + } + + scrollbar.setScrollAmount(0); + updatePageLayout(); + + // TODO ANCHOR + } + + private GuidePage buildNotFoundPage(PageAnchor anchor) { + var document = new LytDocument(); + var title = new LytHeading(); + title.appendText("Page not Found"); + title.setDepth(1); + document.append(title); + var body = new LytParagraph(); + body.appendText("Page " + anchor.pageId() + " could not be found."); + document.append(body); + + return new GuidePage( + AppEng.MOD_ID, + anchor.pageId(), + document); + } + + @Override + public void removed() { + super.removed(); + GuidePageTexture.releaseUsedTextures(); + } + + public void reloadPage() { + GuidePageTexture.releaseUsedTextures(); + currentPage = GuideManager.INSTANCE.getPage(currentPage.getId()); + updatePageLayout(); + } + + @FunctionalInterface + interface EventInvoker { + boolean invoke(InteractiveElement el); + } + + private boolean dispatchEvent(int x, int y, EventInvoker invoker) { + return dispatchInteraction(x, y, el -> { + if (invoker.invoke(el)) { + return Optional.of(true); + } else { + return Optional.empty(); + } + }).orElse(false); + } + + private Optional dispatchInteraction(int x, int y, Function> invoker) { + var underCursor = currentPage.getDocument().pick(x, y); + if (underCursor != null) { + // Iterate through content ancestors + for (var el = underCursor.content(); el != null; el = el.getFlowParent()) { + if (el instanceof InteractiveElement interactiveEl) { + var result = invoker.apply(interactiveEl); + if (result.isPresent()) { + return result; + } + } + } + + // Iterate through node ancestors + for (var node = underCursor.node(); node != null; node = node.getParent()) { + if (node instanceof InteractiveElement interactiveEl) { + var result = invoker.apply(interactiveEl); + if (result.isPresent()) { + return result; + } + } + } + } + + return Optional.empty(); + } + + @Override + public void afterMouseMove() { + super.afterMouseMove(); + + var document = currentPage.getDocument(); + + var mouseHandler = minecraft.mouseHandler; + var scale = (double) minecraft.getWindow().getGuiScaledWidth() + / (double) minecraft.getWindow().getScreenWidth(); + var x = mouseHandler.xpos() * scale; + var y = mouseHandler.ypos() * scale; + + // If there's a widget under the cursor, ignore document hit-testing + if (getChildAt(x, y).isPresent()) { + document.setHoveredElement(null); + return; + } + + var docPoint = getDocumentPoint(x, y); + if (docPoint != null) { + var hoveredEl = document.pick(docPoint.getX(), docPoint.getY()); + document.setHoveredElement(hoveredEl); + } else { + document.setHoveredElement(null); + } + } + + @Nullable + private Point getDocumentPoint(double screenX, double screenY) { + var documentRect = getDocumentRect(); + + if (screenX >= documentRect.x() && screenX < documentRect.right() + && screenY >= documentRect.y() && screenY < documentRect.bottom()) { + var docX = (int) Math.round(screenX - documentRect.x()); + var docY = (int) Math.round(screenY + scrollbar.getScrollAmount() - documentRect.y()); + return new Point(docX, docY); + } + + return null; // Outside the document + } + + private LytRect getDocumentRect() { + // 20 virtual px margin + var margin = 20; + + return new LytRect(margin, margin, width - 2 * margin, height - 2 * margin); + } + + private LytRect getDocumentViewport() { + var documentRect = getDocumentRect(); + return new LytRect(0, scrollbar.getScrollAmount(), documentRect.width(), documentRect.height()); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (!super.mouseScrolled(mouseX, mouseY, delta)) { + return scrollbar.mouseScrolled(mouseX, mouseY, delta); + } + return true; + } + + private void renderTooltip(PoseStack poseStack, GuideTooltip tooltip, int mouseX, int mouseY) { + var minecraft = Minecraft.getInstance(); + var clientLines = tooltip.getLines(this); + + if (clientLines.isEmpty()) { + return; + } + + int frameWidth = 0; + int frameHeight = clientLines.size() == 1 ? -2 : 0; + + for (var clientTooltipComponent : clientLines) { + frameWidth = Math.max(frameWidth, clientTooltipComponent.getWidth(minecraft.font)); + frameHeight += clientTooltipComponent.getHeight(); + } + + if (!tooltip.getIcon().isEmpty()) { + frameWidth += 18; + frameHeight = Math.max(frameHeight, 18); + } + + int x = mouseX + 12; + int y = mouseY - 12; + if (x + frameWidth > this.width) { + x -= 28 + frameWidth; + } + + if (y + frameHeight + 6 > this.height) { + y = this.height - frameHeight - 6; + } + + int zOffset = 400; + + TooltipFrame.render(poseStack, x, y, frameWidth, frameHeight, zOffset); + + float prevZOffset = itemRenderer.blitOffset; + itemRenderer.blitOffset = zOffset; + + if (!tooltip.getIcon().isEmpty()) { + x += 18; + } + + var bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); + poseStack.pushPose(); + poseStack.translate(0.0, 0.0, zOffset); + int currentY = y; + + // Batch-render tooltip text first + for (int i = 0; i < clientLines.size(); ++i) { + var line = clientLines.get(i); + line.renderText(minecraft.font, x, currentY, poseStack.last().pose(), bufferSource); + currentY += line.getHeight() + (i == 0 ? 2 : 0); + } + + bufferSource.endBatch(); + poseStack.popPose(); + + // Then render tooltip decorations, items, etc. + currentY = y; + if (!tooltip.getIcon().isEmpty()) { + itemRenderer.renderGuiItem(tooltip.getIcon(), x - 18, y); + } + + for (int i = 0; i < clientLines.size(); ++i) { + var line = clientLines.get(i); + line.renderImage(minecraft.font, x, currentY, poseStack, this.itemRenderer, zOffset); + currentY += line.getHeight() + (i == 0 ? 2 : 0); + } + this.itemRenderer.blitOffset = prevZOffset; + } + + private void updatePageLayout() { + var docViewport = getDocumentViewport(); + var context = new LayoutContext(new MinecraftFontMetrics(), docViewport); + + // Build layout if needed + var document = currentPage.getDocument(); + document.updateLayout(context, docViewport.width()); + scrollbar.setContentHeight(document.getContentHeight()); + } + + public ResourceLocation getCurrentPageId() { + return currentPage.getId(); + } + + private void updateNavigationButtons() { + backButton.active = historyPosition > 0; + forwardButton.active = historyPosition + 1 < history.size(); + } +} diff --git a/src/main/java/appeng/client/guidebook/screen/GuideScrollbar.java b/src/main/java/appeng/client/guidebook/screen/GuideScrollbar.java new file mode 100644 index 00000000000..ab3f436fb8e --- /dev/null +++ b/src/main/java/appeng/client/guidebook/screen/GuideScrollbar.java @@ -0,0 +1,160 @@ +package appeng.client.guidebook.screen; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; + +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +public class GuideScrollbar extends AbstractWidget { + private static final int WIDTH = 8; + private int contentHeight; + private int scrollAmount; + private Double thumbHeldAt; + + public GuideScrollbar() { + super(0, 0, 0, 0, Component.empty()); + } + + @Override + public void updateNarration(NarrationElementOutput output) { + } + + protected int getMaxScrollAmount() { + return Math.max(0, contentHeight - (this.height - 4)); + } + + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTick) { + if (!visible) { + return; + } + + var maxScrollAmount = getMaxScrollAmount(); + if (maxScrollAmount <= 0) { + return; + } + + int thumbHeight = getThumbHeight(); + int left = x; + int right = x + 8; + int top = y + getThumbTop(); + int bottom = top + thumbHeight; + RenderSystem.setShader(GameRenderer::getPositionColorShader); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + bufferBuilder.vertex(left, bottom, 0.0).color(128, 128, 128, 255).endVertex(); + bufferBuilder.vertex(right, bottom, 0.0).color(128, 128, 128, 255).endVertex(); + bufferBuilder.vertex(right, top, 0.0).color(128, 128, 128, 255).endVertex(); + bufferBuilder.vertex(left, top, 0.0).color(128, 128, 128, 255).endVertex(); + bufferBuilder.vertex(left, bottom - 1, 0.0).color(192, 192, 192, 255).endVertex(); + bufferBuilder.vertex(right - 1, bottom - 1, 0.0).color(192, 192, 192, 255).endVertex(); + bufferBuilder.vertex(right - 1, top, 0.0).color(192, 192, 192, 255).endVertex(); + bufferBuilder.vertex(left, top, 0.0).color(192, 192, 192, 255).endVertex(); + tesselator.end(); + } + + /** + * The thumb is the draggable rectangle representing the current viewport being manipulated by the scrollbar. + */ + private int getThumbTop() { + if (getMaxScrollAmount() == 0) { + return 0; + } + return Math.max(0, scrollAmount * (height - getThumbHeight()) / getMaxScrollAmount()); + } + + private int getThumbHeight() { + if (contentHeight <= 0) { + return 0; + } + return Mth.clamp((int) ((float) (this.height * this.height) / (float) contentHeight), 32, this.height); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.visible || button != 0) { + return false; + } + + var thumbTop = y + getThumbTop(); + var thumbBottom = thumbTop + getThumbHeight(); + + boolean thumbHit = mouseX >= x + && mouseX <= x + WIDTH + && mouseY >= thumbTop + && mouseY < thumbBottom; + if (thumbHit) { + this.thumbHeldAt = mouseY - thumbTop; + return true; + } else { + this.thumbHeldAt = null; + return false; + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button != 0) { + return super.mouseReleased(mouseX, mouseY, button); + } + + this.thumbHeldAt = null; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (this.visible && this.thumbHeldAt != null) { + + var thumbY = (int) Math.round(mouseY - y - thumbHeldAt); + var maxThumbY = height - getThumbHeight(); + var scrollAmount = (int) Math.round(thumbY / (double) maxThumbY * getMaxScrollAmount()); + setScrollAmount(scrollAmount); + + return true; + } else { + return false; + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (this.visible) { + this.setScrollAmount((int) (this.scrollAmount - delta * 10)); + return true; + } else { + return false; + } + } + + public void move(int x, int y, int height) { + this.x = x; + this.y = y; + this.width = WIDTH; + this.height = height; + } + + public void setContentHeight(int contentHeight) { + this.contentHeight = contentHeight; + if (this.scrollAmount > getMaxScrollAmount()) { + this.scrollAmount = getMaxScrollAmount(); + } + } + + public int getScrollAmount() { + return scrollAmount; + } + + public void setScrollAmount(int scrollAmount) { + this.scrollAmount = Mth.clamp(scrollAmount, 0, getMaxScrollAmount()); + } +} diff --git a/src/main/java/appeng/client/guidebook/screen/TooltipFrame.java b/src/main/java/appeng/client/guidebook/screen/TooltipFrame.java new file mode 100644 index 00000000000..ba7a3d0e527 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/screen/TooltipFrame.java @@ -0,0 +1,50 @@ +package appeng.client.guidebook.screen; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.math.Matrix4f; + +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.renderer.GameRenderer; + +final class TooltipFrame extends GuiComponent { + private TooltipFrame() { + } + + public static void render(PoseStack poseStack, int x, int y, int totalWidth, int totalHeight, int z) { + int o = 0xf0100010; + int p = 0x505000ff; + int q = 0x5028007f; + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + Matrix4f matrix4f = poseStack.last().pose(); + fillGradient(matrix4f, bufferBuilder, x - 3, y - 4, x + totalWidth + 3, y - 3, z, o, o); + fillGradient(matrix4f, bufferBuilder, x - 3, y + totalHeight + 3, x + totalWidth + 3, y + totalHeight + 4, z, o, + o); + fillGradient(matrix4f, bufferBuilder, x - 3, y - 3, x + totalWidth + 3, y + totalHeight + 3, z, o, o); + fillGradient(matrix4f, bufferBuilder, x - 4, y - 3, x - 3, y + totalHeight + 3, z, o, o); + fillGradient(matrix4f, bufferBuilder, x + totalWidth + 3, y - 3, x + totalWidth + 4, y + totalHeight + 3, z, o, + o); + fillGradient(matrix4f, bufferBuilder, x - 3, y - 3 + 1, x - 3 + 1, y + totalHeight + 3 - 1, z, p, q); + fillGradient(matrix4f, bufferBuilder, x + totalWidth + 2, y - 3 + 1, x + totalWidth + 3, + y + totalHeight + 3 - 1, z, p, q); + fillGradient(matrix4f, bufferBuilder, x - 3, y - 3, x + totalWidth + 3, y - 3 + 1, z, p, p); + fillGradient(matrix4f, bufferBuilder, x - 3, y + totalHeight + 2, x + totalWidth + 3, y + totalHeight + 3, z, q, + q); + + RenderSystem.enableDepthTest(); + RenderSystem.disableTexture(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + BufferUploader.drawWithShader(bufferBuilder.end()); + RenderSystem.disableBlend(); + RenderSystem.enableTexture(); + } +} diff --git a/src/main/java/appeng/client/guidebook/style/ResolvedTextStyle.java b/src/main/java/appeng/client/guidebook/style/ResolvedTextStyle.java new file mode 100644 index 00000000000..250fcec70fb --- /dev/null +++ b/src/main/java/appeng/client/guidebook/style/ResolvedTextStyle.java @@ -0,0 +1,18 @@ +package appeng.client.guidebook.style; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.render.ColorRef; + +public record ResolvedTextStyle( + float fontScale, + boolean bold, + boolean italic, + boolean underlined, + boolean strikethrough, + boolean obfuscated, + ResourceLocation font, + ColorRef color, + WhiteSpaceMode whiteSpace, + TextAlignment alignment) { +} diff --git a/src/main/java/appeng/client/guidebook/style/Styleable.java b/src/main/java/appeng/client/guidebook/style/Styleable.java new file mode 100644 index 00000000000..0fd6d98ebda --- /dev/null +++ b/src/main/java/appeng/client/guidebook/style/Styleable.java @@ -0,0 +1,54 @@ +package appeng.client.guidebook.style; + +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import appeng.client.guidebook.document.DefaultStyles; + +public interface Styleable { + TextStyle getStyle(); + + void setStyle(TextStyle style); + + TextStyle getHoverStyle(); + + void setHoverStyle(TextStyle style); + + @Nullable + Styleable getStylingParent(); + + default void modifyStyle(Consumer customizer) { + var builder = getStyle().toBuilder(); + customizer.accept(builder); + setStyle(builder.build()); + } + + default void modifyHoverStyle(Consumer customizer) { + var builder = getHoverStyle().toBuilder(); + customizer.accept(builder); + var hoverStyle = builder.build(); + if (hoverStyle.whiteSpace() != null) { + throw new IllegalStateException("Hover-Style may not override layout properties"); + } + setHoverStyle(hoverStyle); + } + + default ResolvedTextStyle resolveStyle() { + var stylingParent = getStylingParent(); + if (stylingParent != null) { + return getStyle().mergeWith(stylingParent.resolveStyle()); + } + + return getStyle().mergeWith(DefaultStyles.BASE_STYLE); + } + + default ResolvedTextStyle resolveHoverStyle(ResolvedTextStyle baseStyle) { + var stylingParent = getStylingParent(); + if (stylingParent != null) { + return getHoverStyle().mergeWith(stylingParent.resolveHoverStyle(baseStyle)); + } + + return getHoverStyle().mergeWith(baseStyle); + } +} diff --git a/src/main/java/appeng/client/guidebook/style/TextAlignment.java b/src/main/java/appeng/client/guidebook/style/TextAlignment.java new file mode 100644 index 00000000000..8fee604300c --- /dev/null +++ b/src/main/java/appeng/client/guidebook/style/TextAlignment.java @@ -0,0 +1,7 @@ +package appeng.client.guidebook.style; + +public enum TextAlignment { + LEFT, + CENTER, + RIGHT +} diff --git a/src/main/java/appeng/client/guidebook/style/TextStyle.java b/src/main/java/appeng/client/guidebook/style/TextStyle.java new file mode 100644 index 00000000000..23f98b01627 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/style/TextStyle.java @@ -0,0 +1,141 @@ +package appeng.client.guidebook.style; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.resources.ResourceLocation; + +import appeng.client.guidebook.render.ColorRef; + +public record TextStyle( + @Nullable Float fontScale, + @Nullable Boolean bold, + @Nullable Boolean italic, + @Nullable Boolean underlined, + @Nullable Boolean strikethrough, + @Nullable Boolean obfuscated, + @Nullable ResourceLocation font, + @Nullable ColorRef color, + @Nullable WhiteSpaceMode whiteSpace, + @Nullable TextAlignment alignment) { + + public static final TextStyle EMPTY = new TextStyle(null, null, null, null, null, null, null, null, null, null); + + public ResolvedTextStyle mergeWith(ResolvedTextStyle base) { + var fontScale = this.fontScale != null ? this.fontScale : base.fontScale(); + var bold = this.bold != null ? this.bold : base.bold(); + var italic = this.italic != null ? this.italic : base.italic(); + var underlined = this.underlined != null ? this.underlined : base.underlined(); + var strikethrough = this.strikethrough != null ? this.strikethrough : base.strikethrough(); + var obfuscated = this.obfuscated != null ? this.obfuscated : base.obfuscated(); + var font = this.font != null ? this.font : base.font(); + var color = this.color != null ? this.color : base.color(); + var whiteSpace = this.whiteSpace != null ? this.whiteSpace : base.whiteSpace(); + var alignment = this.alignment != null ? this.alignment : base.alignment(); + return new ResolvedTextStyle( + fontScale, + bold, + italic, + underlined, + strikethrough, + obfuscated, + font, + color, + whiteSpace, + alignment); + } + + public Builder toBuilder() { + var builder = new Builder(); + builder.fontScale = fontScale; + builder.bold = bold; + builder.italic = italic; + builder.underlined = underlined; + builder.strikethrough = strikethrough; + builder.obfuscated = obfuscated; + builder.font = font; + builder.color = color; + builder.whiteSpace = whiteSpace; + builder.alignment = alignment; + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Float fontScale; + private Boolean bold; + private Boolean italic; + private Boolean underlined; + private Boolean strikethrough; + private Boolean obfuscated; + private ResourceLocation font; + private ColorRef color; + private WhiteSpaceMode whiteSpace; + private TextAlignment alignment; + + public Builder fontScale(Float fontScale) { + this.fontScale = fontScale; + return this; + } + + public Builder bold(Boolean bold) { + this.bold = bold; + return this; + } + + public Builder italic(Boolean italic) { + this.italic = italic; + return this; + } + + public Builder underlined(Boolean underlined) { + this.underlined = underlined; + return this; + } + + public Builder strikethrough(Boolean strikethrough) { + this.strikethrough = strikethrough; + return this; + } + + public Builder obfuscated(Boolean obfuscated) { + this.obfuscated = obfuscated; + return this; + } + + public Builder font(ResourceLocation font) { + this.font = font; + return this; + } + + public Builder color(ColorRef color) { + this.color = color; + return this; + } + + public Builder whiteSpace(WhiteSpaceMode whiteSpace) { + this.whiteSpace = whiteSpace; + return this; + } + + public Builder alignment(TextAlignment alignment) { + this.alignment = alignment; + return this; + } + + public TextStyle build() { + return new TextStyle(fontScale, + bold, + italic, + underlined, + strikethrough, + obfuscated, + font, + color, + whiteSpace, + alignment); + } + } +} diff --git a/src/main/java/appeng/client/guidebook/style/WhiteSpaceMode.java b/src/main/java/appeng/client/guidebook/style/WhiteSpaceMode.java new file mode 100644 index 00000000000..17962245ca2 --- /dev/null +++ b/src/main/java/appeng/client/guidebook/style/WhiteSpaceMode.java @@ -0,0 +1,36 @@ +package appeng.client.guidebook.style; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/CSS/white-space + */ +public enum WhiteSpaceMode { + NORMAL(true, true), + NOWRAP(true, true), + PRE(false, false), + PRE_WRAP(false, false), + PRE_LINE(true, false), + BREAK_SPACES(false, false); + + /** + * Controls collapsing of white-space according to the CSS algo. + */ + private final boolean collapseWhitespace; + + /** + * Controls collapsing of segment breaks according to the CSS algo. + */ + private final boolean collapseSegmentBreaks; + + WhiteSpaceMode(boolean collapseWhitespace, boolean collapseSegmentBreaks) { + this.collapseWhitespace = collapseWhitespace; + this.collapseSegmentBreaks = collapseSegmentBreaks; + } + + public boolean isCollapseWhitespace() { + return collapseWhitespace; + } + + public boolean isCollapseSegmentBreaks() { + return collapseSegmentBreaks; + } +} diff --git a/src/main/java/appeng/core/AppEng.java b/src/main/java/appeng/core/AppEng.java index d1da9cae203..9769485a184 100644 --- a/src/main/java/appeng/core/AppEng.java +++ b/src/main/java/appeng/core/AppEng.java @@ -95,4 +95,10 @@ void spawnEffect(EffectType effect, Level level, double posX, double posY, * registers Hotkeys for {@link appeng.hotkeys.HotkeyActions} */ void registerHotkey(String id); + + /** + * Opens the guidebook (if this is a client) on the last opened page, or the given initial page. + */ + default void openGuide(ResourceLocation initialPage) { + } } diff --git a/src/main/java/appeng/core/AppEngClient.java b/src/main/java/appeng/core/AppEngClient.java index 0be55c6945c..c2b983a5929 100644 --- a/src/main/java/appeng/core/AppEngClient.java +++ b/src/main/java/appeng/core/AppEngClient.java @@ -25,6 +25,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; @@ -56,6 +59,9 @@ import appeng.client.Hotkeys; import appeng.client.gui.me.common.PinnedKeys; import appeng.client.gui.style.StyleManager; +import appeng.client.guidebook.GuideManager; +import appeng.client.guidebook.PageAnchor; +import appeng.client.guidebook.screen.GuideScreen; import appeng.client.render.StorageCellClientTooltipComponent; import appeng.client.render.effects.EnergyParticleData; import appeng.client.render.effects.ParticleTypes; @@ -94,6 +100,8 @@ */ @Environment(EnvType.CLIENT) public class AppEngClient extends AppEngBase { + private static final Logger LOGGER = LoggerFactory.getLogger(AppEngClient.class); + private static AppEngClient INSTANCE; /** @@ -118,6 +126,7 @@ public AppEngClient() { InitAutoRotatingModel.init(); BlockAttackHook.install(); RenderBlockOutlineHook.install(); + GuideManager.init(); ClientLifecycleEvents.CLIENT_STARTED.register(this::clientSetup); @@ -370,4 +379,14 @@ private ItemStack onPickBlock(Player player, HitResult hitResult) { } return ItemStack.EMPTY; } + + @Override + public void openGuide(ResourceLocation initialPage) { + try { + var screen = GuideScreen.openAtPreviousPage(PageAnchor.page(initialPage)); + Minecraft.getInstance().setScreen(screen); + } catch (Exception e) { + LOGGER.error("Failed to open guide.", e); + } + } } diff --git a/src/main/java/appeng/core/definitions/AEItems.java b/src/main/java/appeng/core/definitions/AEItems.java index ba344abeb5e..f46452ccb3e 100644 --- a/src/main/java/appeng/core/definitions/AEItems.java +++ b/src/main/java/appeng/core/definitions/AEItems.java @@ -60,6 +60,7 @@ import appeng.items.storage.StorageTier; import appeng.items.storage.ViewCellItem; import appeng.items.tools.BiometricCardItem; +import appeng.items.tools.GuideItem; import appeng.items.tools.MemoryCardItem; import appeng.items.tools.NetworkToolItem; import appeng.items.tools.fluix.FluixAxeItem; @@ -262,6 +263,8 @@ private static ItemDefinition makePortableFluidCell(ResourceLo public static final ItemDefinition SPATIAL_CELL16 = item("16³ Spatial Storage Cell", AEItemIds.SPATIAL_CELL_16, p -> new SpatialStorageCellItem(p.stacksTo(1), 16)); public static final ItemDefinition SPATIAL_CELL128 = item("128³ Spatial Storage Cell", AEItemIds.SPATIAL_CELL_128, p -> new SpatialStorageCellItem(p.stacksTo(1), 128)); + public static final ItemDefinition TABLET = item("Guide", AEItemIds.GUIDE, p -> new GuideItem(p.stacksTo(1))); + /// /// UNSUPPORTED DEV TOOLS /// diff --git a/src/main/java/appeng/datagen/providers/models/ItemModelProvider.java b/src/main/java/appeng/datagen/providers/models/ItemModelProvider.java index 115d70651d1..bce98b9c530 100644 --- a/src/main/java/appeng/datagen/providers/models/ItemModelProvider.java +++ b/src/main/java/appeng/datagen/providers/models/ItemModelProvider.java @@ -8,6 +8,7 @@ import net.minecraftforge.client.model.generators.ItemModelBuilder; import net.minecraftforge.common.data.ExistingFileHelper; +import appeng.api.ids.AEItemIds; import appeng.api.util.AEColor; import appeng.client.render.model.BiometricCardModel; import appeng.client.render.model.MemoryCardModel; @@ -127,6 +128,7 @@ protected void registerModels() { flatSingleLayer(AEItems.SPEED_CARD, "item/card_speed"); flatSingleLayer(AEItems.SMITHING_TABLE_PATTERN, "item/smithing_table_pattern"); flatSingleLayer(AEItems.STONECUTTING_PATTERN, "item/stonecutting_pattern"); + flatSingleLayer(AEItemIds.GUIDE, "item/guide"); flatSingleLayer(AEItems.VIEW_CELL, "item/view_cell"); flatSingleLayer(AEItems.WIRELESS_BOOSTER, "item/wireless_booster"); flatSingleLayer(AEItems.WIRELESS_CRAFTING_TERMINAL, "item/wireless_crafting_terminal"); diff --git a/src/main/java/appeng/integration/modules/rei/FluidBlockRenderer.java b/src/main/java/appeng/integration/modules/rei/FluidBlockRenderer.java index 8ee87ffbf44..171dbc410ea 100644 --- a/src/main/java/appeng/integration/modules/rei/FluidBlockRenderer.java +++ b/src/main/java/appeng/integration/modules/rei/FluidBlockRenderer.java @@ -2,19 +2,31 @@ import com.mojang.blaze3d.vertex.PoseStack; +import org.jetbrains.annotations.Nullable; + import dev.architectury.fluid.FluidStack; import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer; +import me.shedaniel.rei.api.client.gui.widgets.Tooltip; +import me.shedaniel.rei.api.client.gui.widgets.TooltipContext; import me.shedaniel.rei.api.common.entry.EntryStack; -import me.shedaniel.rei.plugin.client.entry.FluidEntryDefinition; +import appeng.api.client.AEStackRendering; +import appeng.api.stacks.AEFluidKey; import appeng.integration.modules.jeirei.FluidBlockRendering; -public class FluidBlockRenderer extends FluidEntryDefinition.FluidEntryRenderer { +public class FluidBlockRenderer implements EntryRenderer { @Override - public void render(EntryStack entry, PoseStack matrices, Rectangle rectangle, int mouseX, int mouseY, + public void render(EntryStack entry, PoseStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) { var fluid = entry.getValue().getFluid(); - FluidBlockRendering.render(matrices, fluid, rectangle.x, rectangle.y, rectangle.width, rectangle.height); + FluidBlockRendering.render(matrices, fluid, bounds.x, bounds.y, bounds.width, bounds.height); + } + + @Override + public @Nullable Tooltip getTooltip(EntryStack entry, TooltipContext context) { + var key = AEFluidKey.of(entry.getValue().getFluid(), entry.getValue().getTag()); + return Tooltip.create(context.getPoint(), AEStackRendering.getTooltip(key)); } } diff --git a/src/main/java/appeng/integration/modules/rei/TransformCategory.java b/src/main/java/appeng/integration/modules/rei/TransformCategory.java index 2049a4f802a..103f9c51b84 100644 --- a/src/main/java/appeng/integration/modules/rei/TransformCategory.java +++ b/src/main/java/appeng/integration/modules/rei/TransformCategory.java @@ -11,15 +11,14 @@ import dev.architectury.fluid.FluidStack; import me.shedaniel.math.Point; import me.shedaniel.math.Rectangle; -import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer; import me.shedaniel.rei.api.client.gui.Renderer; import me.shedaniel.rei.api.client.gui.widgets.Widget; import me.shedaniel.rei.api.client.gui.widgets.Widgets; import me.shedaniel.rei.api.client.registry.display.DisplayCategory; +import me.shedaniel.rei.api.client.util.ClientEntryStacks; import me.shedaniel.rei.api.common.category.CategoryIdentifier; import me.shedaniel.rei.api.common.entry.EntryStack; import me.shedaniel.rei.api.common.util.EntryStacks; -import me.shedaniel.rei.plugin.client.entry.FluidEntryDefinition; import appeng.core.AppEng; import appeng.core.definitions.AEBlocks; @@ -136,14 +135,15 @@ private Collection> getCatalystForRendering(TransformRec } } + /** + * Creates an entry stack that renders as a 3d block instead of a slot. + */ private static EntryStack makeCustomRenderingFluidEntry(Fluid fluid) { - return EntryStack.of(new FluidEntryDefinition() { - @Override - public EntryRenderer getRenderer() { - return new FluidBlockRenderer(); - } - }, FluidStack.create(fluid, FluidStack.bucketAmount())) - .setting(EntryStack.Settings.FLUID_AMOUNT_VISIBLE, false); + var fluidStack = EntryStacks.of(fluid); + ClientEntryStacks.setRenderer(fluidStack, entryStack -> { + return new FluidBlockRenderer(); + }); + return fluidStack; } @Override diff --git a/src/main/java/appeng/items/tools/GuideItem.java b/src/main/java/appeng/items/tools/GuideItem.java new file mode 100644 index 00000000000..fa990d67a7f --- /dev/null +++ b/src/main/java/appeng/items/tools/GuideItem.java @@ -0,0 +1,40 @@ +package appeng.items.tools; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +import appeng.core.AppEng; +import appeng.items.AEBaseItem; + +/** + * Shows the guidebook when used. + */ +public class GuideItem extends AEBaseItem { + private static final Logger LOGGER = LoggerFactory.getLogger(GuideItem.class); + + public GuideItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + var stack = player.getItemInHand(hand); + + if (level.isClientSide()) { + openGuide(); + } + + return new InteractionResultHolder<>(InteractionResult.FAIL, stack); + } + + private static void openGuide() { + AppEng.instance().openGuide(AppEng.makeId("index.md")); + } +} diff --git a/src/main/java/appeng/util/Platform.java b/src/main/java/appeng/util/Platform.java index 6e40ce9dff2..59db7b58966 100644 --- a/src/main/java/appeng/util/Platform.java +++ b/src/main/java/appeng/util/Platform.java @@ -33,6 +33,7 @@ import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; @@ -48,6 +49,7 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -104,6 +106,18 @@ public class Platform { private static final Class ponderLevelClass = findPonderLevelClass( "com.simibubi.create.foundation.ponder.PonderWorld"); + // This hack is used to allow tests and the guidebook to provide a recipe manager before the client loads a world + public static RecipeManager fallbackClientRecipeManager; + + public static RecipeManager getClientRecipeManager() { + var minecraft = Minecraft.getInstance(); + if (minecraft.level != null) { + return minecraft.level.getRecipeManager(); + } + + return fallbackClientRecipeManager; + } + private static Class findPonderLevelClass(String className) { if (!hasClientClasses()) { return null; // Don't attempt this on a dedicated server diff --git a/src/main/resources/NOTICE b/src/main/resources/NOTICE new file mode 100644 index 00000000000..0418dfc78e5 --- /dev/null +++ b/src/main/resources/NOTICE @@ -0,0 +1,401 @@ +The following attributions pertain to libraries used by Applied Energistics 2: + +For the original JavaScript versions of micromark, MDX, MdAst, Unist, on which +the Java port of Applied Energistics 2s markdown library is based: + +Sources: https://github.com/micromark/, https://github.com/syntax-tree/ + +------------------------------------------------------------------------------- +(The MIT License) + +Copyright (c) 2020 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------- + +For directory-watcher, which is used to provide hot-reload support for +the guidebook. + +Source: https://github.com/gmethvin/directory-watcher + +------------------------------------------------------------------------------- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +------------------------------------------------------------------------------- + +For snakeyaml, which is used to parse Markdown frontmatter, and potentially going forward for reading +configuration files. + +https://bitbucket.org/snakeyaml/snakeyaml/src/master/ + +------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +------------------------------------------------------------------------------- diff --git a/src/main/resources/ae2.accesswidener b/src/main/resources/ae2.accesswidener index b82a0980d9a..c8fdda373f9 100644 --- a/src/main/resources/ae2.accesswidener +++ b/src/main/resources/ae2.accesswidener @@ -8,6 +8,7 @@ accessible method net/minecraft/client/gui/components/EditBox isEditable ()Z accessible field net/minecraft/client/gui/components/AbstractWidget height I extendable method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen isHovering (Lnet/minecraft/world/inventory/Slot;DD)Z accessible method net/minecraft/client/gui/screens/inventory/AbstractContainerScreen findSlot (DD)Lnet/minecraft/world/inventory/Slot; +accessible method net/minecraft/client/gui/GuiComponent fillGradient (Lcom/mojang/blaze3d/vertex/PoseStack;IIIIIII)V # We need to change yPos of existing slots to resize the container mutable field net/minecraft/world/inventory/Slot x I @@ -93,3 +94,6 @@ accessible field net/minecraft/data/loot/BlockLoot HAS_NO_SILK_TOUCH Lnet/minecr accessible field net/minecraft/world/item/crafting/UpgradeRecipe base Lnet/minecraft/world/item/crafting/Ingredient; accessible field net/minecraft/world/item/crafting/UpgradeRecipe addition Lnet/minecraft/world/item/crafting/Ingredient; + +accessible method net/minecraft/client/gui/Font getFontSet (Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/gui/font/FontSet; +accessible field net/minecraft/client/gui/screens/LoadingOverlay reload Lnet/minecraft/server/packs/resources/ReloadInstance; diff --git a/src/main/resources/assets/ae2/ae2guide/gui/entropy_cool.png b/src/main/resources/assets/ae2/ae2guide/gui/entropy_cool.png new file mode 100644 index 00000000000..79cad52fda8 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/entropy_cool.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/entropy_heat.png b/src/main/resources/assets/ae2/ae2guide/gui/entropy_heat.png new file mode 100644 index 00000000000..b90ddfd559a Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/entropy_heat.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/inscriber_arrows_bg_light.png b/src/main/resources/assets/ae2/ae2guide/gui/inscriber_arrows_bg_light.png new file mode 100644 index 00000000000..63cfe54b778 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/inscriber_arrows_bg_light.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/large_slot_light.png b/src/main/resources/assets/ae2/ae2guide/gui/large_slot_light.png new file mode 100644 index 00000000000..27b3c724b75 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/large_slot_light.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_filled_light.png b/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_filled_light.png new file mode 100644 index 00000000000..033f794a3df Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_filled_light.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_light.png b/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_light.png new file mode 100644 index 00000000000..426f5bc1374 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/recipe_arrow_light.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/slot_cross.png b/src/main/resources/assets/ae2/ae2guide/gui/slot_cross.png new file mode 100644 index 00000000000..bb1fcb8d313 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/slot_cross.png differ diff --git a/src/main/resources/assets/ae2/ae2guide/gui/slot_light.png b/src/main/resources/assets/ae2/ae2guide/gui/slot_light.png new file mode 100644 index 00000000000..218df6e9ff9 Binary files /dev/null and b/src/main/resources/assets/ae2/ae2guide/gui/slot_light.png differ diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/concepts.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/concepts.json new file mode 100644 index 00000000000..2d7f1b370f2 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/concepts.json @@ -0,0 +1,6 @@ +{ + "name": "Concepts", + "icon": "minecraft:lectern", + "sortnum": 10, + "description": "In this section we discuss more abstract concepts that need to be explained." +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/getting_started.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/getting_started.json new file mode 100644 index 00000000000..b20f945b2de --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/categories/getting_started.json @@ -0,0 +1,6 @@ +{ + "name": "Getting Started", + "icon": "ae2:meteorite_compass", + "sortnum": 0, + "description": "This is a step-by-step guide on how to start $(thing)Applied Energistics 2$(). This will walk you through finding your first $(l:ae2:getting_started/compass)Meteor$(/l), growing $(l:ae2:getting_started/certus)Certus Quartz$(), and setting up an $(l:ae2:getting_started/inscriber)Inscriber$()." +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/concepts/power.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/concepts/power.json new file mode 100644 index 00000000000..a0a6384c9e5 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/concepts/power.json @@ -0,0 +1,63 @@ +{ + "name": "AE Power", + "icon": "ae2:energy_cell", + "category": "ae2:concepts", + "pages": [ + { + "type": "patchouli:text", + "text": "The $(thing)ME System$() uses a unique form of power measured in $(thing)AE$().$(br2)Some $(item)External Power Systems$() can be converted into $(thing)AE$(), but this conversion only works 1-way.$(br2)What $(item)External Power Systems$() are available depends on your particular modpack setup." + }, + { + "flag": "mod:fabric-api", + "type": "patchouli:text", + "title": "Fabric Conversion(s)", + "text": "Tech Reborn's \"$(thing)Energy$()\" converts into $(thing)AE$() with a default ratio of $(thing)$(bold)2 E = 1 AE$()." + }, + { + "flat": "mod:forge", + "type": "patchouli:text", + "title": "Forge Conversion(s)", + "text": "\"$(thing)Forge Energy$()\" ($(thing)FE$()), sometimes referred to as \"$(thing)Redstone Flux$()\" ($(thing)RF$()), or even some other names ($(thing)IF$(), $(thing)CF$(), $(thing)µI$(), etc.), is the most widely used power system. It converts into $(thing)AE$() with a default ratio of $(thing)$(bold)2 FE = 1 AE$()." + }, + { + "type": "patchouli:text", + "title": "Conversion Blocks", + "text": "The following machines accept $(thing)External Power$() directly for only themselves.$(li)$(l:ae2:getting_started/charger)Charger$(/l)$(li)$(l:ae2:getting_started/inscriber)Inscriber$(/l)$(li)$(l:ae2:todo)ME Chest$(/l)$(br2)The following blocks are able to accept $(thing)External Power$() and turn it into $(thing)AE$() for the entire Network.$(li)$(item)Energy Acceptor$()$(li)$(l:ae2:todo)ME Controller$(/l)" + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/energy_energy_acceptor", + "recipe2": "ae2:network/parts/energy_acceptor" + }, + { + "type": "patchouli:text", + "title": "Energy Storage", + "text": "$(thing)AE$() power is stored in the network. The following blocks increase how much power can be stored.$(li)Any ME Network: 800 $(thing)AE$()$(li)$(l:ae2:todo)ME Controller$(/l): 8,000 $(thing)AE$()$(li)$(item)Energy Cell$(): 200k $(thing)AE$()$(li)$(item)Dense Energy Cell$(): 1.6M $(thing)AE$()$(br2)A common beginner issue is a lack of $(thing)AE energy storage$(). If you encounter power problems (system blanking out or constantly restarting), you should add $(item)Energy Cells$()." + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/energy_energy_cell", + "recipe2": "ae2:network/blocks/energy_dense_energy_cell" + }, + { + "type": "patchouli:text", + "title": "Energy Transfer", + "text": "$(thing)AE Power$() is available across an entire $(thing)Network$(). It follows the same connectivity rules as $(l:ae2:concepts/channels)Channels$(/l), with one exception.$(br2)$(l:ae2:todo)Quartz Fiber$() is a cable multi-part piece which blocks $(thing)Channels$(), but allows $(thing)AE power$() to pass between $(thing)Networks$(). It is useful for $(l:ae2:todo)Sub-Networks$().$(br2)$(item)Energy Cells$() maintain their charge when broken." + }, + { + "type": "patchouli:text", + "title": "Energy Usage", + "text": "$(thing)AE Power$() is consumed by the network for various purposes, such as...$(li)For each block a Channel has to travel through (Controller Networks are more efficient than Ad-Hoc ones).$(li)Components having a passive power consumption.$(li)Components having an active power consumption to perform specific tasks.$(br2)The $(l:ae2:todo)Controller$(/l) or $(l:ae2:todo)Network Tool$(/l) can show you power stats." + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/energy_vibration_chamber", + "text": "If you don't have an $(thing)external power system$() available, then you can use the $(item)Vibration Chamber$() to generate $(thing)AE$() directly using $(thing)furnace fuels$(). With default settings, 1 $(item)Coal$() produces 8,000 $(thing)AE$() at 40 $(thing)AE$()/t." + }, + { + "type": "patchouli:text", + "title": "Extra Remarks", + "text": "It is worth pointing out that various $(thing)HUD Mods$() only show local power storage numbers, and will show 0 energy on things like $(l:ae2:getting_started/inscriber)Inscribers$() when they are powered by the $(thing)Network$(). The $(item)Inscriber$() will run just fine regardless.$(br2)While $(l:ae2:todo)Controllers$(/l) can accept $(thing)external power$(), it normally takes up one face (32 potential $(thing)channels$()) so you are typically better off still using an $(item)Energy Acceptor$() as your input." + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/certus.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/certus.json new file mode 100644 index 00000000000..b807549eced --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/certus.json @@ -0,0 +1,50 @@ +{ + "name": "4) Certus Quartz", + "category": "ae2:getting_started", + "icon": "ae2:quartz_cluster", + "sortnum": 40, + "pages": [ + { + "type": "patchouli:text", + "text": "$(thing)Certus Quartz$() is a wondrous material found in $(l:ae2:getting_started/compass)Meteorites$(/l). It is grown similarly to $(item)Amethyst$(), though most of its $(l:ae2:getting_started/certus#budding)Budding blocks$(/l) appear to decay over time.$(br2)It is used in many $(thing)AE2$() recipes as a crafting ingredient, can be used to make $(l:ae2:equipment/quartz_tools)Iron-level tools$(/l), crushed into a $(item)Dust$(), used to decorate, and more." + }, + { + "type": "patchouli:spotlight", + "item": "ae2:quartz_cluster", + "text": "Growing $(thing)Certus Quartz$() has 3 $(thing)Bud$() stages and then a fully grown $(thing)Cluster$() stage. Breaking it during any of the $(thing)Bud$() stages will drop only a single $(thing)Certus Quartz Dust$() (not affected by $(item)Fortune$()). Breaking a fully grown $(thing)Cluster$() will drop 4 $(thing)Certus Quartz Crystals$(), and this is affected by $(item)Fortune$()." + }, + { + "type": "patchouli:spotlight", + "anchor": "budding", + "item": "ae2:flawless_budding_quartz", + "title": "Budding Certus Quartz", + "text": "$(thing)Budding Certus Quartz$() comes in 4 tiers.$(br2)$(thing)Flawless$() is the best and never decays, but can only be found in Meteors.$(br2)The next 3 tiers, in descending quality, are $(thing)Flawed$(), $(thing)Chipped$(), and $(thing)Damaged$()." + }, + { + "type": "patchouli:text", + "text": "When one of those $(thing)Budding$() blocks grows a $(thing)Certus Quartz Bud$() there is a chance for it to decay to a lower tier, with $(thing)Damaged$() decaying into a normal $(thing)Certus Quartz Block$().$(br2)Conveniently, these decaying $(thing)Budding Blocks$() can be crafted by throwing one of them (or a $(thing)Certus Quartz Block$()), along with a $(l:ae2:getting_started/charger)Charged Certus Quartz$() into a pool of water to increase their tier by 1 (Max is $(thing)Flawed$())." + }, + { + "type": "patchouli:text", + "text": "When broken normally any $(thing)Budding Certus Quartz$() will turn into a normal $(thing)Certus Quartz Block$(). If it is broken using $(item)Silk Touch$() then the $(thing)Budding Block$() will decay one tier. If you want $(thing)Flawless Budding Quartz$() at your base then you will need to figure out another way of moving it." + }, + { + "type": "patchouli:crafting", + "anchor": "cga", + "recipe": "ae2:network/blocks/crystal_processing_quartz_growth_accelerator", + "text": "$(thing)Crystal Growth Accelerators$() significantly increase the speed at which $(thing)Certus Quartz$() grows. They also work on Amethyst. $(thing)CGAs$() are placed adjacent to the root $(l:ae2:getting_started/certus#budding)Budding Block$()." + }, + { + "type": "patchouli:text", + "text": "$(thing)CGAs$() need power to operate. They accept either $(l:ae2:concepts/power)Network Power$(/l) or the $(l:ae2:getting_started/crank)Wooden Crank$(/l). They do not accept $(item)External Power$(). Power only connects to two opposing sides of the $(thing)CGA$().$(br2)$(thing)CGAs$() can be $(l:ae2:equipment/wrench)Rotated$(/l). This only affects where power can connect; adjacent $(thing)Budding Blocks$() will be $(thing)Growth Accelerated$() from any side of the $(thing)CGA$(). Also, multiple $(thing)CGAs$() can accelerate a $(thing)Budding Block$() at the same time." + }, + { + "type": "patchouli:image", + "images": [ + "ae2:textures/patchouli/images/crystal_growth_accelerator_setup.png" + ], + "border": true, + "title": "Example Setup" + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/charger.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/charger.json new file mode 100644 index 00000000000..c2d47080670 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/charger.json @@ -0,0 +1,17 @@ +{ + "name": "1) Charger", + "category": "ae2:getting_started", + "icon": "ae2:charger", + "sortnum": 10, + "pages": [ + { + "type": "patchouli:text", + "text": "The $(thing)Charger$() is used to charge $(l:ae2:catalog/chargeable)various AE2 items$(/l), and for some crafting. Notably it is used to craft the $(l:ae2:getting_started/compass)Meteorite Compass$(/l) and $(thing)Charged Certus Quartz$().$(br2)The $(thing)Charger$() accepts $(l:ae2:concepts/power)Network Power, most external forms of power$(/l), or can be powered manually using a $(l:ae2:getting_started/crank)Wooden Crank$(/l)." + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/crystal_processing_charger", + "text": "It only accepts $(thing)power$() from its \"top\" or \"bottom\" sides. Note that it can be $(l:ae2:equipment/wrench)rotated$(/l).$(br2)You can [$(k:use)] held items into or out of it, or use automation like $(item)Hoppers$(), $(item)Pipes$(), $(l:ae2:examples/subnets/pipe)Sub-Networks$(), etc." + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/compass.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/compass.json new file mode 100644 index 00000000000..43fa1744e62 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/compass.json @@ -0,0 +1,21 @@ +{ + "name": "2) Meteors & Compass", + "category": "ae2:getting_started", + "icon": "ae2:meteorite_compass", + "sortnum": 20, + "extra_recipe_mappings": { + "ae2:mysterious_cube": 0 + }, + "pages": [ + { + "type": "patchouli:text", + "text": "$(thing)Meteorites$() can be found scattered across the surface of the Overworld. They contain some $(l:ae2:getting_started/certus)Certus Quartz$() and a $(thing)Mysterious Cube$() at the center. They are also encased in a large amount of $(item)Sky Stone$().$(br2)The $(thing)Mysterious Cube$() will drop all 4 $(l:ae2:getting_started/inscriber)Inscriber Plates$(/l) when broken." + }, + { + "type": "patchouli:spotlight", + "item": "ae2:meteorite_compass", + "link_recipe": true, + "text": "The $(thing)Meteorite Compass$() is crafted by placing a normal Compass in a $(l:ae2:getting_started/charger)Charger$(/l).$(br2)This $(thing)Compass$() points towards the nearest Chunk containing a $(thing)Mysterious Cube$() (and thus a Meteorite) it can find.$(br2)Slow Spin = Can't find any $(thing)Mysterious Cube$() in range." + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/crank.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/crank.json new file mode 100644 index 00000000000..ee3681bd0e5 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/crank.json @@ -0,0 +1,17 @@ +{ + "name": "Wooden Crank", + "category": "ae2:getting_started", + "icon": "ae2:crank", + "sortnum": 100, + "pages": [ + { + "type": "patchouli:text", + "text": "The $(thing)Wooden Crank$() can be used to manually operate the $(l:ae2:getting_started/charger)Charger$(/l), $(l:ae2:getting_started/inscriber)Inscriber$(/l), and $(l:ae2:getting_started/certus#cga)Crystal Growth Accelerators$(/l).$(br2)To place the $(thing)Crank$() on the $(thing)Charger$(), hold [$(k:sneak)] and then press [$(k:use)]. The $(thing)Wooden Crank$() can only be placed on a valid power input side (normally the top or bottom)." + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/crank", + "text": "Each turn generates $(l:ae2:concepts/power)160 AE$(). It takes 10 turns to generate enough power to craft a single item in the $(thing)Charger$().$(br2)It may break if overused." + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/fluix.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/fluix.json new file mode 100644 index 00000000000..14a65918544 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/fluix.json @@ -0,0 +1,17 @@ +{ + "name": "5) Fluix Crystals", + "category": "ae2:getting_started", + "icon": "ae2:fluix_crystal", + "sortnum": 50, + "pages": [ + { + "type": "patchouli:text", + "text": "$(thing)Fluix Crystals$() are used for several recipes. They are obtained by throwing $(l:ae2:getting_started/charger)Charged Fluix Crystals$(/l), $(item)Redstone$(), and $(item)Nether Quartz$() into a pool of water.$(br2)They can be crushed into $(thing)Fluix Dust$() in an $(l:ae2:getting_started/inscriber)Inscriber$(/l).$(br2)There is also a Block form, and it can be used to upgrade $(l:ae2:equipment/quartz_tools#fluix)Quartz Tools$(/l)." + }, + { + "type": "patchouli:spotlight", + "item": "ae2:fluix_crystal", + "link_recipe": true + } + ] +} diff --git a/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/inscriber.json b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/inscriber.json new file mode 100644 index 00000000000..f32903f4391 --- /dev/null +++ b/src/main/resources/assets/ae2/patchouli_books/guide/en_us/entries/getting_started/inscriber.json @@ -0,0 +1,32 @@ +{ + "name": "3) Inscriber", + "category": "ae2:getting_started", + "icon": "ae2:inscriber", + "sortnum": 30, + "pages": [ + { + "type": "patchouli:text", + "text": "The $(thing)Inscriber$() is a common crafting method in AE2. It is used to craft $(thing)Processors$() and certain $(thing)Dusts$().$(br2)The $(thing)Inscriber$() accepts $(l:ae2:concepts/power)Network Power, most external forms of power$(/l), or can be powered manually using a $(l:ae2:getting_started/crank)Wooden Crank$(/l).$(br2)It is $(l:ae2:concepts/sided)Sided$(/l), and can be $(l:ae2:equipment/wrench)Rotated$(/l)." + }, + { + "type": "patchouli:crafting", + "recipe": "ae2:network/blocks/inscribers", + "text": "Default Orientation Sidedness:$(li)Top Input = \"Top\" Side$(li)Middle Input = \"Sides\"$(li)Bottom Input = \"Bottom\" Side$(li)Output = \"Sides\"$(br)Sides: North, East, South, West$(br2)The items in the Input Slots will not stack." + }, + { + "type": "patchouli:text", + "text": "$(thing)Inscribers$() have 3 $(item)Card Upgrade$() Slots and only accept $(l:ae2:misc/cards/acceleration)Acceleration Cards$(/l). They significantly improve the processing speed.$(br2)You will want to automate the $(thing)Inscriber$(). An early-game example is to use $(item)Hoppers$(). Due to the $(thing)Inscriber's$() Sidedness, and $(item)Hoppers'$() limited interaction sides, you will want to rotate the $(thing)Inscriber$() using a $(l:ae2:equipment/wrench)Quartz Wrench$(/l)." + }, + { + "type": "patchouli:image", + "images": ["ae2:textures/patchouli/images/inscriber_setup.png"], + "border": true, + "title": "Example Semi-Automation" + }, + { + "type": "patchouli:text", + "title": "Notable Recipes", + "text": "$(todo)$()$(li)Printed Circuits & Silicon$(li)Processors$(li)Certus Quartz Dust$(li)Fluix Dust$(li)Ender Dust$(li)Sky Stone Dust$(li)Copy Inscriber Plates$(br2)Logic = Gold Ingot$(br)Engineering = Diamond$(br)Calculation = Certus Quartz" + } + ] +} diff --git a/src/main/resources/assets/ae2/textures/item/guide.png b/src/main/resources/assets/ae2/textures/item/guide.png new file mode 100644 index 00000000000..ff7af7ddd46 Binary files /dev/null and b/src/main/resources/assets/ae2/textures/item/guide.png differ diff --git a/src/main/resources/assets/ae2/textures/patchouli/images/crystal_growth_accelerator_setup.png b/src/main/resources/assets/ae2/textures/patchouli/images/crystal_growth_accelerator_setup.png new file mode 100644 index 00000000000..0b2995a990f Binary files /dev/null and b/src/main/resources/assets/ae2/textures/patchouli/images/crystal_growth_accelerator_setup.png differ diff --git a/src/main/resources/assets/ae2/textures/patchouli/images/inscriber_setup.png b/src/main/resources/assets/ae2/textures/patchouli/images/inscriber_setup.png new file mode 100644 index 00000000000..1c1097947c8 Binary files /dev/null and b/src/main/resources/assets/ae2/textures/patchouli/images/inscriber_setup.png differ diff --git a/src/main/resources/assets/ae2/textures/patchouli/patchouli_filler.png b/src/main/resources/assets/ae2/textures/patchouli/patchouli_filler.png new file mode 100644 index 00000000000..0f39b260478 Binary files /dev/null and b/src/main/resources/assets/ae2/textures/patchouli/patchouli_filler.png differ diff --git a/src/main/resources/assets/ae2/textures/patchouli/patchouli_gui.png b/src/main/resources/assets/ae2/textures/patchouli/patchouli_gui.png new file mode 100644 index 00000000000..435a663b5c5 Binary files /dev/null and b/src/main/resources/assets/ae2/textures/patchouli/patchouli_gui.png differ diff --git a/src/test/java/appeng/client/guidebook/compiler/IdUtilsTest.java b/src/test/java/appeng/client/guidebook/compiler/IdUtilsTest.java new file mode 100644 index 00000000000..052f0373640 --- /dev/null +++ b/src/test/java/appeng/client/guidebook/compiler/IdUtilsTest.java @@ -0,0 +1,20 @@ +package appeng.client.guidebook.compiler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class IdUtilsTest { + + @CsvSource({ + "some_page,ae2,ae2:some_page", + "ae2:some_page,ae2,ae2:some_page", + "minecraft:some_page,ae2,minecraft:some_page", + }) + @ParameterizedTest + void testResolveId(String input, String defaultNamespace, String expected) { + assertEquals(expected, IdUtils.resolveId(input, defaultNamespace).toString()); + } + +} diff --git a/src/test/java/appeng/client/guidebook/layout/flow/LineBuilderTest.java b/src/test/java/appeng/client/guidebook/layout/flow/LineBuilderTest.java new file mode 100644 index 00000000000..837efbe5e60 --- /dev/null +++ b/src/test/java/appeng/client/guidebook/layout/flow/LineBuilderTest.java @@ -0,0 +1,104 @@ +package appeng.client.guidebook.layout.flow; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import appeng.client.guidebook.document.LytRect; +import appeng.client.guidebook.document.flow.LytFlowSpan; +import appeng.client.guidebook.document.flow.LytFlowText; +import appeng.client.guidebook.layout.FontMetrics; +import appeng.client.guidebook.layout.LayoutContext; +import appeng.client.guidebook.style.ResolvedTextStyle; +import appeng.client.guidebook.style.TextAlignment; + +class LineBuilderTest { + + @Test + void breakAtStartOfChunkAfterWhitespaceInPreviousChunk() { + var lines = getLines(3, "A ", "BC"); + + assertThat(lines).extracting(this::getTextContent).containsExactly( + "A ", + "BC"); + } + + /** + * When not necessary, don't break in the middle of a word. + */ + @Test + void dontBreakInWords() { + var lines = getLines(3, "A BC"); + + assertThat(lines).extracting(this::getTextContent).containsExactly( + "A", + "BC"); + } + + /** + * When a white-space character causes a line-break, it is removed. + */ + @Test + void testWhitespaceCausingLineBreakGetsRemoved() { + var lines = getLines(1, "A B"); + + assertThat(lines).extracting(this::getTextContent).containsExactly( + "A", + "B"); + } + + /** + * Test white-space collapsing. + */ + @Test + void testWhitespaceCollapsing() { + var lines = getLines(3, "A B"); + + assertThat(lines).extracting(this::getTextContent).containsExactly( + "A B"); + } + + @NotNull + private static ArrayList getLines(int charsPerLine, String... textChunks) { + var lines = new ArrayList(); + var floats = new ArrayList(); + var context = new LayoutContext(new MockFontMetrics(), LytRect.empty()); + var lineBuilder = new LineBuilder(context, 0, 0, charsPerLine * 5, lines, floats, TextAlignment.LEFT); + + for (String textChunk : textChunks) { + var flowContent = new LytFlowText(); + flowContent.setText(textChunk); + flowContent.setParent(new LytFlowSpan()); + lineBuilder.accept(flowContent); + } + + lineBuilder.end(); + return lines; + } + + private String getTextContent(Line line) { + var result = new StringBuilder(); + for (var el = line.firstElement(); el != null; el = el.next) { + if (el instanceof LineTextRun run) { + result.append(run.text); + } + } + return result.toString(); + } + + // Every character is 5 pixels wide + record MockFontMetrics() implements FontMetrics { + @Override + public float getAdvance(int codePoint, ResolvedTextStyle style) { + return 5; + } + + @Override + public int getLineHeight(ResolvedTextStyle style) { + return 10; + } + } +} diff --git a/src/test/java/appeng/guidebook/compiler/PageCompilerTest.java b/src/test/java/appeng/guidebook/compiler/PageCompilerTest.java new file mode 100644 index 00000000000..0d311acca79 --- /dev/null +++ b/src/test/java/appeng/guidebook/compiler/PageCompilerTest.java @@ -0,0 +1,54 @@ +package appeng.guidebook.compiler; + +import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import appeng.client.guidebook.GuidePage; +import appeng.client.guidebook.compiler.PageCompiler; +import appeng.core.AppEng; +import appeng.util.BootstrapMinecraft; + +@BootstrapMinecraft +class PageCompilerTest { + private Path guidebookFolder; + + @BeforeEach + void setUp() throws Exception { + guidebookFolder = findGuidebookFolder(); + } + + @Test + void testCompileIndexPage() throws Exception { + compilePage("index"); + } + + private GuidePage compilePage(String id) throws Exception { + var path = guidebookFolder.resolve(id + ".md"); + try (var in = Files.newInputStream(path)) { + var parsed = PageCompiler.parse("ae2", AppEng.makeId(id), in); + return PageCompiler.compile(resourceLocation -> null, parsed); + } + } + + private static Path findGuidebookFolder() throws Exception { + // Search up for the guidebook folder + var url = PageCompilerTest.class.getProtectionDomain().getCodeSource().getLocation(); + var jarPath = Paths.get(url.toURI()); + var current = jarPath.getParent(); + while (current != null) { + var guidebookFolder = current.resolve("guidebook"); + if (Files.isDirectory(guidebookFolder) && Files.exists(guidebookFolder.resolve("index.md"))) { + return guidebookFolder; + } + + current = current.getParent(); + } + + throw new FileNotFoundException("Couldn't find guidebook folder. Started looking at " + jarPath); + } +}