diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index df6b257..79cd02e 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,13 +1,10 @@ name: Publish release - on: release: types: [published] - jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - name: Set up JDK 21 @@ -19,21 +16,33 @@ jobs: run: chmod +x gradlew - name: Build run: ./gradlew build + - name: Build inklecate jar + run: ./gradlew :inklecate:jar - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v5 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Release build deploy + - name: Publish runtime to Maven Central env: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - run: - ./gradlew clean publish -Prelease -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} - - name: Trigger manual upload to Central Repository run: | - curl -X POST \ - -H "Authorization: Bearer $(echo -n '${{ secrets.NEXUS_USERNAME }}:${{ secrets.NEXUS_PASSWORD }}' | base64)" \ - -H "Content-Type: application/json" \ - https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.bladecoder \ No newline at end of file + ./gradlew :runtime:clean :runtime:publish -Prelease \ + -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} \ + -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} \ + -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} + - name: Publish compiler to Maven Central + env: + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: | + ./gradlew :compiler:clean :compiler:publish -Prelease \ + -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} \ + -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} \ + -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} + - name: Upload inklecate jar to GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: inklecate/build/libs/*.jar diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 16e4365..ff2b572 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -1,14 +1,11 @@ name: Publish snapshot - on: push: branches: - master jobs: build: - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - name: Set up JDK 21 @@ -20,8 +17,13 @@ jobs: run: chmod +x gradlew - name: Build run: ./gradlew build - - name: Publish snapshot + - name: Publish runtime snapshot env: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - run: ./gradlew publish \ No newline at end of file + run: ./gradlew :runtime:publish + - name: Publish compiler snapshot + env: + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: ./gradlew :compiler:publish diff --git a/.gitignore b/.gitignore index c3b7aab..7a490ea 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ build/ *.log .DS_Store + +prompts/ diff --git a/README.md b/README.md index e9138dc..7b54159 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # blade-ink -This is a Java port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative. +This is a Java port of inkle's [ink](https://github.com/inkle/ink), a scripting language for writing interactive narrative, including the compiler and inklecate. **blade-ink** should support pretty much everything the original version does. If you find any bugs, please report them here! diff --git a/build.gradle b/build.gradle index 0738be8..3b16596 100644 --- a/build.gradle +++ b/build.gradle @@ -1,148 +1,103 @@ plugins { - id 'java-library' - id 'maven-publish' - id 'signing' - id "com.diffplug.spotless" version "7.2.1" + id "com.diffplug.spotless" version "7.2.1" apply false } -group = 'com.bladecoder.ink' +// Configuración común para todos los subproyectos +subprojects { + apply plugin: 'java-library' + apply plugin: 'maven-publish' + apply plugin: 'signing' + apply plugin: 'com.diffplug.spotless' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + group = 'com.bladecoder.ink' -repositories { - mavenCentral() -} + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -dependencies { - testImplementation 'junit:junit:4.13' -} + repositories { + mavenCentral() + } -if (!hasProperty("release") && !version.endsWith("-SNAPSHOT")) { - version += "-SNAPSHOT" -} + dependencies { + testImplementation 'junit:junit:4.13' + } + + if (!hasProperty("release") && !version.endsWith("-SNAPSHOT")) { + version += "-SNAPSHOT" + } -// DISABLES JAVADOC ULTRACHECKS IN JDK8 -if (JavaVersion.current().isJava8Compatible()) { - allprojects { + // DISABLES JAVADOC ULTRACHECKS IN JDK8 + if (JavaVersion.current().isJava8Compatible()) { tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') } } -} -spotless { - java { - target fileTree('src') { - include '**/*.java' + spotless { + java { + target fileTree('src') { + include '**/*.java' + } + toggleOffOn() + palantirJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() } - toggleOffOn() - palantirJavaFormat() - removeUnusedImports() - trimTrailingWhitespace() - endWithNewline() } -} - -jar { - manifest.attributes += [ - 'github' : 'https://github.com/bladecoder/blade-ink/', - 'license' : 'Apache-2.0', - 'group' : project.group, - 'version' : project.version, - 'java' : targetCompatibility, - 'timestamp': System.currentTimeMillis() - ] -} -javadoc { - title = "Blade Ink" - options { - memberLevel = JavadocMemberLevel.PUBLIC - author true - setUse true - encoding "UTF-8" + jar { + manifest.attributes += [ + 'github' : 'https://github.com/bladecoder/blade-ink/', + 'license' : 'Apache-2.0', + 'group' : project.group, + 'version' : project.version, + 'java' : targetCompatibility, + 'timestamp': System.currentTimeMillis() + ] } -} - -task sourcesJar(type: Jar) { - from sourceSets.main.allJava - archiveClassifier = 'sources' -} - -task javadocJar(type: Jar) { - from javadoc - archiveClassifier = 'javadoc' -} -publishing { - publications { - mavenJava(MavenPublication) { - artifactId = 'blade-ink' - from components.java - artifact sourcesJar - artifact javadocJar + javadoc { + options { + memberLevel = JavadocMemberLevel.PUBLIC + author true + setUse true + encoding "UTF-8" + } + if (JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } + } - versionMapping { - usage('java-api') { - fromResolutionOf('runtimeClasspath') - } - usage('java-runtime') { - fromResolutionResult() - } - } + task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = 'sources' + } - pom { - name = 'blade-ink' - description = "This is a Java port of inkle's ink, a scripting language for writing interactive narrative." - url = 'https://github.com/bladecoder/blade-ink' + task javadocJar(type: Jar) { + from javadoc + archiveClassifier = 'javadoc' + } - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'bladecoder' - name = 'Rafael Garcia' - email = 'bladecoder@gmail.com' - } - } - scm { - connection = 'scm:git@github.com:bladecoder/blade-ink.git' - developerConnection = 'scm:git@github.com:bladecoder/blade-ink.git' - url = 'scm:git@github.com:bladecoder/blade-ink.git' + publishing { + repositories { + maven { + def releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials { + username project.hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "$System.env.NEXUS_USERNAME" + password project.hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "$System.env.NEXUS_PASSWORD" } } } } - repositories { - maven { - def releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - credentials { - username project.hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "$System.env.NEXUS_USERNAME" - password project.hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "$System.env.NEXUS_PASSWORD" - } - } - } -} -signing { - if (!version.endsWith('SNAPSHOT')) { - useGpgCmd() - sign publishing.publications.mavenJava - } -} - -javadoc { - if (JavaVersion.current().isJava9Compatible()) { - options.addBooleanOption('html5', true) + signing { + if (!version.endsWith('SNAPSHOT')) { + useGpgCmd() + sign publishing.publications.mavenJava + } } } - - diff --git a/compiler/build.gradle b/compiler/build.gradle new file mode 100644 index 0000000..a44189c --- /dev/null +++ b/compiler/build.gradle @@ -0,0 +1,47 @@ +dependencies { + implementation project(':runtime') +} +javadoc { + title = "Blade Ink Compiler" +} +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'blade-ink-compiler' + from components.java + artifact sourcesJar + artifact javadocJar + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + pom { + name = 'blade-ink-compiler' + description = "Compiler for inkle's ink scripting language. Converts .ink files to JSON format." + url = 'https://github.com/bladecoder/blade-ink' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'bladecoder' + name = 'Rafael Garcia' + email = 'bladecoder@gmail.com' + } + } + scm { + connection = 'scm:git@github.com:bladecoder/blade-ink-java.git' + developerConnection = 'scm:git@github.com:bladecoder/blade-ink-java.git' + url = 'scm:git@github.com:bladecoder/blade-ink-java.git' + } + } + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterRange.java b/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterRange.java new file mode 100644 index 0000000..e68e750 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterRange.java @@ -0,0 +1,44 @@ +package com.bladecoder.ink.compiler; + +import java.util.Collection; +import java.util.HashSet; + +public final class CharacterRange { + public static CharacterRange define(char start, char end) { + return new CharacterRange(start, end, null); + } + + public static CharacterRange define(char start, char end, Collection excludes) { + return new CharacterRange(start, end, excludes); + } + + public CharacterSet toCharacterSet() { + if (_correspondingCharSet.isEmpty()) { + for (char c = _start; c <= _end; c++) { + if (!_excludes.contains(c)) { + _correspondingCharSet.add(c); + } + } + } + return _correspondingCharSet; + } + + public char getStart() { + return _start; + } + + public char getEnd() { + return _end; + } + + private CharacterRange(char start, char end, Collection excludes) { + _start = start; + _end = end; + _excludes = excludes == null ? new HashSet<>() : new HashSet<>(excludes); + } + + private final char _start; + private final char _end; + private final Collection _excludes; + private final CharacterSet _correspondingCharSet = new CharacterSet(); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterSet.java b/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterSet.java new file mode 100644 index 0000000..e5b901c --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/CharacterSet.java @@ -0,0 +1,41 @@ +package com.bladecoder.ink.compiler; + +import java.util.Collection; +import java.util.HashSet; + +public class CharacterSet extends HashSet { + public static CharacterSet fromRange(char start, char end) { + return new CharacterSet().addRange(start, end); + } + + public CharacterSet() {} + + public CharacterSet(String str) { + addCharacters(str); + } + + public CharacterSet(CharacterSet charSetToCopy) { + addCharacters(charSetToCopy); + } + + public CharacterSet addRange(char start, char end) { + for (char c = start; c <= end; ++c) { + add(c); + } + return this; + } + + public CharacterSet addCharacters(Collection chars) { + for (char c : chars) { + add(c); + } + return this; + } + + public CharacterSet addCharacters(String chars) { + for (char c : chars.toCharArray()) { + add(c); + } + return this; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/CommandLineInput.java b/compiler/src/main/java/com/bladecoder/ink/compiler/CommandLineInput.java new file mode 100644 index 0000000..24c86d2 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/CommandLineInput.java @@ -0,0 +1,10 @@ +package com.bladecoder.ink.compiler; + +public class CommandLineInput { + public boolean isHelp; + public boolean isExit; + public Integer choiceInput; + public Integer debugSource; + public String debugPathLookup; + public Object userImmediateModeStatement; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/CommentEliminator.java b/compiler/src/main/java/com/bladecoder/ink/compiler/CommentEliminator.java new file mode 100644 index 0000000..03cfa6a --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/CommentEliminator.java @@ -0,0 +1,80 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.compiler.StringParser.StringParser; +import java.util.List; + +public class CommentEliminator extends StringParser { + public CommentEliminator(String input) { + super(input); + } + + public String process() { + List stringList = interleave(optional(this::commentsAndNewlines), optional(this::mainInk)); + + if (stringList != null) { + return String.join("", stringList); + } + + return null; + } + + private String mainInk() { + return parseUntil(this::commentsAndNewlines, commentOrNewlineStartCharacter, null); + } + + private String commentsAndNewlines() { + List newlines = interleave(optional(this::parseNewline), optional(this::parseSingleComment)); + + if (newlines != null) { + return String.join("", newlines); + } + + return null; + } + + private Object parseSingleComment() { + return oneOf(this::endOfLineComment, this::blockComment); + } + + private String endOfLineComment() { + if (parseString("//") == null) { + return null; + } + + parseUntilCharactersFromCharSet(newlineCharacters); + + return ""; + } + + private String blockComment() { + if (parseString("/*") == null) { + return null; + } + + int startLineIndex = getLineIndex(); + + String commentResult = parseUntil(stringRule("*/"), commentBlockEndCharacter, null); + + if (!isEndOfInput()) { + parseString("*/"); + } + + if (commentResult != null) { + return repeatChar('\n', getLineIndex() - startLineIndex); + } + + return null; + } + + private String repeatChar(char ch, int count) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + sb.append(ch); + } + return sb.toString(); + } + + private final CharacterSet commentOrNewlineStartCharacter = new CharacterSet("/\r\n"); + private final CharacterSet commentBlockEndCharacter = new CharacterSet("*"); + private final CharacterSet newlineCharacters = new CharacterSet("\n\r"); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/Compiler.java b/compiler/src/main/java/com/bladecoder/ink/compiler/Compiler.java new file mode 100644 index 0000000..3d181bd --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/Compiler.java @@ -0,0 +1,254 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.compiler.ParsedHierarchy.ParsedObject; +import com.bladecoder.ink.compiler.ParsedHierarchy.Story; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.DebugMetadata; +import com.bladecoder.ink.runtime.Error.ErrorHandler; +import com.bladecoder.ink.runtime.Error.ErrorType; +import com.bladecoder.ink.runtime.RTObject; +import java.util.ArrayList; +import java.util.List; + +public class Compiler { + private static final ErrorHandler DEFAULT_ERROR_HANDLER = new ErrorHandler() { + @Override + public void error(String message, ErrorType errorType) { + if (errorType == ErrorType.Error) { + throw new RuntimeException(message); + } + if (message != null && !message.isEmpty()) { + System.err.println(message); + } + } + }; + + public static class Options { + public String sourceFilename; + public List pluginDirectories; + public boolean countAllVisits; + public ErrorHandler errorHandler; + public IFileHandler fileHandler; + } + + public Story getParsedStory() { + return parsedStory; + } + + public Compiler(String inkSource, Options options) { + inputString = inkSource; + if (options != null) { + this.options = options; + } else { + this.options = new Options(); + this.options.countAllVisits = true; + } + if (this.options.errorHandler == null) { + this.options.errorHandler = DEFAULT_ERROR_HANDLER; + } + if (this.options.pluginDirectories != null) { + pluginManager = new PluginManager(this.options.pluginDirectories); + } + } + + public Compiler() { + this(null, null); + } + + public Story parse() { + parser = new InkParser(inputString, options.sourceFilename, this::onParseError, options.fileHandler); + parsedStory = parser.parse(); + return parsedStory; + } + + public com.bladecoder.ink.runtime.Story compile() { + if (pluginManager != null) { + inputString = pluginManager.preParse(inputString); + } + + parse(); + + if (pluginManager != null) { + parsedStory = pluginManager.postParse(parsedStory); + } + + if (parsedStory != null && !hadParseError) { + parsedStory.countAllVisits = options.countAllVisits; + + runtimeStory = parsedStory.exportRuntime(options.errorHandler); + + if (pluginManager != null) { + runtimeStory = pluginManager.postExport(parsedStory, runtimeStory); + } + } else { + runtimeStory = null; + } + + return runtimeStory; + } + + public String compile(String source) { + Compiler compiler = new Compiler(source, null); + com.bladecoder.ink.runtime.Story story = compiler.compile(); + if (story == null) { + return "{}"; + } + try { + return story.toJson(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static class CommandLineInputResult { + public boolean requestsExit; + public int choiceIdx = -1; + public String divertedPath; + public String output; + } + + public CommandLineInputResult handleInput(CommandLineInput inputResult) { + CommandLineInputResult result = new CommandLineInputResult(); + + if (inputResult.debugSource != null) { + int offset = inputResult.debugSource; + DebugMetadata dm = debugMetadataForContentAtOffset(offset); + if (dm != null) { + result.output = "DebugSource: " + dm.toString(); + } else { + result.output = "DebugSource: Unknown source"; + } + } else if (inputResult.debugPathLookup != null) { + String pathStr = inputResult.debugPathLookup; + try { + com.bladecoder.ink.runtime.SearchResult contentResult = + runtimeStory.contentAtPath(new com.bladecoder.ink.runtime.Path(pathStr)); + DebugMetadata dm = contentResult.obj.getDebugMetadata(); + if (dm != null) { + result.output = "DebugSource: " + dm.toString(); + } else { + result.output = "DebugSource: Unknown source"; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (inputResult.userImmediateModeStatement != null) { + ParsedObject parsedObj = (ParsedObject) inputResult.userImmediateModeStatement; + return executeImmediateStatement(parsedObj); + } else { + return null; + } + + return result; + } + + private CommandLineInputResult executeImmediateStatement(ParsedObject parsedObj) { + CommandLineInputResult result = new CommandLineInputResult(); + + if (parsedObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment) { + com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment varAssign = + (com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment) parsedObj; + if (varAssign.isNewTemporaryDeclaration) { + parsedStory.tryAddNewVariableDeclaration(varAssign); + } + } + + parsedObj.parent = parsedStory; + RTObject runtimeObj = parsedObj.getRuntimeObject(); + + parsedObj.resolveReferences(parsedStory); + + if (!parsedStory.hadError()) { + if (parsedObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert parsedDivert = + (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) parsedObj; + try { + result.divertedPath = + parsedDivert.runtimeDivert.getTargetPath().toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (parsedObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Expression + || parsedObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment) { + try { + RTObject evalResult = runtimeStory.evaluateExpression((Container) runtimeObj); + if (evalResult != null) { + result.output = evalResult.toString(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } else { + parsedStory.resetError(); + } + + return result; + } + + public void retrieveDebugSourceForLatestContent() { + for (RTObject outputObj : runtimeStory.getState().getOutputStream()) { + com.bladecoder.ink.runtime.StringValue textContent = + outputObj instanceof com.bladecoder.ink.runtime.StringValue + ? (com.bladecoder.ink.runtime.StringValue) outputObj + : null; + if (textContent != null) { + DebugSourceRange range = new DebugSourceRange(); + range.length = textContent.getValue().length(); + range.debugMetadata = textContent.getDebugMetadata(); + range.text = textContent.getValue(); + debugSourceRanges.add(range); + } + } + } + + private DebugMetadata debugMetadataForContentAtOffset(int offset) { + int currOffset = 0; + + DebugMetadata lastValidMetadata = null; + for (DebugSourceRange range : debugSourceRanges) { + if (range.debugMetadata != null) { + lastValidMetadata = range.debugMetadata; + } + + if (offset >= currOffset && offset < currOffset + range.length) { + return lastValidMetadata; + } + + currOffset += range.length; + } + + return null; + } + + public static class DebugSourceRange { + public int length; + public DebugMetadata debugMetadata; + public String text; + } + + private void onParseError(String message, ErrorType errorType) { + if (errorType == ErrorType.Error) { + hadParseError = true; + } + + if (options.errorHandler != null) { + options.errorHandler.error(message, errorType); + } else { + throw new RuntimeException(message); + } + } + + private String inputString; + private Options options; + + private InkParser parser; + private Story parsedStory; + private com.bladecoder.ink.runtime.Story runtimeStory; + + private PluginManager pluginManager; + + private boolean hadParseError; + + private final List debugSourceRanges = new ArrayList<>(); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/FileHandler.java b/compiler/src/main/java/com/bladecoder/ink/compiler/FileHandler.java new file mode 100644 index 0000000..5f91843 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/FileHandler.java @@ -0,0 +1,21 @@ +package com.bladecoder.ink.compiler; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +class DefaultFileHandler implements IFileHandler { + @Override + public String resolveInkFilename(String includeName) { + Path workingDir = Paths.get("").toAbsolutePath(); + return workingDir.resolve(includeName).toString(); + } + + @Override + public String loadInkFileContents(String fullFilename) throws IOException { + byte[] bytes = Files.readAllBytes(Paths.get(fullFilename)); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/IFileHandler.java b/compiler/src/main/java/com/bladecoder/ink/compiler/IFileHandler.java new file mode 100644 index 0000000..97f24d4 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/IFileHandler.java @@ -0,0 +1,9 @@ +package com.bladecoder.ink.compiler; + +import java.io.IOException; + +public interface IFileHandler { + String resolveInkFilename(String includeName); + + String loadInkFileContents(String fullFilename) throws IOException; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/InkParser.java b/compiler/src/main/java/com/bladecoder/ink/compiler/InkParser.java new file mode 100644 index 0000000..3ea3fbf --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/InkParser.java @@ -0,0 +1,2802 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.compiler.ParsedHierarchy.Identifier; +import com.bladecoder.ink.compiler.ParsedHierarchy.ParsedObject; +import com.bladecoder.ink.compiler.ParsedHierarchy.Story; +import com.bladecoder.ink.compiler.StringParser.StringParser; +import com.bladecoder.ink.compiler.StringParser.StringParser.ParseRule; +import com.bladecoder.ink.compiler.StringParser.StringParser.SpecificParseRule; +import com.bladecoder.ink.compiler.StringParser.StringParserState; +import com.bladecoder.ink.runtime.DebugMetadata; +import com.bladecoder.ink.runtime.Error.ErrorType; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class InkParser extends StringParser { + public InkParser( + String str, + String filenameForMetadata, + com.bladecoder.ink.runtime.Error.ErrorHandler externalErrorHandler, + IFileHandler fileHandler) { + this(str, filenameForMetadata, externalErrorHandler, null, fileHandler); + } + + public InkParser(String str) { + this(str, null, null, null, null); + } + + private InkParser( + String str, + String inkFilename, + com.bladecoder.ink.runtime.Error.ErrorHandler externalErrorHandler, + InkParser rootParser, + IFileHandler fileHandler) { + super(str); + filename = inkFilename; + registerExpressionOperators(); + generateStatementLevelRules(); + + this.errorHandler = this::onStringParserError; + + this.externalErrorHandler = externalErrorHandler; + + this.fileHandler = fileHandler != null ? fileHandler : new DefaultFileHandler(); + + if (rootParser == null) { + rootParserRef = this; + openFilenames = new HashSet<>(); + if (inkFilename != null) { + String fullRootInkPath = this.fileHandler.resolveInkFilename(inkFilename); + openFilenames.add(fullRootInkPath); + } + } else { + rootParserRef = rootParser; + } + } + + public Story parse() { + List topLevelContent = statementsAtLevel(StatementLevel.Top); + return new Story(topLevelContent, rootParserRef != this); + } + + protected List separatedList(SpecificParseRule mainRule, ParseRule separatorRule) { + T firstElement = parse(mainRule); + if (firstElement == null) { + return null; + } + + List allElements = new ArrayList<>(); + allElements.add(firstElement); + + while (true) { + int nextElementRuleId = beginRule(); + + Object sep = separatorRule.parse(); + if (sep == null) { + failRule(nextElementRuleId); + break; + } + + T nextElement = parse(mainRule); + if (nextElement == null) { + failRule(nextElementRuleId); + break; + } + + succeedRule(nextElementRuleId, null); + allElements.add(nextElement); + } + + return allElements; + } + + @Override + protected String preProcessInputString(String str) { + return new CommentEliminator(str).process(); + } + + protected DebugMetadata createDebugMetadata( + StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd) { + DebugMetadata md = new DebugMetadata(); + md.startLineNumber = stateAtStart.lineIndex + 1; + md.endLineNumber = stateAtEnd.lineIndex + 1; + md.startCharacterNumber = stateAtStart.characterInLineIndex + 1; + md.endCharacterNumber = stateAtEnd.characterInLineIndex + 1; + md.fileName = filename; + return md; + } + + @Override + protected void ruleDidSucceed( + Object result, StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd) { + ParsedObject parsedObj = result instanceof ParsedObject ? (ParsedObject) result : null; + if (parsedObj != null) { + parsedObj.setDebugMetadata(createDebugMetadata(stateAtStart, stateAtEnd)); + return; + } + + if (result instanceof List) { + List parsedListObjs = (List) result; + for (Object obj : parsedListObjs) { + ParsedObject parsedListObj = obj instanceof ParsedObject ? (ParsedObject) obj : null; + if (parsedListObj != null && !parsedListObj.hasOwnDebugMetadata()) { + parsedListObj.setDebugMetadata(createDebugMetadata(stateAtStart, stateAtEnd)); + } + } + } + + Identifier id = result instanceof Identifier ? (Identifier) result : null; + if (id != null) { + id.debugMetadata = createDebugMetadata(stateAtStart, stateAtEnd); + } + } + + protected boolean isParsingStringExpression() { + return getFlag(CustomFlags.ParsingString.flag); + } + + protected void setParsingStringExpression(boolean value) { + setFlag(CustomFlags.ParsingString.flag, value); + } + + protected boolean isTagActive() { + return getFlag(CustomFlags.TagActive.flag); + } + + protected void setTagActive(boolean value) { + setFlag(CustomFlags.TagActive.flag, value); + } + + protected enum CustomFlags { + ParsingString(0x1), + TagActive(0x2); + + final long flag; + + CustomFlags(long flag) { + this.flag = flag; + } + } + + protected static class FlowDecl { + public com.bladecoder.ink.compiler.ParsedHierarchy.Identifier name; + public List arguments; + public boolean isFunction; + } + + private void onStringParserError(String message, int index, int lineIndex, boolean isWarning) { + String warningType = isWarning ? "WARNING:" : "ERROR:"; + String fullMessage; + + if (filename != null) { + fullMessage = String.format("%s '%s' line %d: %s", warningType, filename, lineIndex + 1, message); + } else { + fullMessage = String.format("%s line %d: %s", warningType, lineIndex + 1, message); + } + + if (externalErrorHandler != null) { + externalErrorHandler.error(fullMessage, isWarning ? ErrorType.Warning : ErrorType.Error); + } else { + throw new RuntimeException(fullMessage); + } + } + + protected Object endOfLine() { + return oneOf(this::newline, this::endOfFile); + } + + protected Object newline() { + whitespace(); + + boolean gotNewline = parseNewline() != null; + + if (!gotNewline) { + return null; + } + + return ParseSuccess; + } + + protected Object endOfFile() { + whitespace(); + + if (!isEndOfInput()) { + return null; + } + + return ParseSuccess; + } + + protected Object multilineWhitespace() { + List newlines = oneOrMore(this::newline); + if (newlines == null) { + return null; + } + + int numNewlines = newlines.size(); + if (numNewlines >= 1) { + return ParseSuccess; + } + + return null; + } + + protected Object whitespace() { + if (parseCharactersFromCharSet(inlineWhitespaceChars) != null) { + return ParseSuccess; + } + + return null; + } + + protected ParseRule spaced(ParseRule rule) { + return () -> { + whitespace(); + + Object result = parseObject(rule); + if (result == null) { + return null; + } + + whitespace(); + + return result; + }; + } + + protected Object anyWhitespace() { + boolean anyWhitespace = false; + while (oneOf(this::whitespace, this::multilineWhitespace) != null) { + anyWhitespace = true; + } + return anyWhitespace ? ParseSuccess : null; + } + + protected ParseRule multiSpaced(ParseRule rule) { + return () -> { + anyWhitespace(); + + Object result = parseObject(rule); + if (result == null) { + return null; + } + + anyWhitespace(); + + return result; + }; + } + + protected enum StatementLevel { + InnerBlock, + Stitch, + Knot, + Top + } + + protected List statementsAtLevel(StatementLevel level) { + if (level == StatementLevel.InnerBlock) { + Object badGatherDashCount = parse(this::gatherDashes); + if (badGatherDashCount != null) { + error( + "You can't use a gather (the dashes) within the { curly braces } context. For multi-line sequences and conditions, you should only use one dash.", + false); + } + } + + List results = interleave( + optional(this::multilineWhitespace), + () -> statementAtLevel(level), + () -> statementsBreakForLevel(level), + true); + if (results == null) { + return null; + } + return results; + } + + protected void registerExpressionOperators() { + maxBinaryOpLength = 0; + binaryOperators = new ArrayList<>(); + + registerBinaryOperator("&&", 1, false); + registerBinaryOperator("||", 1, false); + registerBinaryOperator("and", 1, true); + registerBinaryOperator("or", 1, true); + + registerBinaryOperator("==", 2, false); + registerBinaryOperator(">=", 2, false); + registerBinaryOperator("<=", 2, false); + registerBinaryOperator("<", 2, false); + registerBinaryOperator(">", 2, false); + registerBinaryOperator("!=", 2, false); + + registerBinaryOperator("?", 3, false); + registerBinaryOperator("has", 3, true); + registerBinaryOperator("!?", 3, false); + registerBinaryOperator("hasnt", 3, true); + registerBinaryOperator("^", 3, false); + + registerBinaryOperator("+", 4, false); + registerBinaryOperator("-", 5, false); + registerBinaryOperator("*", 6, false); + registerBinaryOperator("/", 7, false); + + registerBinaryOperator("%", 8, false); + registerBinaryOperator("mod", 8, true); + } + + protected void generateStatementLevelRules() { + for (StatementLevel level : StatementLevel.values()) { + List rulesAtLevel = new ArrayList<>(); + List breakingRules = new ArrayList<>(); + + rulesAtLevel.add(line(this::multiDivert)); + + if (level.ordinal() >= StatementLevel.Top.ordinal()) { + rulesAtLevel.add(this::knotDefinition); + } + + rulesAtLevel.add(line(this::choice)); + rulesAtLevel.add(line(this::authorWarning)); + + if (level.ordinal() > StatementLevel.InnerBlock.ordinal()) { + rulesAtLevel.add(this::gather); + } + + if (level.ordinal() >= StatementLevel.Knot.ordinal()) { + rulesAtLevel.add(this::stitchDefinition); + } + + rulesAtLevel.add(line(this::listDeclaration)); + rulesAtLevel.add(line(this::variableDeclaration)); + rulesAtLevel.add(line(this::constDeclaration)); + rulesAtLevel.add(line(this::externalDeclaration)); + + rulesAtLevel.add(line(this::includeStatement)); + + rulesAtLevel.add(this::logicLine); + rulesAtLevel.add(this::lineOfMixedTextAndLogic); + + if (level.ordinal() <= StatementLevel.Knot.ordinal()) { + breakingRules.add(this::knotDeclaration); + } + + if (level.ordinal() <= StatementLevel.Stitch.ordinal()) { + breakingRules.add(this::stitchDeclaration); + } + + if (level.ordinal() <= StatementLevel.InnerBlock.ordinal()) { + breakingRules.add(this::parseDashNotArrow); + breakingRules.add(stringRule("}")); + } + + statementRulesAtLevel.put(level, rulesAtLevel); + statementBreakRulesAtLevel.put(level, breakingRules); + } + } + + protected Object statementsBreakForLevel(StatementLevel level) { + whitespace(); + + List breakRules = statementBreakRulesAtLevel.get(level); + if (breakRules == null || breakRules.isEmpty()) { + return null; + } + + Object breakRuleResult = oneOf(breakRules.toArray(new ParseRule[0])); + if (breakRuleResult == null) { + return null; + } + + return breakRuleResult; + } + + protected Object statementAtLevel(StatementLevel level) { + List rulesAtLevel = statementRulesAtLevel.get(level); + if (rulesAtLevel == null || rulesAtLevel.isEmpty()) { + return null; + } + Object statement = oneOf(rulesAtLevel.toArray(new ParseRule[0])); + + if (level == StatementLevel.Top && statement instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Return) { + error("should not have return statement outside of a knot", false); + } + + return statement; + } + + protected List lineOfMixedTextAndLogic() { + parse(this::whitespace); + + List result = mixedTextAndLogic(); + if (result == null || result.isEmpty()) { + return null; + } + + ParsedObject first = result.get(0); + if (first instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Text) { + com.bladecoder.ink.compiler.ParsedHierarchy.Text text = + (com.bladecoder.ink.compiler.ParsedHierarchy.Text) first; + if (text.getText().startsWith("return")) { + warning( + "Do you need a '~' before 'return'? If not, perhaps use a glue: <> (since it's lowercase) or rewrite somehow?"); + } + } + + ParsedObject lastObj = result.get(result.size() - 1); + if (!(lastObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert)) { + trimEndWhitespace(result, false); + } + + endTagIfNecessary(result); + + boolean lineIsPureTag = !result.isEmpty() + && result.get(0) instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Tag + && ((com.bladecoder.ink.compiler.ParsedHierarchy.Tag) result.get(0)).isStart; + if (!lineIsPureTag) { + result.add(new com.bladecoder.ink.compiler.ParsedHierarchy.Text("\n")); + } + + expect(this::endOfLine, "end of line", this::skipToNextLine); + + return result; + } + + protected List mixedTextAndLogic() { + Object disallowedTilda = parseObject(spaced(stringRule("~"))); + if (disallowedTilda != null) { + error( + "You shouldn't use a '~' here - tildas are for logic that's on its own line. To do inline logic, use { curly braces } instead", + false); + } + + List results = + interleave(optional(this::contentText), optional(this::inlineLogicOrGlueOrStartTag)); + + if (!parsingChoice) { + List diverts = parse(this::multiDivert); + if (diverts != null) { + if (results == null) { + results = new ArrayList<>(); + } + + endTagIfNecessary(results); + trimEndWhitespace(results, true); + results.addAll(diverts); + } + } + + return results; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Text contentText() { + return contentTextAllowingEscapeChar(); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Text contentTextAllowingEscapeChar() { + StringBuilder sb = null; + + while (true) { + String str = parse(this::contentTextNoEscape); + boolean gotEscapeChar = parseString("\\\\") != null; + + if (gotEscapeChar || str != null) { + if (sb == null) { + sb = new StringBuilder(); + } + + if (str != null) { + sb.append(str); + } + + if (gotEscapeChar) { + char c = parseSingleCharacter(); + sb.append(c); + } + } else { + break; + } + } + + if (sb != null) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.Text(sb.toString()); + } + + return null; + } + + protected String contentTextNoEscape() { + if (nonTextPauseCharacters == null) { + nonTextPauseCharacters = new CharacterSet("-<"); + } + + if (nonTextEndCharacters == null) { + nonTextEndCharacters = new CharacterSet("{}|\n\r\\#"); + notTextEndCharactersChoice = new CharacterSet(nonTextEndCharacters); + notTextEndCharactersChoice.addCharacters("[]"); + notTextEndCharactersString = new CharacterSet(nonTextEndCharacters); + notTextEndCharactersString.addCharacters("\""); + } + + ParseRule nonTextRule = + () -> oneOf(this::parseDivertArrow, this::parseThreadArrow, this::endOfLine, this::glue); + + CharacterSet endChars; + if (isParsingStringExpression()) { + endChars = notTextEndCharactersString; + } else if (parsingChoice) { + endChars = notTextEndCharactersChoice; + } else { + endChars = nonTextEndCharacters; + } + + return parseUntil(nonTextRule, nonTextPauseCharacters, endChars); + } + + private void trimEndWhitespace(List mixedTextAndLogicResults, boolean terminateWithSpace) { + if (mixedTextAndLogicResults.isEmpty()) { + return; + } + int lastObjIdx = mixedTextAndLogicResults.size() - 1; + ParsedObject lastObj = mixedTextAndLogicResults.get(lastObjIdx); + if (lastObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Text) { + com.bladecoder.ink.compiler.ParsedHierarchy.Text text = + (com.bladecoder.ink.compiler.ParsedHierarchy.Text) lastObj; + text.setText(text.getText().replaceAll("[ \\t]+$", "")); + + if (terminateWithSpace) { + text.setText(text.getText() + " "); + } else if (text.getText().isEmpty()) { + mixedTextAndLogicResults.remove(lastObjIdx); + trimEndWhitespace(mixedTextAndLogicResults, false); + } + } + } + + private void endTagIfNecessary(List results) { + if (isTagActive()) { + if (results != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.Tag tag = + new com.bladecoder.ink.compiler.ParsedHierarchy.Tag(); + tag.isStart = false; + results.add(tag); + } + setTagActive(false); + } + } + + private void endTagIfNecessary(com.bladecoder.ink.compiler.ParsedHierarchy.ContentList contentList) { + if (isTagActive()) { + if (contentList != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.Tag tag = + new com.bladecoder.ink.compiler.ParsedHierarchy.Tag(); + tag.isStart = false; + contentList.addContent(tag); + } + setTagActive(false); + } + } + + protected Object skipToNextLine() { + parseUntilCharactersFromString("\n\r"); + parseNewline(); + return ParseSuccess; + } + + protected ParseRule line(ParseRule inlineRule) { + return () -> { + Object result = parseObject(inlineRule); + if (result == null) { + return null; + } + + expect(this::endOfLine, "end of line", this::skipToNextLine); + + return result; + }; + } + + private ParsedObject inlineLogicOrGlueOrStartTag() { + return (ParsedObject) oneOf(this::inlineLogic, this::glue, this::startTag); + } + + protected ParsedObject tempDeclarationOrAssignment() { + whitespace(); + + boolean isNewDeclaration = parseTempKeyword(); + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier varIdentifier = null; + if (isNewDeclaration) { + varIdentifier = (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "variable name"); + } else { + varIdentifier = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + } + + if (varIdentifier == null) { + return null; + } + + whitespace(); + + boolean isIncrement = parseString("+") != null; + boolean isDecrement = parseString("-") != null; + if (isIncrement && isDecrement) { + error("Unexpected sequence '+-'", false); + } + + if (parseString("=") == null) { + if (isNewDeclaration) { + error("Expected '='", false); + } + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression assignedExpression = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + expect(this::expression, "value expression to be assigned"); + + if (isIncrement || isDecrement) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.IncDecExpression( + varIdentifier, assignedExpression, isIncrement); + } else { + com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment result = + new com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment( + varIdentifier, assignedExpression); + result.isNewTemporaryDeclaration = isNewDeclaration; + return result; + } + } + + protected void disallowIncrement(ParsedObject expr) { + if (expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.IncDecExpression) { + error("Can't use increment/decrement here. It can only be used on a ~ line", false); + } + } + + protected boolean parseTempKeyword() { + int ruleId = beginRule(); + + String id = parse(this::identifier); + if ("temp".equals(id)) { + succeedRule(ruleId, null); + return true; + } + + failRule(ruleId); + return false; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Return returnStatement() { + whitespace(); + + String returnOrDone = parse(this::identifier); + if (!"return".equals(returnOrDone)) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = expression(); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.Return(expr); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expression() { + return expression(0); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expression(int minimumPrecedence) { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = expressionUnary(); + if (expr == null) { + return null; + } + + whitespace(); + + while (true) { + int ruleId = beginRule(); + + InfixOperator infixOp = parseInfixOperator(); + if (infixOp != null && infixOp.precedence > minimumPrecedence) { + String expectationMessage = String.format("right side of '%s' expression", infixOp.type); + com.bladecoder.ink.compiler.ParsedHierarchy.Expression leftExpr = expr; + InfixOperator opSnapshot = infixOp; + com.bladecoder.ink.compiler.ParsedHierarchy.Expression multiaryExpr = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + expect(() -> expressionInfixRight(leftExpr, opSnapshot), expectationMessage, null); + if (multiaryExpr == null) { + failRule(ruleId); + return null; + } + + expr = (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) succeedRule(ruleId, multiaryExpr); + continue; + } + + failRule(ruleId); + break; + } + + whitespace(); + + return expr; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionUnary() { + com.bladecoder.ink.compiler.ParsedHierarchy.Expression divertTarget = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) parse(this::expressionDivertTarget); + if (divertTarget != null) { + return divertTarget; + } + + String prefixOp = (String) oneOf(stringRule("-"), stringRule("!")); + if (prefixOp == null) { + prefixOp = parse(this::expressionNot); + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) oneOf( + this::expressionList, + this::expressionParen, + this::expressionFunctionCall, + this::expressionVariableName, + this::expressionLiteral); + + if (expr == null && prefixOp != null) { + expr = expressionUnary(); + } + + if (expr == null) { + return null; + } + + if (prefixOp != null) { + expr = com.bladecoder.ink.compiler.ParsedHierarchy.UnaryExpression.withInner(expr, prefixOp); + } + + whitespace(); + + String postfixOp = (String) oneOf(stringRule("++"), stringRule("--")); + if (postfixOp != null) { + boolean isInc = "++".equals(postfixOp); + + if (!(expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference)) { + error("can only increment and decrement variables, but saw '" + expr + "'", false); + } else { + com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference varRef = + (com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference) expr; + expr = new com.bladecoder.ink.compiler.ParsedHierarchy.IncDecExpression(varRef.getIdentifier(), isInc); + } + } + + return expr; + } + + protected String expressionNot() { + String id = identifier(); + if ("not".equals(id)) { + return id; + } + + return null; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionLiteral() { + return (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + oneOf(this::expressionFloat, this::expressionInt, this::expressionBool, this::expressionString); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionDivertTarget() { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert = parse(this::singleDivert); + if (divert == null) { + return null; + } + + if (divert.isThread) { + return null; + } + + whitespace(); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.DivertTarget(divert); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Number expressionInt() { + Integer intOrNull = parseInt(); + if (intOrNull == null) { + return null; + } + return new com.bladecoder.ink.compiler.ParsedHierarchy.Number(intOrNull); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Number expressionFloat() { + Float floatOrNull = parseFloat(); + if (floatOrNull == null) { + return null; + } + return new com.bladecoder.ink.compiler.ParsedHierarchy.Number(floatOrNull); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression expressionString() { + String openQuote = parseString("\""); + if (openQuote == null) { + return null; + } + + setParsingStringExpression(true); + + List textAndLogic = parse(this::mixedTextAndLogic); + + expect(stringRule("\""), "close quote for string expression", null); + + setParsingStringExpression(false); + + if (textAndLogic == null) { + textAndLogic = new ArrayList<>(); + textAndLogic.add(new com.bladecoder.ink.compiler.ParsedHierarchy.Text("")); + } else { + for (ParsedObject obj : textAndLogic) { + if (obj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert) { + error("String expressions cannot contain diverts (->)", false); + break; + } + } + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression(textAndLogic); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Number expressionBool() { + String id = parse(this::identifier); + if ("true".equals(id)) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.Number(true); + } else if ("false".equals(id)) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.Number(false); + } + + return null; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionFunctionCall() { + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier iden = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (iden == null) { + return null; + } + + whitespace(); + + List arguments = + parse(this::expressionFunctionCallArguments); + if (arguments == null) { + return null; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall(iden, arguments); + } + + protected List expressionFunctionCallArguments() { + if (parseString("(") == null) { + return null; + } + + ParseRule commas = exclude(stringRule(",")); + List arguments = + interleave(this::expression, commas, null, true); + if (arguments == null) { + arguments = new ArrayList<>(); + } + + whitespace(); + + expect(stringRule(")"), "closing ')' for function call", null); + + return arguments; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionVariableName() { + List path = + interleave(this::identifierWithMetadata, exclude(spaced(stringRule("."))), null, true); + + if (path == null + || path.isEmpty() + || com.bladecoder.ink.compiler.ParsedHierarchy.Story.isReservedKeyword(path.get(0).name)) { + return null; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference(path); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionParen() { + if (parseString("(") == null) { + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression innerExpr = parse(this::expression); + if (innerExpr == null) { + return null; + } + + whitespace(); + + expect(stringRule(")"), "closing parenthesis ')' for expression", null); + + return innerExpr; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression expressionInfixRight( + com.bladecoder.ink.compiler.ParsedHierarchy.Expression left, InfixOperator op) { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression right = parse(() -> expression(op.precedence)); + if (right != null) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.BinaryExpression(left, right, op.type); + } + + return null; + } + + private InfixOperator parseInfixOperator() { + for (InfixOperator op : binaryOperators) { + int ruleId = beginRule(); + + if (parseString(op.type) != null) { + if (op.requireWhitespace) { + if (whitespace() == null) { + failRule(ruleId); + continue; + } + } + + return (InfixOperator) succeedRule(ruleId, op); + } + + failRule(ruleId); + } + + return null; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.List expressionList() { + whitespace(); + + if (parseString("(") == null) { + return null; + } + + whitespace(); + + List memberNames = + separatedList(this::listMember, spaced(stringRule(","))); + + whitespace(); + + if (parseString(")") == null) { + return null; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.List(memberNames); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Identifier listMember() { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier identifier = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (identifier == null) { + return null; + } + + String dot = parseString("."); + if (dot != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier identifier2 = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "element name within the set " + identifier, null); + identifier.name = identifier.name + "." + (identifier2 != null ? identifier2.name : ""); + } + + whitespace(); + + return identifier; + } + + private void registerBinaryOperator(String op, int precedence, boolean requireWhitespace) { + binaryOperators.add(new InfixOperator(op, precedence, requireWhitespace)); + maxBinaryOpLength = Math.max(maxBinaryOpLength, op.length()); + } + + private void registerBinaryOperator(String op, int precedence) { + registerBinaryOperator(op, precedence, false); + } + + protected static class InfixOperator { + public final String type; + public final int precedence; + public final boolean requireWhitespace; + + public InfixOperator(String type, int precedence, boolean requireWhitespace) { + this.type = type; + this.precedence = precedence; + this.requireWhitespace = requireWhitespace; + } + + @Override + public String toString() { + return type; + } + } + + private List multiDivert() { + whitespace(); + + List diverts = null; + + com.bladecoder.ink.compiler.ParsedHierarchy.Divert threadDivert = parse(this::startThread); + if (threadDivert != null) { + diverts = new ArrayList<>(); + diverts.add(threadDivert); + return diverts; + } + + List arrowsAndDiverts = + interleave(this::parseDivertArrowOrTunnelOnwards, this::divertIdentifierWithArguments, null, true); + if (arrowsAndDiverts == null) { + return null; + } + + diverts = new ArrayList<>(); + + endTagIfNecessary(diverts); + + for (int i = 0; i < arrowsAndDiverts.size(); ++i) { + boolean isArrow = (i % 2) == 0; + + if (isArrow) { + if ("->->".equals(arrowsAndDiverts.get(i))) { + boolean tunnelOnwardsPlacementValid = + (i == 0 || i == arrowsAndDiverts.size() - 1 || i == arrowsAndDiverts.size() - 2); + if (!tunnelOnwardsPlacementValid) { + error("Tunnel onwards '->->' must only come at the begining or the start of a divert", false); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.TunnelOnwards tunnelOnwards = + new com.bladecoder.ink.compiler.ParsedHierarchy.TunnelOnwards(); + if (i < arrowsAndDiverts.size() - 1) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert tunnelOnwardDivert = + (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) arrowsAndDiverts.get(i + 1); + tunnelOnwards.divertAfter = tunnelOnwardDivert; + tunnelOnwards.addContent(tunnelOnwardDivert); + } + + diverts.add(tunnelOnwards); + break; + } + } else { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert = + (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) arrowsAndDiverts.get(i); + if (i < arrowsAndDiverts.size() - 1) { + divert.isTunnel = true; + } + diverts.add(divert); + } + } + + if (diverts.isEmpty() && arrowsAndDiverts.size() == 1) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert gatherDivert = + new com.bladecoder.ink.compiler.ParsedHierarchy.Divert((ParsedObject) null); + gatherDivert.isEmpty = true; + diverts.add(gatherDivert); + + if (!parsingChoice) { + error("Empty diverts (->) are only valid on choices", false); + } + } + + return diverts; + } + + private Object parseDivertArrow() { + return parseString("->"); + } + + private Object parseThreadArrow() { + return parseString("<-"); + } + + protected ParsedObject logicLine() { + whitespace(); + + if (parseString("~") == null) { + return null; + } + + whitespace(); + + ParseRule afterTilda = () -> oneOf(this::returnStatement, this::tempDeclarationOrAssignment, this::expression); + + ParsedObject result = (ParsedObject) expect(afterTilda, "expression after '~'", this::skipToNextLine); + + if (result == null) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(); + } + + if (result instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Expression + && !(result instanceof com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall + || result instanceof com.bladecoder.ink.compiler.ParsedHierarchy.IncDecExpression)) { + com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference varRef = + result instanceof com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference + ? (com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference) result + : null; + if (varRef != null && "include".equals(varRef.getName())) { + error( + "'~ include' is no longer the correct syntax - please use 'INCLUDE your_filename.ink', without the tilda, and in block capitals.", + false); + } else { + error( + "Logic following a '~' can't be that type of expression. It can only be something like:\n\t~ return\n\t~ var x = blah\n\t~ x++\n\t~ myFunction()", + false); + } + } + + com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall funCall = + result instanceof com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall + ? (com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall) result + : null; + if (funCall != null) { + funCall.shouldPopReturnedValue = true; + } + + if (result.find(com.bladecoder.ink.compiler.ParsedHierarchy.FunctionCall.class) != null) { + result = new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList( + result, new com.bladecoder.ink.compiler.ParsedHierarchy.Text("\n")); + } + + expect(this::endOfLine, "end of line", this::skipToNextLine); + + return result; + } + + protected ParsedObject variableDeclaration() { + whitespace(); + + String id = parse(this::identifier); + if (!"VAR".equals(id)) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier varName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "variable name"); + + whitespace(); + + expect( + stringRule("="), + "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)", + null); + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression definition = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + expect(this::expression, "initial value for ", null); + + if (definition != null) { + if (!(definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Number + || definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression + || definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.DivertTarget + || definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.VariableReference + || definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.List)) { + error("initial value for a variable must be a number, constant, list or divert target", false); + } + + if (parse(this::listElementDefinitionSeparator) != null) { + error("Unexpected ','. If you're trying to declare a new list, use the LIST keyword, not VAR", false); + } else if (definition instanceof com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression) { + com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression strExpr = + (com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression) definition; + if (!strExpr.isSingleString()) { + error("Constant strings cannot contain any logic.", false); + } + } + + com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment result = + new com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment(varName, definition); + result.isGlobalDeclaration = true; + return result; + } + + return null; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment listDeclaration() { + whitespace(); + + String id = parse(this::identifier); + if (!"LIST".equals(id)) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier varName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "list name"); + + whitespace(); + + expect(stringRule("="), "the '=' for an assignment of the list definition", null); + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition definition = + (com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition) + expect(this::listDefinition, "list item names", null); + + if (definition != null) { + definition.identifier = varName; + return new com.bladecoder.ink.compiler.ParsedHierarchy.VariableAssignment(varName, definition); + } + + return null; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition listDefinition() { + anyWhitespace(); + + List allElements = + separatedList(this::listElementDefinition, this::listElementDefinitionSeparator); + if (allElements == null) { + return null; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition(allElements); + } + + protected String listElementDefinitionSeparator() { + anyWhitespace(); + + if (parseString(",") == null) { + return null; + } + + anyWhitespace(); + + return ","; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.ListElementDefinition listElementDefinition() { + boolean inInitialList = parseString("(") != null; + boolean needsToCloseParen = inInitialList; + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier name = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (name == null) { + return null; + } + + whitespace(); + + if (inInitialList) { + if (parseString(")") != null) { + needsToCloseParen = false; + whitespace(); + } + } + + Integer elementValue = null; + if (parseString("=") != null) { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Number elementValueNum = + (com.bladecoder.ink.compiler.ParsedHierarchy.Number) + expect(this::expressionInt, "value to be assigned to list item", null); + if (elementValueNum != null && elementValueNum.value instanceof Integer) { + elementValue = (Integer) elementValueNum.value; + } + + if (needsToCloseParen) { + whitespace(); + if (parseString(")") != null) { + needsToCloseParen = false; + } + } + } + + if (needsToCloseParen) { + error("Expected closing ')'", false); + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.ListElementDefinition(name, inInitialList, elementValue); + } + + protected ParsedObject constDeclaration() { + whitespace(); + + String id = parse(this::identifier); + if (!"CONST".equals(id)) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier varName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "constant name"); + + whitespace(); + + expect( + stringRule("="), + "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)", + null); + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + expect(this::expression, "initial value for ", null); + if (!(expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Number + || expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.DivertTarget + || expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression)) { + error("initial value for a constant must be a number or divert target", false); + } else if (expr instanceof com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression) { + com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression strExpr = + (com.bladecoder.ink.compiler.ParsedHierarchy.StringExpression) expr; + if (!strExpr.isSingleString()) { + error("Constant strings cannot contain any logic.", false); + } + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.ConstantDeclaration(varName, expr); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.AuthorWarning authorWarning() { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier identifier = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (identifier == null || !"TODO".equals(identifier.name)) { + return null; + } + + whitespace(); + + parseString(":"); + + whitespace(); + + String message = parseUntilCharactersFromString("\n\r"); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.AuthorWarning(message); + } + + protected Object includeStatement() { + whitespace(); + + if (parseString("INCLUDE") == null) { + return null; + } + + whitespace(); + + String filename = + (String) expect(() -> parseUntilCharactersFromString("\n\r"), "filename for include statement", null); + if (filename != null) { + filename = filename.trim(); + } + + String fullFilename = rootParserRef.fileHandler.resolveInkFilename(filename); + + if (filenameIsAlreadyOpen(fullFilename)) { + error("Recursive INCLUDE detected: '" + fullFilename + "' is already open.", false); + parseUntilCharactersFromString("\r\n"); + return new com.bladecoder.ink.compiler.ParsedHierarchy.IncludedFile(null); + } else { + addOpenFilename(fullFilename); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Story includedStory = null; + String includedString = null; + try { + includedString = rootParserRef.fileHandler.loadInkFileContents(fullFilename); + } catch (Exception e) { + error("Failed to load: '" + filename + "'", false); + } + + if (includedString != null) { + InkParser parser = + new InkParser(includedString, filename, externalErrorHandler, rootParserRef, fileHandler); + includedStory = parser.parse(); + } + + removeOpenFilename(fullFilename); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.IncludedFile(includedStory); + } + + private boolean filenameIsAlreadyOpen(String fullFilename) { + return rootParserRef.openFilenames.contains(fullFilename); + } + + private void addOpenFilename(String fullFilename) { + rootParserRef.openFilenames.add(fullFilename); + } + + private void removeOpenFilename(String fullFilename) { + rootParserRef.openFilenames.remove(fullFilename); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Choice choice() { + boolean onceOnlyChoice = true; + List bullets = interleave(optionalExclude(this::whitespace), stringRule("*"), null, true); + if (bullets == null) { + bullets = interleave(optionalExclude(this::whitespace), stringRule("+"), null, true); + if (bullets == null) { + return null; + } + + onceOnlyChoice = false; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier optionalName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::bracketedName); + + whitespace(); + + if (optionalName != null) { + newline(); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression conditionExpr = parse(this::choiceCondition); + + whitespace(); + + if (parsingChoice) { + throw new RuntimeException("Already parsing a choice - shouldn't have nested choices"); + } + parsingChoice = true; + + com.bladecoder.ink.compiler.ParsedHierarchy.ContentList startContent = null; + List startTextAndLogic = parse(this::mixedTextAndLogic); + if (startTextAndLogic != null) { + startContent = new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(startTextAndLogic); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.ContentList optionOnlyContent = null; + com.bladecoder.ink.compiler.ParsedHierarchy.ContentList innerContent = null; + + boolean hasWeaveStyleInlineBrackets = parseString("[") != null; + if (hasWeaveStyleInlineBrackets) { + endTagIfNecessary(startContent); + + List optionOnlyTextAndLogic = parse(this::mixedTextAndLogic); + if (optionOnlyTextAndLogic != null) { + optionOnlyContent = new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(optionOnlyTextAndLogic); + } + + expect(stringRule("]"), "closing ']' for weave-style option", null); + + endTagIfNecessary(optionOnlyContent); + + List innerTextAndLogic = parse(this::mixedTextAndLogic); + if (innerTextAndLogic != null) { + innerContent = new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(innerTextAndLogic); + } + } + + whitespace(); + + endTagIfNecessary(innerContent != null ? innerContent : startContent); + + List diverts = parse(this::multiDivert); + + parsingChoice = false; + + whitespace(); + + boolean emptyContent = startContent == null && innerContent == null && optionOnlyContent == null; + if (emptyContent && diverts == null) { + warning( + "Choice is completely empty. Interpretting as a default fallback choice. Add a divert arrow to remove this warning: * ->"); + } else if (startContent == null && hasWeaveStyleInlineBrackets && optionOnlyContent == null) { + warning("Blank choice - if you intended a default fallback choice, use the `* ->` syntax"); + } + + if (innerContent == null) { + innerContent = new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(); + } + + endTagIfNecessary(innerContent); + + if (diverts != null) { + for (ParsedObject divObj : diverts) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert div = + divObj instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert + ? (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) divObj + : null; + + if (div != null && div.isEmpty) { + continue; + } + + innerContent.addContent(divObj); + } + } + + innerContent.addContent(new com.bladecoder.ink.compiler.ParsedHierarchy.Text("\n")); + + com.bladecoder.ink.compiler.ParsedHierarchy.Choice choice = + new com.bladecoder.ink.compiler.ParsedHierarchy.Choice(startContent, optionOnlyContent, innerContent); + choice.identifier = optionalName; + choice.indentationDepth = bullets.size(); + choice.hasWeaveStyleInlineBrackets = hasWeaveStyleInlineBrackets; + choice.setCondition(conditionExpr); + choice.onceOnly = onceOnlyChoice; + choice.isInvisibleDefault = emptyContent; + + return choice; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression choiceCondition() { + List conditions = + interleave(this::choiceSingleCondition, this::choiceConditionsSpace, null, true); + if (conditions == null) { + return null; + } else if (conditions.size() == 1) { + return conditions.get(0); + } + return new com.bladecoder.ink.compiler.ParsedHierarchy.MultipleConditionExpression(conditions); + } + + protected Object choiceConditionsSpace() { + newline(); + whitespace(); + return ParseSuccess; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression choiceSingleCondition() { + if (parseString("{") == null) { + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression condExpr = + (com.bladecoder.ink.compiler.ParsedHierarchy.Expression) + expect(this::expression, "choice condition inside { }", null); + disallowIncrement(condExpr); + + expect(stringRule("}"), "closing '}' for choice condition", null); + + return condExpr; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Gather gather() { + Object gatherDashCountObj = parse(this::gatherDashes); + if (gatherDashCountObj == null) { + return null; + } + + int gatherDashCount = (int) gatherDashCountObj; + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier optionalName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::bracketedName); + + com.bladecoder.ink.compiler.ParsedHierarchy.Gather gather = + new com.bladecoder.ink.compiler.ParsedHierarchy.Gather(optionalName, gatherDashCount); + + newline(); + + return gather; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Identifier bracketedName() { + if (parseString("(") == null) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier name = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (name == null) { + return null; + } + + whitespace(); + + expect(stringRule(")"), "closing ')' for bracketed name", null); + + return name; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Knot knotDefinition() { + FlowDecl knotDecl = parse(this::knotDeclaration); + if (knotDecl == null) { + return null; + } + + expect(this::endOfLine, "end of line after knot name definition", this::skipToNextLine); + + ParseRule innerKnotStatements = () -> statementsAtLevel(StatementLevel.Knot); + + List content = (List) + expect(innerKnotStatements, "at least one line within the knot", this::knotStitchNoContentRecoveryRule); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.Knot( + knotDecl.name, content, knotDecl.arguments, knotDecl.isFunction); + } + + protected FlowDecl knotDeclaration() { + whitespace(); + + if (knotTitleEquals() == null) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier identifier = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier knotName; + + boolean isFunc = identifier != null && "function".equals(identifier.name); + if (isFunc) { + expect(this::whitespace, "whitespace after the 'function' keyword", null); + knotName = (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + } else { + knotName = identifier; + } + + if (knotName == null) { + error("Expected the name of the " + (isFunc ? "function" : "knot"), false); + knotName = new com.bladecoder.ink.compiler.ParsedHierarchy.Identifier(); + knotName.name = ""; + } + + whitespace(); + + List parameterNames = + parse(this::bracketedKnotDeclArguments); + + whitespace(); + + parse(this::knotTitleEquals); + + FlowDecl decl = new FlowDecl(); + decl.name = knotName; + decl.arguments = parameterNames; + decl.isFunction = isFunc; + return decl; + } + + protected String knotTitleEquals() { + String multiEquals = parseCharactersFromString("="); + if (multiEquals == null || multiEquals.length() <= 1) { + return null; + } + return multiEquals; + } + + protected ParsedObject stitchDefinition() { + FlowDecl decl = parse(this::stitchDeclaration); + if (decl == null) { + return null; + } + + expect(this::endOfLine, "end of line after stitch name", this::skipToNextLine); + + ParseRule innerStitchStatements = () -> statementsAtLevel(StatementLevel.Stitch); + + List content = (List) expect( + innerStitchStatements, "at least one line within the stitch", this::knotStitchNoContentRecoveryRule); + + return new com.bladecoder.ink.compiler.ParsedHierarchy.Stitch( + decl.name, content, decl.arguments, decl.isFunction); + } + + protected FlowDecl stitchDeclaration() { + whitespace(); + + if (parseString("=") == null) { + return null; + } + + if (parseString("=") != null) { + return null; + } + + whitespace(); + + boolean isFunc = parseString("function") != null; + if (isFunc) { + whitespace(); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier stitchName = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (stitchName == null) { + return null; + } + + whitespace(); + + List flowArgs = + parse(this::bracketedKnotDeclArguments); + + whitespace(); + + FlowDecl decl = new FlowDecl(); + decl.name = stitchName; + decl.arguments = flowArgs; + decl.isFunction = isFunc; + return decl; + } + + protected Object knotStitchNoContentRecoveryRule() { + parseUntil(this::knotDeclaration, new CharacterSet("="), null); + + List recoveredFlowContent = new ArrayList<>(); + recoveredFlowContent.add(new com.bladecoder.ink.compiler.ParsedHierarchy.Text("")); + return recoveredFlowContent; + } + + protected List bracketedKnotDeclArguments() { + if (parseString("(") == null) { + return null; + } + + List flowArguments = + interleave(spaced(this::flowDeclArgument), exclude(stringRule(",")), null, true); + + expect(stringRule(")"), "closing ')' for parameter list", null); + + if (flowArguments == null) { + flowArguments = new ArrayList<>(); + } + + return flowArguments; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.FlowBase.Argument flowDeclArgument() { + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier firstIden = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + whitespace(); + Object divertArrow = parse(this::parseDivertArrow); + whitespace(); + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier secondIden = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + + if (firstIden == null && secondIden == null) { + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.FlowBase.Argument flowArg = + new com.bladecoder.ink.compiler.ParsedHierarchy.FlowBase.Argument(); + if (divertArrow != null) { + flowArg.isDivertTarget = true; + } + + if (firstIden != null && "ref".equals(firstIden.name)) { + if (secondIden == null) { + error("Expected an parameter name after 'ref'", false); + } + + flowArg.identifier = secondIden; + flowArg.isByReference = true; + } else { + if (flowArg.isDivertTarget) { + flowArg.identifier = secondIden; + } else { + flowArg.identifier = firstIden; + } + + if (flowArg.identifier == null) { + error("Expected an parameter name", false); + } + + flowArg.isByReference = false; + } + + return flowArg; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.ExternalDeclaration externalDeclaration() { + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier external = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (external == null || !"EXTERNAL".equals(external.name)) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier funcIdentifier = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) + expect(this::identifierWithMetadata, "name of external function", null); + if (funcIdentifier == null) { + funcIdentifier = new com.bladecoder.ink.compiler.ParsedHierarchy.Identifier(); + } + + whitespace(); + + List parameterNames = + (List) expect( + this::bracketedKnotDeclArguments, + "declaration of arguments for EXTERNAL, even if empty, i.e. 'EXTERNAL " + funcIdentifier + + "()'", + null); + if (parameterNames == null) { + parameterNames = new ArrayList<>(); + } + + List argNames = new ArrayList<>(); + for (com.bladecoder.ink.compiler.ParsedHierarchy.FlowBase.Argument arg : parameterNames) { + argNames.add(arg.identifier != null ? arg.identifier.name : null); + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.ExternalDeclaration(funcIdentifier, argNames); + } + + protected ParsedObject inlineLogic() { + if (parseString("{") == null) { + return null; + } + + boolean wasParsingString = isParsingStringExpression(); + boolean wasTagActive = isTagActive(); + + whitespace(); + + ParsedObject logic = (ParsedObject) + expect(this::innerLogic, "some kind of logic, conditional or sequence within braces: { ... }", null); + if (logic == null) { + setParsingStringExpression(wasParsingString); + return null; + } + + disallowIncrement(logic); + + com.bladecoder.ink.compiler.ParsedHierarchy.ContentList contentList = + logic instanceof com.bladecoder.ink.compiler.ParsedHierarchy.ContentList + ? (com.bladecoder.ink.compiler.ParsedHierarchy.ContentList) logic + : new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(logic); + + whitespace(); + + expect(stringRule("}"), "closing brace '}' for inline logic", null); + + setParsingStringExpression(wasParsingString); + + if (!wasTagActive) { + endTagIfNecessary(contentList.getContent()); + } + + return contentList; + } + + protected ParsedObject innerLogic() { + whitespace(); + + Integer explicitSeqType = (Integer) parseObject(this::sequenceTypeAnnotation); + if (explicitSeqType != null) { + @SuppressWarnings("unchecked") + List contentLists = + (List) + expect(this::innerSequenceObjects, "sequence elements (for cycle/stoping etc)", null); + if (contentLists == null) { + return null; + } + return new com.bladecoder.ink.compiler.ParsedHierarchy.Sequence(contentLists, explicitSeqType); + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression initialQueryExpression = + parse(this::conditionExpression); + if (initialQueryExpression != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.Conditional conditional = + (com.bladecoder.ink.compiler.ParsedHierarchy.Conditional) expect( + () -> innerConditionalContent(initialQueryExpression), + "conditional content following query", + null); + return conditional; + } + + ParseRule[] rules = { + this::innerConditionalContent, this::innerSequence, this::innerExpression, + }; + + for (ParseRule rule : rules) { + int ruleId = beginRule(); + + ParsedObject result = (ParsedObject) parseObject(rule); + if (result != null) { + if (peek(spaced(stringRule("}"))) == null) { + failRule(ruleId); + } else { + return (ParsedObject) succeedRule(ruleId, result); + } + } else { + failRule(ruleId); + } + } + + return null; + } + + protected ParsedObject innerExpression() { + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = parse(this::expression); + if (expr != null) { + expr.setOutputWhenComplete(true); + } + return expr; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Sequence innerSequence() { + whitespace(); + + int seqType = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping; + + Integer parsedSeqType = (Integer) parseObject(this::sequenceTypeAnnotation); + if (parsedSeqType != null) { + seqType = parsedSeqType; + } + + List contentLists = parse(this::innerSequenceObjects); + if (contentLists == null || contentLists.size() <= 1) { + return null; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.Sequence(contentLists, seqType); + } + + protected Object sequenceTypeAnnotation() { + Integer annotation = (Integer) parseObject(this::sequenceTypeSymbolAnnotation); + + if (annotation == null) { + annotation = (Integer) parseObject(this::sequenceTypeWordAnnotation); + } + + if (annotation == null) { + return null; + } + + int value = annotation; + int shuffleStopping = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Shuffle + | com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping; + int shuffleOnce = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Shuffle + | com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Once; + + if (value != com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Once + && value != com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Cycle + && value != com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping + && value != com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Shuffle + && value != shuffleStopping + && value != shuffleOnce) { + error("Sequence type combination not supported: " + value, false); + return com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping; + } + + return annotation; + } + + protected Object sequenceTypeSymbolAnnotation() { + if (sequenceTypeSymbols == null) { + sequenceTypeSymbols = new CharacterSet("!&~$ "); + } + + int sequenceType = 0; + String sequenceAnnotations = parseCharactersFromCharSet(sequenceTypeSymbols); + if (sequenceAnnotations == null) { + return null; + } + + for (char symbolChar : sequenceAnnotations.toCharArray()) { + switch (symbolChar) { + case '!': + sequenceType |= com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Once; + break; + case '&': + sequenceType |= com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Cycle; + break; + case '~': + sequenceType |= com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Shuffle; + break; + case '$': + sequenceType |= com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping; + break; + default: + break; + } + } + + if (sequenceType == 0) { + return null; + } + + return sequenceType; + } + + protected Object sequenceTypeWordAnnotation() { + List sequenceTypes = interleave(this::sequenceTypeSingleWord, exclude(this::whitespace), null, true); + if (sequenceTypes == null || sequenceTypes.isEmpty()) { + return null; + } + + if (parseString(":") == null) { + return null; + } + + int combinedSequenceType = 0; + for (Integer seqType : sequenceTypes) { + combinedSequenceType |= seqType; + } + + return combinedSequenceType; + } + + protected Object sequenceTypeSingleWord() { + Integer seqType = null; + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier word = + (com.bladecoder.ink.compiler.ParsedHierarchy.Identifier) parse(this::identifierWithMetadata); + if (word != null) { + switch (word.name) { + case "once": + seqType = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Once; + break; + case "cycle": + seqType = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Cycle; + break; + case "shuffle": + seqType = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Shuffle; + break; + case "stopping": + seqType = com.bladecoder.ink.compiler.ParsedHierarchy.SequenceType.Stopping; + break; + default: + break; + } + } + + if (seqType == null) { + return null; + } + + return seqType; + } + + protected List innerSequenceObjects() { + boolean multiline = parse(this::newline) != null; + + List result; + if (multiline) { + result = parse(this::innerMultilineSequenceObjects); + } else { + result = parse(this::innerInlineSequenceObjects); + } + + return result; + } + + protected List innerInlineSequenceObjects() { + List interleavedContentAndPipes = + interleave(optional(this::mixedTextAndLogic), stringRule("|"), null, false); + if (interleavedContentAndPipes == null) { + return null; + } + + List result = new ArrayList<>(); + + boolean justHadContent = false; + for (Object contentOrPipe : interleavedContentAndPipes) { + if (contentOrPipe instanceof String && "|".equals(contentOrPipe)) { + if (!justHadContent) { + result.add(new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList()); + } + + justHadContent = false; + } else { + @SuppressWarnings("unchecked") + List content = (List) contentOrPipe; + if (content == null) { + error("Expected content, but got " + contentOrPipe + " (this is an ink compiler bug!)", false); + } else { + result.add(new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(content)); + } + + justHadContent = true; + } + } + + if (!justHadContent) { + result.add(new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList()); + } + + return result; + } + + protected List innerMultilineSequenceObjects() { + multilineWhitespace(); + + List contentLists = oneOrMore(this::singleMultilineSequenceElement); + if (contentLists == null) { + return null; + } + + @SuppressWarnings("unchecked") + List result = + (List) (List) contentLists; + return result; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.ContentList singleMultilineSequenceElement() { + whitespace(); + + if (parseString("->") != null) { + return null; + } + + if (parseString("-") == null) { + return null; + } + + whitespace(); + + List content = statementsAtLevel(StatementLevel.InnerBlock); + + if (content == null) { + multilineWhitespace(); + } else { + content.add(0, new com.bladecoder.ink.compiler.ParsedHierarchy.Text("\n")); + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(content); + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Conditional innerConditionalContent() { + com.bladecoder.ink.compiler.ParsedHierarchy.Expression initialQueryExpression = + parse(this::conditionExpression); + com.bladecoder.ink.compiler.ParsedHierarchy.Conditional conditional = + parse(() -> innerConditionalContent(initialQueryExpression)); + if (conditional == null) { + return null; + } + + return conditional; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Conditional innerConditionalContent( + com.bladecoder.ink.compiler.ParsedHierarchy.Expression initialQueryExpression) { + List alternatives; + + boolean canBeInline = initialQueryExpression != null; + boolean isInline = parse(this::newline) == null; + + if (isInline && !canBeInline) { + return null; + } + + if (isInline) { + alternatives = inlineConditionalBranches(); + } else { + alternatives = multilineConditionalBranches(); + if (alternatives == null) { + if (initialQueryExpression != null) { + List soleContent = statementsAtLevel(StatementLevel.InnerBlock); + if (soleContent != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch soleBranch = + new com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch(soleContent); + alternatives = new ArrayList<>(); + alternatives.add(soleBranch); + + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch elseBranch = + parse(this::singleMultilineCondition); + if (elseBranch != null) { + if (!elseBranch.isElse) { + errorWithParsedObject( + "Expected an '- else:' clause here rather than an extra condition", + elseBranch, + false); + elseBranch.isElse = true; + } + alternatives.add(elseBranch); + } + } + } + + if (alternatives == null) { + return null; + } + } else if (alternatives.size() == 1 && alternatives.get(0).isElse && initialQueryExpression != null) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch emptyTrueBranch = + new com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch(null); + emptyTrueBranch.isTrueBranch = true; + alternatives.add(0, emptyTrueBranch); + } + + if (initialQueryExpression != null) { + boolean earlierBranchesHaveOwnExpression = false; + for (int i = 0; i < alternatives.size(); ++i) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch branch = alternatives.get(i); + boolean isLast = (i == alternatives.size() - 1); + + if (branch.getOwnExpression() != null) { + branch.matchingEquality = true; + earlierBranchesHaveOwnExpression = true; + } else if (earlierBranchesHaveOwnExpression && isLast) { + branch.matchingEquality = true; + branch.isElse = true; + } else { + if (!isLast && alternatives.size() > 2) { + errorWithParsedObject( + "Only final branch can be an 'else'. Did you miss a ':'?", branch, false); + } else { + if (i == 0) { + branch.isTrueBranch = true; + } else { + branch.isElse = true; + } + } + } + } + } else { + for (int i = 0; i < alternatives.size(); ++i) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch alt = alternatives.get(i); + boolean isLast = (i == alternatives.size() - 1); + if (alt.getOwnExpression() == null) { + if (isLast) { + alt.isElse = true; + } else { + if (alt.isElse) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch finalClause = + alternatives.get(alternatives.size() - 1); + if (finalClause.isElse) { + errorWithParsedObject( + "Multiple 'else' cases. Can have a maximum of one, at the end.", + finalClause, + false); + } else { + errorWithParsedObject( + "'else' case in conditional should always be the final one", alt, false); + } + } else { + errorWithParsedObject( + "Branch doesn't have condition. Are you missing a ':'? ", alt, false); + } + } + } + } + + if (alternatives.size() == 1 && alternatives.get(0).getOwnExpression() == null) { + errorWithParsedObject("Condition block with no conditions", alternatives.get(0), false); + } + } + } + + if (alternatives == null) { + return null; + } + + for (com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch branch : alternatives) { + branch.isInline = isInline; + } + + return new com.bladecoder.ink.compiler.ParsedHierarchy.Conditional(initialQueryExpression, alternatives); + } + + protected List inlineConditionalBranches() { + List> listOfLists = + interleave(this::mixedTextAndLogic, exclude(stringRule("|")), null, false); + if (listOfLists == null || listOfLists.isEmpty()) { + return null; + } + + List result = new ArrayList<>(); + + if (listOfLists.size() > 2) { + error("Expected one or two alternatives separated by '|' in inline conditional", false); + } else { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch trueBranch = + new com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch(listOfLists.get(0)); + trueBranch.isTrueBranch = true; + result.add(trueBranch); + + if (listOfLists.size() > 1) { + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch elseBranch = + new com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch(listOfLists.get(1)); + elseBranch.isElse = true; + result.add(elseBranch); + } + } + + return result; + } + + protected List multilineConditionalBranches() { + multilineWhitespace(); + + List multipleConditions = oneOrMore(this::singleMultilineCondition); + if (multipleConditions == null) { + return null; + } + + multilineWhitespace(); + + @SuppressWarnings("unchecked") + List result = + (List) + (List) multipleConditions; + return result; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch singleMultilineCondition() { + whitespace(); + + if (parseString("->") != null) { + return null; + } + + if (parseString("-") == null) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = null; + boolean isElse = parse(this::elseExpression) != null; + + if (!isElse) { + expr = parse(this::conditionExpression); + } + + List content = statementsAtLevel(StatementLevel.InnerBlock); + if (expr == null && content == null) { + error("expected content for the conditional branch following '-'", false); + + content = new ArrayList<>(); + content.add(new com.bladecoder.ink.compiler.ParsedHierarchy.Text("")); + } + + multilineWhitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch branch = + new com.bladecoder.ink.compiler.ParsedHierarchy.ConditionalSingleBranch(content); + branch.setOwnExpression(expr); + branch.isElse = isElse; + return branch; + } + + protected com.bladecoder.ink.compiler.ParsedHierarchy.Expression conditionExpression() { + com.bladecoder.ink.compiler.ParsedHierarchy.Expression expr = parse(this::expression); + if (expr == null) { + return null; + } + + disallowIncrement(expr); + + whitespace(); + + if (parseString(":") == null) { + return null; + } + + return expr; + } + + protected Object elseExpression() { + if (parseString("else") == null) { + return null; + } + + whitespace(); + + if (parseString(":") == null) { + return null; + } + + return ParseSuccess; + } + + private ParsedObject startTag() { + whitespace(); + + if (parseString("#") == null) { + return null; + } + + if (isParsingStringExpression()) { + error("Tags aren't allowed inside of strings. Please use \\# if you want a hash symbol.", false); + } + + ParsedObject result; + + if (isTagActive()) { + com.bladecoder.ink.compiler.ParsedHierarchy.ContentList contentList = + new com.bladecoder.ink.compiler.ParsedHierarchy.ContentList(); + com.bladecoder.ink.compiler.ParsedHierarchy.Tag endTag = + new com.bladecoder.ink.compiler.ParsedHierarchy.Tag(); + endTag.isStart = false; + contentList.addContent(endTag); + com.bladecoder.ink.compiler.ParsedHierarchy.Tag startTag = + new com.bladecoder.ink.compiler.ParsedHierarchy.Tag(); + startTag.isStart = true; + contentList.addContent(startTag); + result = contentList; + } else { + com.bladecoder.ink.compiler.ParsedHierarchy.Tag tag = new com.bladecoder.ink.compiler.ParsedHierarchy.Tag(); + tag.isStart = true; + result = tag; + } + + setTagActive(true); + + whitespace(); + + return result; + } + + private ParsedObject glue() { + String glueStr = parseString("<>"); + if (glueStr != null) { + return new com.bladecoder.ink.compiler.ParsedHierarchy.Glue(new com.bladecoder.ink.runtime.Glue()); + } + return null; + } + + private String identifier() { + String name = parseCharactersFromCharSet(getIdentifierCharSet()); + if (name == null) { + return null; + } + + boolean isNumberCharsOnly = true; + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (!(c >= '0' && c <= '9')) { + isNumberCharsOnly = false; + break; + } + } + + if (isNumberCharsOnly) { + return null; + } + + return name; + } + + private Object identifierWithMetadata() { + String id = identifier(); + if (id == null) { + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Identifier identifier = + new com.bladecoder.ink.compiler.ParsedHierarchy.Identifier(); + identifier.name = id; + return identifier; + } + + private Object gatherDashes() { + whitespace(); + + int gatherDashCount = 0; + + while (parseDashNotArrow() != null) { + gatherDashCount++; + whitespace(); + } + + if (gatherDashCount == 0) { + return null; + } + + return gatherDashCount; + } + + private Object parseDashNotArrow() { + int ruleId = beginRule(); + + if (parseString("->") == null && parseSingleCharacter() == '-') { + return succeedRule(ruleId, ParseSuccess); + } + + return failRule(ruleId); + } + + private com.bladecoder.ink.compiler.ParsedHierarchy.Divert startThread() { + whitespace(); + + if (parseThreadArrow() == null) { + return null; + } + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert = + (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) expect( + this::divertIdentifierWithArguments, + "target for new thread", + () -> new com.bladecoder.ink.compiler.ParsedHierarchy.Divert((ParsedObject) null)); + divert.isThread = true; + return divert; + } + + private com.bladecoder.ink.compiler.ParsedHierarchy.Divert divertIdentifierWithArguments() { + whitespace(); + + List targetComponents = + parse(this::dotSeparatedDivertPathComponents); + if (targetComponents == null) { + return null; + } + + whitespace(); + + List optionalArguments = + parse(this::expressionFunctionCallArguments); + + whitespace(); + + com.bladecoder.ink.compiler.ParsedHierarchy.Path targetPath = + new com.bladecoder.ink.compiler.ParsedHierarchy.Path(targetComponents); + return new com.bladecoder.ink.compiler.ParsedHierarchy.Divert(targetPath, optionalArguments); + } + + private com.bladecoder.ink.compiler.ParsedHierarchy.Divert singleDivert() { + List diverts = parse(this::multiDivert); + if (diverts == null) { + return null; + } + + if (diverts.size() != 1) { + return null; + } + + ParsedObject singleDivert = diverts.get(0); + if (singleDivert instanceof com.bladecoder.ink.compiler.ParsedHierarchy.TunnelOnwards) { + return null; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert = + singleDivert instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert + ? (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) singleDivert + : null; + if (divert != null && divert.isTunnel) { + return null; + } + + return divert; + } + + private List dotSeparatedDivertPathComponents() { + return interleave(spaced(this::identifierWithMetadata), exclude(stringRule(".")), null, true); + } + + private Object parseDivertArrowOrTunnelOnwards() { + int numArrows = 0; + while (parseString("->") != null) { + numArrows++; + } + + if (numArrows == 0) { + return null; + } else if (numArrows == 1) { + return "->"; + } else if (numArrows == 2) { + return "->->"; + } else { + error("Unexpected number of arrows in divert. Should only have '->' or '->->'", false); + return "->->"; + } + } + + public static final CharacterRange LatinBasic = + CharacterRange.define('\u0041', '\u007A', new CharacterSet().addRange('\u005B', '\u0060')); + public static final CharacterRange LatinExtendedA = CharacterRange.define('\u0100', '\u017F'); + public static final CharacterRange LatinExtendedB = CharacterRange.define('\u0180', '\u024F'); + public static final CharacterRange Greek = CharacterRange.define( + '\u0370', + '\u03FF', + new CharacterSet() + .addRange('\u0378', '\u0385') + .addCharacters("\u0374\u0375\u0378\u0387\u038B\u038D\u03A2")); + public static final CharacterRange Cyrillic = + CharacterRange.define('\u0400', '\u04FF', new CharacterSet().addRange('\u0482', '\u0489')); + public static final CharacterRange Armenian = CharacterRange.define( + '\u0530', + '\u058F', + new CharacterSet() + .addCharacters("\u0530") + .addRange('\u0557', '\u0560') + .addRange('\u0588', '\u058E')); + public static final CharacterRange Hebrew = CharacterRange.define('\u0590', '\u05FF'); + public static final CharacterRange Arabic = CharacterRange.define('\u0600', '\u06FF'); + public static final CharacterRange Korean = CharacterRange.define('\uAC00', '\uD7AF'); + public static final CharacterRange Latin1Supplement = CharacterRange.define('\u0080', '\u00FF'); + public static final CharacterRange CJKUnifiedIdeographs = CharacterRange.define('\u4E00', '\u9FFF'); + public static final CharacterRange Hiragana = CharacterRange.define('\u3041', '\u3096'); + public static final CharacterRange Katakana = CharacterRange.define('\u30A0', '\u30FC'); + + public static CharacterRange[] listAllCharacterRanges() { + return new CharacterRange[] { + LatinBasic, + LatinExtendedA, + LatinExtendedB, + Arabic, + Armenian, + Cyrillic, + Greek, + Hebrew, + Korean, + Latin1Supplement, + CJKUnifiedIdeographs, + Hiragana, + Katakana, + }; + } + + private void extendIdentifierCharacterRanges(CharacterSet identifierCharSet) { + for (CharacterRange range : listAllCharacterRanges()) { + identifierCharSet.addCharacters(range.toCharacterSet()); + } + } + + private CharacterSet getIdentifierCharSet() { + if (identifierCharSet == null) { + identifierCharSet = new CharacterSet() + .addRange('A', 'Z') + .addRange('a', 'z') + .addRange('0', '9') + .addCharacters("_"); + extendIdentifierCharacterRanges(identifierCharSet); + } + return identifierCharSet; + } + + public CommandLineInput CommandLineUserInput() { + CommandLineInput result = new CommandLineInput(); + + whitespace(); + + if (parseString("help") != null) { + result.isHelp = true; + return result; + } + + if (parseString("exit") != null || parseString("quit") != null) { + result.isExit = true; + return result; + } + + return (CommandLineInput) oneOf( + this::debugSource, this::debugPathLookup, this::userChoiceNumber, this::userImmediateModeStatement); + } + + private CommandLineInput debugSource() { + whitespace(); + + if (parseString("DebugSource") == null) { + return null; + } + + whitespace(); + + String expectMsg = "character offset in parentheses, e.g. DebugSource(5)"; + if (expect(stringRule("("), expectMsg, null) == null) { + return null; + } + + whitespace(); + + Integer characterOffset = parseInt(); + if (characterOffset == null) { + error(expectMsg, false); + return null; + } + + whitespace(); + + expect(stringRule(")"), "closing parenthesis", null); + + CommandLineInput inputStruct = new CommandLineInput(); + inputStruct.debugSource = characterOffset; + return inputStruct; + } + + private CommandLineInput debugPathLookup() { + whitespace(); + + if (parseString("DebugPath") == null) { + return null; + } + + if (whitespace() == null) { + return null; + } + + String pathStr = (String) expect(this::runtimePath, "path", null); + + CommandLineInput inputStruct = new CommandLineInput(); + inputStruct.debugPathLookup = pathStr; + return inputStruct; + } + + private String runtimePath() { + if (runtimePathCharacterSet == null) { + runtimePathCharacterSet = new CharacterSet(getIdentifierCharSet()); + runtimePathCharacterSet.addCharacters("-"); + runtimePathCharacterSet.addCharacters("."); + } + + return parseCharactersFromCharSet(runtimePathCharacterSet); + } + + private CommandLineInput userChoiceNumber() { + whitespace(); + + Integer number = parseInt(); + if (number == null) { + return null; + } + + whitespace(); + + if (parse(this::endOfLine) == null) { + return null; + } + + CommandLineInput inputStruct = new CommandLineInput(); + inputStruct.choiceInput = number; + return inputStruct; + } + + private CommandLineInput userImmediateModeStatement() { + Object statement = oneOf(this::singleDivert, this::tempDeclarationOrAssignment, this::expression); + + CommandLineInput inputStruct = new CommandLineInput(); + inputStruct.userImmediateModeStatement = statement; + return inputStruct; + } + + private final CharacterSet inlineWhitespaceChars = new CharacterSet(" \t"); + private final java.util.Map> statementRulesAtLevel = + new java.util.EnumMap<>(StatementLevel.class); + private final java.util.Map> statementBreakRulesAtLevel = + new java.util.EnumMap<>(StatementLevel.class); + private CharacterSet nonTextPauseCharacters; + private CharacterSet nonTextEndCharacters; + private CharacterSet notTextEndCharactersChoice; + private CharacterSet notTextEndCharactersString; + private CharacterSet sequenceTypeSymbols; + private CharacterSet identifierCharSet; + private CharacterSet runtimePathCharacterSet; + private boolean parsingChoice; + private List binaryOperators = new ArrayList<>(); + private int maxBinaryOpLength; + + private IFileHandler fileHandler; + private com.bladecoder.ink.runtime.Error.ErrorHandler externalErrorHandler; + private String filename; + + private InkParser rootParserRef; + private Set openFilenames; + private List basicParsedContent; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/InkStringConversionExtensions.java b/compiler/src/main/java/com/bladecoder/ink/compiler/InkStringConversionExtensions.java new file mode 100644 index 0000000..9a01c81 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/InkStringConversionExtensions.java @@ -0,0 +1,18 @@ +package com.bladecoder.ink.compiler; + +import java.util.List; + +public final class InkStringConversionExtensions { + private InkStringConversionExtensions() {} + + public static String[] toStringsArray(List list) { + int count = list.size(); + String[] strings = new String[count]; + + for (int i = 0; i < count; i++) { + strings[i] = list.get(i).toString(); + } + + return strings; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/AuthorWarning.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/AuthorWarning.java new file mode 100644 index 0000000..101a445 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/AuthorWarning.java @@ -0,0 +1,15 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class AuthorWarning extends ParsedObject { + public String warningMessage; + + public AuthorWarning(String message) { + warningMessage = message; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + warning(warningMessage); + return null; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/BinaryExpression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/BinaryExpression.java new file mode 100644 index 0000000..c3b747b --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/BinaryExpression.java @@ -0,0 +1,67 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.NativeFunctionCall; + +public class BinaryExpression extends Expression { + public Expression leftExpression; + public Expression rightExpression; + public String opName; + + public BinaryExpression(Expression left, Expression right, String opName) { + leftExpression = addContent(left); + rightExpression = addContent(right); + this.opName = opName; + } + + @Override + public void generateIntoContainer(Container container) { + leftExpression.generateIntoContainer(container); + rightExpression.generateIntoContainer(container); + + opName = nativeNameForOp(opName); + + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName(opName)); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (nativeNameForOp(opName).equals("?")) { + UnaryExpression leftUnary = + leftExpression instanceof UnaryExpression ? (UnaryExpression) leftExpression : null; + if (leftUnary != null && (leftUnary.op.equals("not") || leftUnary.op.equals("!"))) { + error( + "Using 'not' or '!' here negates '" + leftUnary.innerExpression + + "' rather than the result of the '?' or 'has' operator. You need to add parentheses around the (A ? B) expression."); + } + } + } + + private String nativeNameForOp(String opName) { + if (opName.equals("and")) { + return "&&"; + } + if (opName.equals("or")) { + return "||"; + } + if (opName.equals("mod")) { + return "%"; + } + if (opName.equals("has")) { + return "?"; + } + if (opName.equals("hasnt")) { + return "!?"; + } + + return opName; + } + + @Override + public String toString() { + return String.format("(%s %s %s)", leftExpression, opName, rightExpression); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Choice.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Choice.java new file mode 100644 index 0000000..7459c93 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Choice.java @@ -0,0 +1,251 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.Divert; +import com.bladecoder.ink.runtime.DivertTargetValue; +import com.bladecoder.ink.runtime.RTObject; +import com.bladecoder.ink.runtime.VariableAssignment; + +public class Choice extends ParsedObject implements IWeavePoint, INamedContent { + public ContentList startContent; + public ContentList choiceOnlyContent; + public ContentList innerContent; + + public Identifier identifier; + + private Expression condition; + + public boolean onceOnly; + public boolean isInvisibleDefault; + + public int indentationDepth; + public boolean hasWeaveStyleInlineBrackets; + + private com.bladecoder.ink.runtime.ChoicePoint runtimeChoice; + private Container innerContentContainer; + private Container outerContainer; + private Container startContentRuntimeContainer; + private Divert divertToStartContentOuter; + private Divert divertToStartContentInner; + private Container r1Label; + private Container r2Label; + private DivertTargetValue returnToR1; + private DivertTargetValue returnToR2; + + public Choice(ContentList startContent, ContentList choiceOnlyContent, ContentList innerContent) { + this.startContent = startContent; + this.choiceOnlyContent = choiceOnlyContent; + this.innerContent = innerContent; + this.indentationDepth = 1; + + if (startContent != null) { + addContent(this.startContent); + } + + if (choiceOnlyContent != null) { + addContent(this.choiceOnlyContent); + } + + if (innerContent != null) { + addContent(this.innerContent); + } + + this.onceOnly = true; + } + + public Expression getCondition() { + return condition; + } + + public void setCondition(Expression value) { + condition = value; + if (condition != null) { + addContent(condition); + } + } + + @Override + public String getName() { + return identifier != null ? identifier.name : null; + } + + @Override + public Identifier getIdentifier() { + return identifier; + } + + @Override + public int getIndentationDepth() { + return indentationDepth; + } + + @Override + public Container getRuntimeContainer() { + return innerContentContainer; + } + + @Override + public Container getContainerForCounting() { + if (innerContentContainer != null) { + return innerContentContainer; + } + return super.getContainerForCounting(); + } + + @Override + public com.bladecoder.ink.runtime.Path getRuntimePath() { + if (innerContentContainer != null) { + return innerContentContainer.getPath(); + } + return super.getRuntimePath(); + } + + public Container getInnerContentContainer() { + return innerContentContainer; + } + + @Override + public RTObject generateRuntimeObject() { + outerContainer = new Container(); + + runtimeChoice = new com.bladecoder.ink.runtime.ChoicePoint(onceOnly); + runtimeChoice.setIsInvisibleDefault(isInvisibleDefault); + + if (startContent != null || choiceOnlyContent != null || condition != null) { + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.EvalStart); + } + + if (startContent != null) { + returnToR1 = new DivertTargetValue(); + RuntimeUtils.addContent(outerContainer, returnToR1); + try { + VariableAssignment varAssign = new VariableAssignment("$r", true); + RuntimeUtils.addContent(outerContainer, varAssign); + } catch (Exception e) { + throw new RuntimeException(e); + } + + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.BeginString); + + divertToStartContentOuter = new Divert(); + RuntimeUtils.addContent(outerContainer, divertToStartContentOuter); + + startContentRuntimeContainer = (Container) startContent.getRuntimeObject(); + startContentRuntimeContainer.setName("s"); + + Divert varDivert = new Divert(); + varDivert.setVariableDivertName("$r"); + RuntimeUtils.addContent(startContentRuntimeContainer, varDivert); + + outerContainer.addToNamedContentOnly(startContentRuntimeContainer); + + r1Label = new Container(); + r1Label.setName("$r1"); + RuntimeUtils.addContent(outerContainer, r1Label); + + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.EndString); + + runtimeChoice.setHasStartContent(true); + } + + if (choiceOnlyContent != null) { + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.BeginString); + + Container choiceOnlyRuntimeContent = (Container) choiceOnlyContent.getRuntimeObject(); + outerContainer.addContentsOfContainer(choiceOnlyRuntimeContent); + + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.EndString); + + runtimeChoice.setHasChoiceOnlyContent(true); + } + + if (condition != null) { + condition.generateIntoContainer(outerContainer); + runtimeChoice.setHasCondition(true); + } + + if (startContent != null || choiceOnlyContent != null || condition != null) { + RuntimeUtils.addContent(outerContainer, ControlCommand.CommandType.EvalEnd); + } + + RuntimeUtils.addContent(outerContainer, runtimeChoice); + + innerContentContainer = new Container(); + + if (startContent != null) { + returnToR2 = new DivertTargetValue(); + RuntimeUtils.addContent(innerContentContainer, ControlCommand.CommandType.EvalStart); + RuntimeUtils.addContent(innerContentContainer, returnToR2); + RuntimeUtils.addContent(innerContentContainer, ControlCommand.CommandType.EvalEnd); + try { + VariableAssignment varAssign = new VariableAssignment("$r", true); + RuntimeUtils.addContent(innerContentContainer, varAssign); + } catch (Exception e) { + throw new RuntimeException(e); + } + + divertToStartContentInner = new Divert(); + RuntimeUtils.addContent(innerContentContainer, divertToStartContentInner); + + r2Label = new Container(); + r2Label.setName("$r2"); + RuntimeUtils.addContent(innerContentContainer, r2Label); + } + + if (innerContent != null) { + Container innerChoiceOnlyContent = (Container) innerContent.getRuntimeObject(); + innerContentContainer.addContentsOfContainer(innerChoiceOnlyContent); + } + + if (getStory().countAllVisits) { + innerContentContainer.setVisitsShouldBeCounted(true); + } + + innerContentContainer.setCountingAtStartOnly(true); + + return outerContainer; + } + + @Override + public void resolveReferences(Story context) { + if (innerContentContainer != null) { + runtimeChoice.setPathOnChoice(innerContentContainer.getPath()); + + if (onceOnly) { + innerContentContainer.setVisitsShouldBeCounted(true); + } + } + + if (returnToR1 != null) { + returnToR1.setTargetPath(r1Label.getPath()); + } + + if (returnToR2 != null) { + returnToR2.setTargetPath(r2Label.getPath()); + } + + if (divertToStartContentOuter != null) { + divertToStartContentOuter.setTargetPath(startContentRuntimeContainer.getPath()); + } + + if (divertToStartContentInner != null) { + divertToStartContentInner.setTargetPath(startContentRuntimeContainer.getPath()); + } + + super.resolveReferences(context); + + if (identifier != null && identifier.name.length() > 0) { + context.checkForNamingCollisions(this, identifier, Story.SymbolType.SubFlowAndWeave, null); + } + } + + @Override + public String toString() { + if (choiceOnlyContent != null) { + return String.format("* %s[%s]...", startContent, choiceOnlyContent); + } + return String.format("* %s...", startContent); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Conditional.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Conditional.java new file mode 100644 index 0000000..5683419 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Conditional.java @@ -0,0 +1,67 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import java.util.ArrayList; +import java.util.List; + +public class Conditional extends ParsedObject { + private Expression initialCondition; + private List branches; + private ControlCommand reJoinTarget; + + public Conditional(Expression condition, List branches) { + this.initialCondition = condition; + if (this.initialCondition != null) { + addContent(condition); + } + + this.branches = branches; + if (this.branches != null) { + addContent(new ArrayList(this.branches)); + } + } + + public Expression getInitialCondition() { + return initialCondition; + } + + public List getBranches() { + return branches; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Container container = new Container(); + + if (initialCondition != null) { + RuntimeUtils.addContent(container, initialCondition.getRuntimeObject()); + } + + for (ConditionalSingleBranch branch : branches) { + Container branchContainer = (Container) branch.getRuntimeObject(); + RuntimeUtils.addContent(container, branchContainer); + } + + if (initialCondition != null + && branches.get(0).getOwnExpression() != null + && !branches.get(branches.size() - 1).isElse) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.PopEvaluatedValue); + } + + reJoinTarget = new ControlCommand(ControlCommand.CommandType.NoOp); + RuntimeUtils.addContent(container, reJoinTarget); + + return container; + } + + @Override + public void resolveReferences(Story context) { + for (ConditionalSingleBranch branch : branches) { + branch.getReturnDivert().setTargetPath(reJoinTarget.getPath()); + } + + super.resolveReferences(context); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConditionalSingleBranch.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConditionalSingleBranch.java new file mode 100644 index 0000000..e512f07 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConditionalSingleBranch.java @@ -0,0 +1,133 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.Divert; +import com.bladecoder.ink.runtime.NativeFunctionCall; +import com.bladecoder.ink.runtime.StringValue; +import java.util.List; + +public class ConditionalSingleBranch extends ParsedObject { + public boolean isTrueBranch; + private Expression ownExpression; + public boolean matchingEquality; + public boolean isElse; + public boolean isInline; + + public Divert returnDivert; + + public ConditionalSingleBranch(List content) { + if (content != null) { + innerWeave = new Weave(content); + addContent(innerWeave); + } + } + + public Expression getOwnExpression() { + return ownExpression; + } + + public Divert getReturnDivert() { + return returnDivert; + } + + public void setOwnExpression(Expression value) { + ownExpression = value; + if (ownExpression != null) { + addContent(ownExpression); + } + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + if (innerWeave != null && innerWeave.getContent() != null) { + for (ParsedObject obj : innerWeave.getContent()) { + Text text = obj instanceof Text ? (Text) obj : null; + if (text != null) { + if (text.getText().startsWith("else:")) { + warning( + "Saw the text 'else:' which is being treated as content. Did you mean '- else:'?", + text); + } + } + } + } + + Container container = new Container(); + + boolean duplicatesStackValue = matchingEquality && !isElse; + if (duplicatesStackValue) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.Duplicate); + } + + conditionalDivert = new Divert(); + conditionalDivert.setConditional(!isElse); + + if (!isTrueBranch && !isElse) { + boolean needsEval = ownExpression != null; + if (needsEval) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + } + + if (ownExpression != null) { + ownExpression.generateIntoContainer(container); + } + + if (matchingEquality) { + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("==")); + } + + if (needsEval) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + } + } + + RuntimeUtils.addContent(container, conditionalDivert); + + contentContainer = generateRuntimeForContent(); + contentContainer.setName("b"); + + if (!isInline) { + try { + contentContainer.insertContent(new StringValue("\n"), 0); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + if (duplicatesStackValue || (isElse && matchingEquality)) { + try { + contentContainer.insertContent(new ControlCommand(ControlCommand.CommandType.PopEvaluatedValue), 0); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + container.addToNamedContentOnly(contentContainer); + + returnDivert = new Divert(); + RuntimeUtils.addContent(contentContainer, returnDivert); + + return container; + } + + public Container generateRuntimeForContent() { + if (innerWeave == null) { + return new Container(); + } + innerWeave.getRuntimeObject(); + return innerWeave.rootContainer; + } + + @Override + public void resolveReferences(Story context) { + conditionalDivert.setTargetPath(contentContainer.getPath()); + + super.resolveReferences(context); + } + + private Container contentContainer; + private Divert conditionalDivert; + private Weave innerWeave; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConstantDeclaration.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConstantDeclaration.java new file mode 100644 index 0000000..b76da97 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ConstantDeclaration.java @@ -0,0 +1,35 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class ConstantDeclaration extends ParsedObject { + public Identifier constantIdentifier; + public Expression expression; + + public ConstantDeclaration(Identifier name, Expression assignedExpression) { + constantIdentifier = name; + + if (assignedExpression != null) { + expression = addContent(assignedExpression); + } + } + + public String getConstantName() { + return constantIdentifier != null ? constantIdentifier.name : null; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + return null; + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + context.checkForNamingCollisions(this, constantIdentifier, Story.SymbolType.Var, null); + } + + @Override + public String getTypeName() { + return "Constant"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ContentList.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ContentList.java new file mode 100644 index 0000000..c83560f --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ContentList.java @@ -0,0 +1,90 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.InkStringConversionExtensions; +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import java.util.ArrayList; +import java.util.List; + +public class ContentList extends ParsedObject { + private boolean dontFlatten; + + public Container getRuntimeContainer() { + return (Container) getRuntimeObject(); + } + + public ContentList(List objects) { + if (objects != null) { + addContent(objects); + } + } + + public ContentList(ParsedObject... objects) { + if (objects != null) { + List objList = new ArrayList<>(); + for (ParsedObject obj : objects) { + objList.add(obj); + } + addContent(objList); + } + } + + public ContentList() {} + + public boolean isDontFlatten() { + return dontFlatten; + } + + public void setDontFlatten(boolean value) { + dontFlatten = value; + } + + public void trimTrailingWhitespace() { + if (content == null) { + return; + } + for (int i = content.size() - 1; i >= 0; --i) { + Text text = content.get(i) instanceof Text ? (Text) content.get(i) : null; + if (text == null) { + break; + } + + text.setText(text.getText().replaceAll("[ \t]+$", "")); + if (text.getText().isEmpty()) { + content.remove(i); + } else { + break; + } + } + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Container container = new Container(); + if (content != null) { + for (ParsedObject obj : content) { + com.bladecoder.ink.runtime.RTObject contentObjRuntime = obj.getRuntimeObject(); + if (contentObjRuntime != null) { + RuntimeUtils.addContent(container, contentObjRuntime); + } + } + } + + if (dontFlatten) { + getStory().dontFlattenContainer(container); + } + + return container; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ContentList("); + if (content != null) { + sb.append(String.join(", ", InkStringConversionExtensions.toStringsArray(content))); + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Divert.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Divert.java new file mode 100644 index 0000000..57e0234 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Divert.java @@ -0,0 +1,346 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.PushPopType; +import com.bladecoder.ink.runtime.VariablePointerValue; +import java.util.ArrayList; +import java.util.List; + +public class Divert extends ParsedObject { + public Path target; + public ParsedObject targetContent; + public List arguments; + public com.bladecoder.ink.runtime.Divert runtimeDivert; + public boolean isFunctionCall; + public boolean isEmpty; + public boolean isTunnel; + public boolean isThread; + + public Divert(Path target, List arguments) { + this.target = target; + this.arguments = arguments; + + if (arguments != null) { + List argObjects = new ArrayList<>(); + argObjects.addAll(arguments); + addContent(argObjects); + } + } + + public Divert(Path target) { + this(target, null); + } + + public Divert(ParsedObject targetContent) { + this.targetContent = targetContent; + } + + public boolean isEnd() { + return target != null && "END".equals(target.getDotSeparatedComponents()); + } + + public boolean isDone() { + return target != null && "DONE".equals(target.getDotSeparatedComponents()); + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + if (isEnd()) { + return new com.bladecoder.ink.runtime.ControlCommand(ControlCommand.CommandType.End); + } + if (isDone()) { + return new com.bladecoder.ink.runtime.ControlCommand(ControlCommand.CommandType.Done); + } + + runtimeDivert = new com.bladecoder.ink.runtime.Divert(); + + resolveTargetContent(); + + checkArgumentValidity(); + + boolean requiresArgCodeGen = arguments != null && !arguments.isEmpty(); + if (requiresArgCodeGen || isFunctionCall || isTunnel || isThread) { + Container container = new Container(); + + if (requiresArgCodeGen) { + if (!isFunctionCall) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + } + + List targetArguments = null; + if (targetContent != null && targetContent instanceof FlowBase) { + targetArguments = ((FlowBase) targetContent).getArguments(); + } + + for (int i = 0; i < arguments.size(); ++i) { + Expression argToPass = arguments.get(i); + FlowBase.Argument argExpected = null; + if (targetArguments != null && i < targetArguments.size()) { + argExpected = targetArguments.get(i); + } + + if (argExpected != null && argExpected.isByReference) { + VariableReference varRef = + argToPass instanceof VariableReference ? (VariableReference) argToPass : null; + if (varRef == null) { + error("Expected variable name to pass by reference to 'ref " + argExpected.identifier + + "' but saw " + argToPass); + break; + } + + Path targetPath = new Path(varRef.pathIdentifiers); + ParsedObject targetForCount = targetPath.resolveFromContext(this); + if (targetForCount != null) { + error("can't pass a read count by reference. '" + targetPath.getDotSeparatedComponents() + + "' is a knot/stitch/label, but '" + target.getDotSeparatedComponents() + + "' requires the name of a VAR to be passed."); + break; + } + + VariablePointerValue varPointer = new VariablePointerValue(varRef.getName()); + RuntimeUtils.addContent(container, varPointer); + } else { + argToPass.generateIntoContainer(container); + } + } + + if (!isFunctionCall) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + } + } + + if (isThread) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.StartThread); + } else if (isFunctionCall || isTunnel) { + runtimeDivert.setPushesToStack(true); + runtimeDivert.setStackPushType(isFunctionCall ? PushPopType.Function : PushPopType.Tunnel); + } + + RuntimeUtils.addContent(container, runtimeDivert); + + return container; + } + + return runtimeDivert; + } + + public String pathAsVariableName() { + return target.getFirstComponent(); + } + + private void resolveTargetContent() { + if (isEmpty || isEnd()) { + return; + } + + if (targetContent == null) { + String variableTargetName = pathAsVariableName(); + if (variableTargetName != null) { + FlowBase flowBaseScope = closestFlowBase(); + FlowBase.VariableResolveResult resolveResult = + flowBaseScope.resolveVariableWithName(variableTargetName, this); + if (resolveResult.found) { + if (resolveResult.isArgument) { + for (FlowBase.Argument arg : resolveResult.ownerFlow.getArguments()) { + if (arg.identifier != null && variableTargetName.equals(arg.identifier.name)) { + if (!arg.isDivertTarget) { + error( + "Since '" + arg.identifier + "' is used as a variable divert target (on " + + getDebugMetadata() + "), it should be marked as: -> " + + arg.identifier, + resolveResult.ownerFlow, + false); + } + break; + } + } + } + + runtimeDivert.setVariableDivertName(variableTargetName); + return; + } + } + + targetContent = target.resolveFromContext(this); + } + } + + @Override + public void resolveReferences(Story context) { + if (isEmpty || isEnd() || isDone()) { + return; + } + + if (targetContent != null) { + runtimeDivert.setTargetPath(targetContent.getRuntimePath()); + } + + super.resolveReferences(context); + + FlowBase targetFlow = targetContent instanceof FlowBase ? (FlowBase) targetContent : null; + if (targetFlow != null) { + if (!targetFlow.isFunction() && isFunctionCall) { + error(targetFlow.getIdentifier() + + " hasn't been marked as a function, but it's being called as one. Do you need to declare the knot as '== function " + + targetFlow.getIdentifier() + " =='?"); + } else if (targetFlow.isFunction() && !isFunctionCall && !(parent instanceof DivertTarget)) { + error(targetFlow.getIdentifier() + + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + + targetFlow.getIdentifier() + "(...)'"); + } + } + + boolean targetWasFound = targetContent != null; + boolean isBuiltIn = false; + boolean isExternal = false; + + if (target.getNumberOfComponents() == 1) { + isBuiltIn = FunctionCall.isBuiltIn(target.getFirstComponent()); + isExternal = context.isExternal(target.getFirstComponent()); + + if (isBuiltIn || isExternal) { + if (!isFunctionCall) { + error(target.getFirstComponent() + " must be called as a function: ~ " + target.getFirstComponent() + + "()"); + } + if (isExternal) { + runtimeDivert.setExternal(true); + if (arguments != null) { + runtimeDivert.setExternalArgs(arguments.size()); + } + runtimeDivert.setPushesToStack(false); + runtimeDivert.setTargetPath(new com.bladecoder.ink.runtime.Path(target.getFirstComponent())); + checkExternalArgumentValidity(context); + } + return; + } + } + + if (runtimeDivert.getVariableDivertName() != null) { + return; + } + + if (!targetWasFound && !isBuiltIn && !isExternal) { + error("target not found: '" + target + "'"); + } + } + + private void checkArgumentValidity() { + if (isEmpty) { + return; + } + + int numArgs = 0; + if (arguments != null && !arguments.isEmpty()) { + numArgs = arguments.size(); + } + + if (targetContent == null) { + return; + } + + FlowBase targetFlow = targetContent instanceof FlowBase ? (FlowBase) targetContent : null; + + if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters())) { + return; + } + + if (targetFlow == null && numArgs > 0) { + error("target needs to be a knot or stitch in order to pass arguments"); + return; + } + + if (targetFlow.getArguments() == null && numArgs > 0) { + error("target (" + targetFlow.getName() + ") doesn't take parameters"); + return; + } + + if (parent instanceof DivertTarget) { + if (numArgs > 0) { + error("can't store arguments in a divert target variable"); + } + return; + } + + int paramCount = targetFlow.getArguments().size(); + if (paramCount != numArgs) { + String butClause; + if (numArgs == 0) { + butClause = "but there weren't any passed to it"; + } else if (numArgs < paramCount) { + butClause = "but only got " + numArgs; + } else { + butClause = "but got " + numArgs; + } + error("to '" + targetFlow.getIdentifier() + "' requires " + paramCount + " arguments, " + butClause); + return; + } + + for (int i = 0; i < paramCount; ++i) { + FlowBase.Argument flowArg = targetFlow.getArguments().get(i); + Expression divArgExpr = arguments.get(i); + + if (flowArg.isDivertTarget) { + VariableReference varRef = + divArgExpr instanceof VariableReference ? (VariableReference) divArgExpr : null; + if (!(divArgExpr instanceof DivertTarget) && varRef == null) { + error( + "Target '" + targetFlow.getIdentifier() + + "' expects a divert target for the parameter named -> " + flowArg.identifier + + " but saw " + + divArgExpr, + divArgExpr, + false); + } else if (varRef != null) { + Path knotCountPath = new Path(varRef.pathIdentifiers); + ParsedObject targetForCount = knotCountPath.resolveFromContext(varRef); + if (targetForCount != null) { + error("Passing read count of '" + knotCountPath.getDotSeparatedComponents() + + "' instead of a divert target. You probably meant '" + knotCountPath + "'"); + } + } + } + } + + if (targetFlow == null) { + error("Can't call as a function or with arguments unless it's a knot or stitch"); + } + } + + private void checkExternalArgumentValidity(Story context) { + String externalName = target.getFirstComponent(); + ExternalDeclaration external = context.externals.get(externalName); + + int externalArgCount = external.argumentNames.size(); + int ownArgCount = arguments != null ? arguments.size() : 0; + + if (ownArgCount != externalArgCount) { + error("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + + externalArgCount + " but got " + ownArgCount); + } + } + + @Override + public void error(String message, ParsedObject source, boolean isWarning) { + if (source != this && source != null) { + super.error(message, source, isWarning); + return; + } + + if (isFunctionCall) { + super.error("Function call " + message, source, isWarning); + } else { + super.error("Divert " + message, source, isWarning); + } + } + + @Override + public String toString() { + if (target != null) { + return target.toString(); + } + return "-> "; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/DivertTarget.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/DivertTarget.java new file mode 100644 index 0000000..12a864f --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/DivertTarget.java @@ -0,0 +1,152 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.DivertTargetValue; + +public class DivertTarget extends Expression { + public Divert divert; + + private DivertTargetValue runtimeDivertTargetValue; + private com.bladecoder.ink.runtime.Divert runtimeDivert; + + public DivertTarget(Divert divert) { + this.divert = addContent(divert); + } + + @Override + public void generateIntoContainer(Container container) { + divert.generateRuntimeObject(); + + runtimeDivert = divert.runtimeDivert; + runtimeDivertTargetValue = new DivertTargetValue(); + + RuntimeUtils.addContent(container, runtimeDivertTargetValue); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (divert.isDone() || divert.isEnd()) { + error("Can't use -> DONE or -> END as variable divert targets", this, false); + return; + } + + ParsedObject usageContext = this; + while (usageContext != null && usageContext instanceof Expression) { + boolean badUsage = false; + boolean foundUsage = false; + + ParsedObject usageParent = usageContext.parent; + if (usageParent instanceof BinaryExpression) { + BinaryExpression binaryExprParent = (BinaryExpression) usageParent; + if (!binaryExprParent.opName.equals("==") && !binaryExprParent.opName.equals("!=")) { + badUsage = true; + } else { + if (!(binaryExprParent.leftExpression instanceof DivertTarget + || binaryExprParent.leftExpression instanceof VariableReference)) { + badUsage = true; + } + if (!(binaryExprParent.rightExpression instanceof DivertTarget + || binaryExprParent.rightExpression instanceof VariableReference)) { + badUsage = true; + } + } + foundUsage = true; + } else if (usageParent instanceof FunctionCall) { + FunctionCall funcCall = (FunctionCall) usageParent; + if (!funcCall.isTurnsSince() && !funcCall.isReadCount()) { + badUsage = true; + } + foundUsage = true; + } else if (usageParent instanceof Expression) { + badUsage = true; + foundUsage = true; + } else if (usageParent instanceof MultipleConditionExpression) { + badUsage = true; + foundUsage = true; + } else if (usageParent instanceof Choice && ((Choice) usageParent).getCondition() == usageContext) { + badUsage = true; + foundUsage = true; + } else if (usageParent instanceof Conditional || usageParent instanceof ConditionalSingleBranch) { + badUsage = true; + foundUsage = true; + } + + if (badUsage) { + error( + "Can't use a divert target like that. Did you intend to call '" + divert.target + + "' as a function: likeThis(), or check the read count: likeThis, with no arrows?", + this, + false); + } + + if (foundUsage) { + break; + } + + usageContext = usageParent; + } + + if (runtimeDivert.hasVariableTarget()) { + error( + "Since '" + divert.target.getDotSeparatedComponents() + + "' is a variable, it shouldn't be preceded by '->' here.", + this, + false); + } + + try { + runtimeDivertTargetValue.setTargetPath(runtimeDivert.getTargetPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + ParsedObject targetContent = divert.targetContent; + if (targetContent != null) { + com.bladecoder.ink.runtime.Container target = targetContent.getContainerForCounting(); + if (target != null) { + FunctionCall parentFunc = parent instanceof FunctionCall ? (FunctionCall) parent : null; + if (parentFunc != null && parentFunc.isTurnsSince()) { + target.setTurnIndexShouldBeCounted(true); + } else { + target.setVisitsShouldBeCounted(true); + target.setTurnIndexShouldBeCounted(true); + } + } + + FlowBase targetFlow = targetContent instanceof FlowBase ? (FlowBase) targetContent : null; + if (targetFlow != null && targetFlow.getArguments() != null) { + for (FlowBase.Argument arg : targetFlow.getArguments()) { + if (arg.isByReference) { + error( + "Can't store a divert target to a knot or function that has by-reference arguments ('" + + targetFlow.getIdentifier() + "' has 'ref " + arg.identifier + "').", + this, + false); + } + } + } + } + } + + @Override + public boolean equals(java.lang.Object obj) { + DivertTarget otherDivTarget = obj instanceof DivertTarget ? (DivertTarget) obj : null; + if (otherDivTarget == null) { + return false; + } + + String targetStr = divert.target.getDotSeparatedComponents(); + String otherTargetStr = otherDivTarget.divert.target.getDotSeparatedComponents(); + + return targetStr.equals(otherTargetStr); + } + + @Override + public int hashCode() { + String targetStr = divert.target.getDotSeparatedComponents(); + return targetStr.hashCode(); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Expression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Expression.java new file mode 100644 index 0000000..d0b1257 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Expression.java @@ -0,0 +1,52 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; + +public abstract class Expression extends ParsedObject { + private boolean outputWhenComplete; + private Container prototypeRuntimeConstantExpression; + + public boolean isOutputWhenComplete() { + return outputWhenComplete; + } + + public void setOutputWhenComplete(boolean value) { + outputWhenComplete = value; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Container container = new Container(); + + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + + generateIntoContainer(container); + + if (outputWhenComplete) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalOutput); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + + return container; + } + + public void generateConstantIntoContainer(Container container) { + if (prototypeRuntimeConstantExpression == null) { + prototypeRuntimeConstantExpression = new Container(); + generateIntoContainer(prototypeRuntimeConstantExpression); + } + + for (com.bladecoder.ink.runtime.RTObject runtimeObj : prototypeRuntimeConstantExpression.getContent()) { + try { + RuntimeUtils.addContent(container, runtimeObj.copy()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public abstract void generateIntoContainer(Container container); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ExternalDeclaration.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ExternalDeclaration.java new file mode 100644 index 0000000..8e11cac --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ExternalDeclaration.java @@ -0,0 +1,24 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import java.util.List; + +public class ExternalDeclaration extends ParsedObject implements INamedContent { + public Identifier identifier; + public List argumentNames; + + public ExternalDeclaration(Identifier identifier, List argumentNames) { + this.identifier = identifier; + this.argumentNames = argumentNames; + } + + @Override + public String getName() { + return identifier != null ? identifier.name : null; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + getStory().addExternal(this); + return null; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowBase.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowBase.java new file mode 100644 index 0000000..7e4a734 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowBase.java @@ -0,0 +1,434 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.DebugMetadata; +import com.bladecoder.ink.runtime.Divert; +import com.bladecoder.ink.runtime.INamedContent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Base class for Knots and Stitches +public abstract class FlowBase extends ParsedObject implements INamedContent { + public static class Argument { + public Identifier identifier; + public boolean isByReference; + public boolean isDivertTarget; + } + + protected Identifier identifier; + protected List arguments; + protected boolean isFunction; + protected Map variableDeclarations; + + private Weave rootWeave; + private Map subFlowsByName; + private Divert startingSubFlowDivert; + private com.bladecoder.ink.runtime.RTObject startingSubFlowRuntime; + private FlowBase firstChildFlow; + + public abstract FlowLevel getFlowLevel(); + + @Override + public String getName() { + return identifier != null ? identifier.name : null; + } + + public Identifier getIdentifier() { + return identifier; + } + + public boolean isFunction() { + return isFunction; + } + + public List getArguments() { + return arguments; + } + + public boolean hasParameters() { + return arguments != null && !arguments.isEmpty(); + } + + protected FlowBase( + Identifier name, + List topLevelObjects, + List arguments, + boolean isFunction, + boolean isIncludedStory) { + this.identifier = name; + + if (topLevelObjects == null) { + topLevelObjects = new ArrayList<>(); + } + + preProcessTopLevelObjects(topLevelObjects); + + topLevelObjects = splitWeaveAndSubFlowContent(topLevelObjects, this instanceof Story && !isIncludedStory); + + addContent(topLevelObjects); + + this.arguments = arguments; + this.isFunction = isFunction; + this.variableDeclarations = new HashMap<>(); + } + + protected FlowBase() { + this(null, null, null, false, false); + } + + private List splitWeaveAndSubFlowContent(List contentObjs, boolean isRootStory) { + List weaveObjs = new ArrayList<>(); + List subFlowObjs = new ArrayList<>(); + + subFlowsByName = new HashMap<>(); + + for (ParsedObject obj : contentObjs) { + FlowBase subFlow = obj instanceof FlowBase ? (FlowBase) obj : null; + if (subFlow != null) { + if (firstChildFlow == null) { + firstChildFlow = subFlow; + } + + subFlowObjs.add(obj); + subFlowsByName.put(subFlow.getIdentifier() != null ? subFlow.getIdentifier().name : null, subFlow); + } else { + weaveObjs.add(obj); + } + } + + if (isRootStory) { + weaveObjs.add(new Gather(null, 1)); + weaveObjs.add(new com.bladecoder.ink.compiler.ParsedHierarchy.Divert(new Path(Identifier.Done))); + } + + List finalContent = new ArrayList<>(); + + if (!weaveObjs.isEmpty()) { + rootWeave = new Weave(weaveObjs, 0); + finalContent.add(rootWeave); + } + + if (!subFlowObjs.isEmpty()) { + finalContent.addAll(subFlowObjs); + } + + return finalContent; + } + + protected void preProcessTopLevelObjects(List topLevelObjects) {} + + public static class VariableResolveResult { + public boolean found; + public boolean isGlobal; + public boolean isArgument; + public boolean isTemporary; + public FlowBase ownerFlow; + } + + public VariableResolveResult resolveVariableWithName(String varName, ParsedObject fromNode) { + VariableResolveResult result = new VariableResolveResult(); + + FlowBase ownerFlow = fromNode == null ? this : fromNode.closestFlowBase(); + + if (ownerFlow.arguments != null) { + for (Argument arg : ownerFlow.arguments) { + if (arg.identifier != null && arg.identifier.name.equals(varName)) { + result.found = true; + result.isArgument = true; + result.ownerFlow = ownerFlow; + return result; + } + } + } + + Story story = getStory(); + if (ownerFlow != story && ownerFlow.variableDeclarations.containsKey(varName)) { + result.found = true; + result.ownerFlow = ownerFlow; + result.isTemporary = true; + return result; + } + + if (story.variableDeclarations.containsKey(varName)) { + result.found = true; + result.ownerFlow = story; + result.isGlobal = true; + return result; + } + + result.found = false; + return result; + } + + public void tryAddNewVariableDeclaration(VariableAssignment varDecl) { + String varName = varDecl.getVariableName(); + if (variableDeclarations.containsKey(varName)) { + String prevDeclError = ""; + DebugMetadata debugMetadata = variableDeclarations.get(varName).getDebugMetadata(); + if (debugMetadata != null) { + prevDeclError = " (" + variableDeclarations.get(varName).getDebugMetadata() + ")"; + } + error( + "found declaration variable '" + varName + "' that was already declared" + prevDeclError, + varDecl, + false); + return; + } + + variableDeclarations.put(varDecl.getVariableName(), varDecl); + } + + public void resolveWeavePointNaming() { + if (rootWeave != null) { + rootWeave.resolveWeavePointNaming(); + } + + if (subFlowsByName != null) { + for (FlowBase subFlow : subFlowsByName.values()) { + subFlow.resolveWeavePointNaming(); + } + } + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Return foundReturn = null; + if (isFunction) { + checkForDisallowedFunctionFlowControl(); + } else if (getFlowLevel() == FlowLevel.Knot || getFlowLevel() == FlowLevel.Stitch) { + foundReturn = find(Return.class); + if (foundReturn != null) { + error( + "Return statements can only be used in knots that are declared as functions: == function " + + this.identifier + " ==", + foundReturn, + false); + } + } + + Container container = new Container(); + container.setName(identifier != null ? identifier.name : null); + + if (getStory().countAllVisits) { + container.setVisitsShouldBeCounted(true); + } + + generateArgumentVariableAssignments(container); + + int contentIdx = 0; + while (content != null && contentIdx < content.size()) { + ParsedObject obj = content.get(contentIdx); + + if (obj instanceof FlowBase) { + FlowBase childFlow = (FlowBase) obj; + + com.bladecoder.ink.runtime.RTObject childFlowRuntime = childFlow.getRuntimeObject(); + + if (contentIdx == 0 && !childFlow.hasParameters() && this.getFlowLevel() == FlowLevel.Knot) { + startingSubFlowDivert = new Divert(); + RuntimeUtils.addContent(container, startingSubFlowDivert); + startingSubFlowRuntime = childFlowRuntime; + } + + INamedContent namedChild = (INamedContent) childFlowRuntime; + INamedContent existingChild = container.getNamedContent().get(namedChild.getName()); + if (existingChild != null) { + String errorMsg = String.format( + "%s already contains flow named '%s' (at %s)", + this.getClass().getSimpleName(), + namedChild.getName(), + ((com.bladecoder.ink.runtime.RTObject) existingChild).getDebugMetadata()); + + error(errorMsg, childFlow, false); + } + + container.addToNamedContentOnly(namedChild); + } else { + RuntimeUtils.addContent(container, obj.getRuntimeObject()); + } + + contentIdx++; + } + + if (getFlowLevel() != FlowLevel.Story && !this.isFunction && rootWeave != null && foundReturn == null) { + rootWeave.validateTermination(this::warningInTermination); + } + + return container; + } + + private void generateArgumentVariableAssignments(Container container) { + if (this.arguments == null || this.arguments.isEmpty()) { + return; + } + + for (int i = arguments.size() - 1; i >= 0; --i) { + String paramName = arguments.get(i).identifier != null ? arguments.get(i).identifier.name : null; + + try { + com.bladecoder.ink.runtime.VariableAssignment assign = + new com.bladecoder.ink.runtime.VariableAssignment(paramName, true); + RuntimeUtils.addContent(container, assign); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public ParsedObject contentWithNameAtLevel(String name, FlowLevel level, boolean deepSearch) { + if (level == getFlowLevel() || level == null) { + if (identifier != null && name.equals(identifier.name)) { + return this; + } + } + + if (level == FlowLevel.WeavePoint || level == null) { + ParsedObject weavePointResult = null; + + if (rootWeave != null) { + weavePointResult = (ParsedObject) rootWeave.weavePointNamed(name); + if (weavePointResult != null) { + return weavePointResult; + } + } + + if (level == FlowLevel.WeavePoint) { + return deepSearch ? deepSearchForAnyLevelContent(name) : null; + } + } + + if (level != null && level.ordinal() < getFlowLevel().ordinal()) { + return null; + } + + FlowBase subFlow = subFlowsByName.get(name); + if (subFlow != null) { + if (level == null || level == subFlow.getFlowLevel()) { + return subFlow; + } + } + + return deepSearch ? deepSearchForAnyLevelContent(name) : null; + } + + private ParsedObject deepSearchForAnyLevelContent(String name) { + ParsedObject weaveResultSelf = contentWithNameAtLevel(name, FlowLevel.WeavePoint, false); + if (weaveResultSelf != null) { + return weaveResultSelf; + } + + for (FlowBase subFlow : subFlowsByName.values()) { + ParsedObject deepResult = subFlow.contentWithNameAtLevel(name, null, true); + if (deepResult != null) { + return deepResult; + } + } + + return null; + } + + @Override + public void resolveReferences(Story context) { + if (startingSubFlowDivert != null) { + startingSubFlowDivert.setTargetPath(startingSubFlowRuntime.getPath()); + } + + super.resolveReferences(context); + + if (arguments != null) { + for (Argument arg : arguments) { + context.checkForNamingCollisions(this, arg.identifier, Story.SymbolType.Arg, "argument"); + } + + for (int i = 0; i < arguments.size(); i++) { + for (int j = i + 1; j < arguments.size(); j++) { + if (arguments.get(i).identifier != null + && arguments.get(j).identifier != null + && arguments.get(i).identifier.name.equals(arguments.get(j).identifier.name)) { + error("Multiple arguments with the same name: '" + arguments.get(i).identifier + "'"); + } + } + } + } + + if (getFlowLevel() != FlowLevel.Story) { + Story.SymbolType symbolType = + getFlowLevel() == FlowLevel.Knot ? Story.SymbolType.Knot : Story.SymbolType.SubFlowAndWeave; + context.checkForNamingCollisions(this, identifier, symbolType, null); + } + } + + private void checkForDisallowedFunctionFlowControl() { + if (!(this instanceof Knot)) { + error( + "Functions cannot be stitches - i.e. they should be defined as '== function myFunc ==' rather than public to another knot."); + } + + for (FlowBase subFlow : subFlowsByName.values()) { + String name = subFlow.getIdentifier() != null ? subFlow.getIdentifier().name : null; + error( + "Functions may not contain stitches, but saw '" + name + "' within the function '" + this.identifier + + "'", + subFlow, + false); + } + + List allDiverts = + rootWeave.findAll(com.bladecoder.ink.compiler.ParsedHierarchy.Divert.class); + for (com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert : allDiverts) { + if (!divert.isFunctionCall && !(divert.parent instanceof DivertTarget)) { + error("Functions may not contain diverts, but saw '" + divert + "'", divert, false); + } + } + + List allChoices = rootWeave.findAll(Choice.class); + for (Choice choice : allChoices) { + error("Functions may not contain choices, but saw '" + choice + "'", choice, false); + } + } + + @Override + public boolean hasValidName() { + return getName() != null && !getName().isEmpty(); + } + + private void warningInTermination(ParsedObject terminatingObject) { + String message = + "Apparent loose end exists where the flow runs out. Do you need a '-> DONE' statement, choice or divert?"; + if (terminatingObject.parent == rootWeave && firstChildFlow != null) { + message = message + " Note that if you intend to enter '" + firstChildFlow.identifier + + "' next, you need to divert to it explicitly."; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Divert terminatingDivert = + terminatingObject instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert + ? (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) terminatingObject + : null; + if (terminatingDivert != null && terminatingDivert.isTunnel) { + message = message + " When final tunnel to '" + terminatingDivert.target + + " ->' returns it won't have anywhere to go."; + } + + warning(message, terminatingObject); + } + + public Map getSubFlowsByName() { + return subFlowsByName; + } + + @Override + public String getTypeName() { + if (isFunction) { + return "Function"; + } + return getFlowLevel().toString(); + } + + @Override + public String toString() { + return getTypeName() + " '" + identifier + "'"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowLevel.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowLevel.java new file mode 100644 index 0000000..767237f --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FlowLevel.java @@ -0,0 +1,8 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public enum FlowLevel { + Story, + Knot, + Stitch, + WeavePoint +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FunctionCall.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FunctionCall.java new file mode 100644 index 0000000..d08a6ce --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/FunctionCall.java @@ -0,0 +1,287 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.InkStringConversionExtensions; +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.InkList; +import com.bladecoder.ink.runtime.ListValue; +import com.bladecoder.ink.runtime.NativeFunctionCall; +import com.bladecoder.ink.runtime.StringValue; +import java.util.List; + +public class FunctionCall extends Expression { + private final Divert proxyDivert; + private DivertTarget divertTargetToCount; + private VariableReference variableReferenceToCount; + + public boolean shouldPopReturnedValue; + + public FunctionCall(Identifier functionName, List arguments) { + proxyDivert = new Divert(new Path(functionName), arguments); + proxyDivert.isFunctionCall = true; + addContent(proxyDivert); + } + + public String getName() { + return proxyDivert.target.getFirstComponent(); + } + + public Divert getProxyDivert() { + return proxyDivert; + } + + public List getArguments() { + return proxyDivert.arguments; + } + + public com.bladecoder.ink.runtime.Divert getRuntimeDivert() { + return proxyDivert.runtimeDivert; + } + + public boolean isChoiceCount() { + return "CHOICE_COUNT".equals(getName()); + } + + public boolean isTurns() { + return "TURNS".equals(getName()); + } + + public boolean isTurnsSince() { + return "TURNS_SINCE".equals(getName()); + } + + public boolean isRandom() { + return "RANDOM".equals(getName()); + } + + public boolean isSeedRandom() { + return "SEED_RANDOM".equals(getName()); + } + + public boolean isListRange() { + return "LIST_RANGE".equals(getName()); + } + + public boolean isListRandom() { + return "LIST_RANDOM".equals(getName()); + } + + public boolean isReadCount() { + return "READ_COUNT".equals(getName()); + } + + @Override + public void generateIntoContainer(Container container) { + ListDefinition foundList = getStory().resolveList(getName()); + + boolean usingProxyDivert = false; + + if (isChoiceCount()) { + if (getArguments().size() > 0) { + error("The CHOICE_COUNT() function shouldn't take any arguments"); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.ChoiceCount); + + } else if (isTurns()) { + if (getArguments().size() > 0) { + error("The TURNS() function shouldn't take any arguments"); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.Turns); + + } else if (isTurnsSince() || isReadCount()) { + DivertTarget divertTarget = getArguments().get(0) instanceof DivertTarget + ? (DivertTarget) getArguments().get(0) + : null; + VariableReference variableDivertTarget = getArguments().get(0) instanceof VariableReference + ? (VariableReference) getArguments().get(0) + : null; + + if (getArguments().size() != 1 || (divertTarget == null && variableDivertTarget == null)) { + error( + "The " + getName() + + "() function should take one argument: a divert target to the target knot, stitch, gather or choice you want to check. e.g. TURNS_SINCE(-> myKnot)"); + return; + } + + if (divertTarget != null) { + divertTargetToCount = divertTarget; + addContent(divertTargetToCount); + divertTargetToCount.generateIntoContainer(container); + } else { + variableReferenceToCount = variableDivertTarget; + addContent(variableReferenceToCount); + variableReferenceToCount.generateIntoContainer(container); + } + + if (isTurnsSince()) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.TurnsSince); + } else { + RuntimeUtils.addContent(container, ControlCommand.CommandType.ReadCount); + } + + } else if (isRandom()) { + if (getArguments().size() != 2) { + error("RANDOM should take 2 parameters: a minimum and a maximum integer"); + } + + for (int arg = 0; arg < getArguments().size(); arg++) { + if (getArguments().get(arg) instanceof Number) { + Number num = (Number) getArguments().get(arg); + if (!(num.value instanceof Integer)) { + String paramName = arg == 0 ? "minimum" : "maximum"; + error("RANDOM's " + paramName + " parameter should be an integer"); + } + } + + getArguments().get(arg).generateIntoContainer(container); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.Random); + + } else if (isSeedRandom()) { + if (getArguments().size() != 1) { + error("SEED_RANDOM should take 1 parameter - an integer seed"); + } + + Number num = getArguments().get(0) instanceof Number + ? (Number) getArguments().get(0) + : null; + if (num != null && !(num.value instanceof Integer)) { + error("SEED_RANDOM's parameter should be an integer seed"); + } + + getArguments().get(0).generateIntoContainer(container); + + RuntimeUtils.addContent(container, ControlCommand.CommandType.SeedRandom); + + } else if (isListRange()) { + if (getArguments().size() != 3) { + error("LIST_RANGE should take 3 parameters - a list, a min and a max"); + } + + for (Expression arg : getArguments()) { + arg.generateIntoContainer(container); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.ListRange); + + } else if (isListRandom()) { + if (getArguments().size() != 1) { + error("LIST_RANDOM should take 1 parameter - a list"); + } + + getArguments().get(0).generateIntoContainer(container); + + RuntimeUtils.addContent(container, ControlCommand.CommandType.ListRandom); + + } else if (NativeFunctionCall.callExistsWithName(getName())) { + NativeFunctionCall nativeCall = NativeFunctionCall.callWithName(getName()); + + if (nativeCall.getNumberOfParameters() != getArguments().size()) { + String msg = getName() + " should take " + nativeCall.getNumberOfParameters() + " parameter"; + if (nativeCall.getNumberOfParameters() > 1) { + msg += "s"; + } + error(msg); + } + + for (Expression arg : getArguments()) { + arg.generateIntoContainer(container); + } + + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName(getName())); + + } else if (foundList != null) { + if (getArguments().size() > 1) { + error( + "Can currently only construct a list from one integer (or an empty list from a given list definition)"); + } + + if (getArguments().size() == 1) { + RuntimeUtils.addContent(container, new StringValue(getName())); + getArguments().get(0).generateIntoContainer(container); + RuntimeUtils.addContent(container, ControlCommand.CommandType.ListFromInt); + } else { + InkList list = new InkList(); + list.setInitialOriginName(getName()); + RuntimeUtils.addContent(container, new ListValue(list)); + } + + } else { + RuntimeUtils.addContent(container, proxyDivert.getRuntimeObject()); + usingProxyDivert = true; + } + + if (!usingProxyDivert && content != null) { + content.remove(proxyDivert); + } + + if (shouldPopReturnedValue) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.PopEvaluatedValue); + } + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (content == null || !content.contains(proxyDivert)) { + if (getArguments() != null) { + for (Expression arg : getArguments()) { + arg.resolveReferences(context); + } + } + } + + if (divertTargetToCount != null) { + Divert divert = divertTargetToCount.divert; + boolean attemptingTurnCountOfVariableTarget = divert.runtimeDivert.getVariableDivertName() != null; + + if (attemptingTurnCountOfVariableTarget) { + error( + "When getting the TURNS_SINCE() of a variable target, remove the '->' - i.e. it should just be TURNS_SINCE(" + + divert.runtimeDivert.getVariableDivertName() + ")"); + return; + } + + ParsedObject targetObject = divert.targetContent; + if (targetObject == null) { + if (!attemptingTurnCountOfVariableTarget) { + error("Failed to find target for TURNS_SINCE: '" + divert.target + "'"); + } + } else { + targetObject.getContainerForCounting().setTurnIndexShouldBeCounted(true); + } + } else if (variableReferenceToCount != null) { + com.bladecoder.ink.runtime.VariableReference runtimeVarRef = variableReferenceToCount.getRuntimeVarRef(); + if (runtimeVarRef != null && runtimeVarRef.getPathForCount() != null) { + error("Should be " + getName() + "(-> " + variableReferenceToCount.getName() + + "). Usage without the '->' only makes sense for variable targets."); + } + } + } + + public static boolean isBuiltIn(String name) { + if (NativeFunctionCall.callExistsWithName(name)) { + return true; + } + + return "CHOICE_COUNT".equals(name) + || "TURNS_SINCE".equals(name) + || "TURNS".equals(name) + || "RANDOM".equals(name) + || "SEED_RANDOM".equals(name) + || "LIST_VALUE".equals(name) + || "LIST_RANDOM".equals(name) + || "READ_COUNT".equals(name); + } + + @Override + public String toString() { + String strArgs = String.join(", ", InkStringConversionExtensions.toStringsArray(getArguments())); + return String.format("%s(%s)", getName(), strArgs); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Gather.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Gather.java new file mode 100644 index 0000000..effee0d --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Gather.java @@ -0,0 +1,63 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; + +public class Gather extends ParsedObject implements IWeavePoint, INamedContent { + public Identifier identifier; + private final int indentationDepth; + + public Gather(Identifier identifier, int indentationDepth) { + this.identifier = identifier; + this.indentationDepth = indentationDepth; + } + + @Override + public int getIndentationDepth() { + return indentationDepth; + } + + @Override + public Container getRuntimeContainer() { + return (Container) getRuntimeObject(); + } + + @Override + public String getName() { + return identifier != null ? identifier.name : null; + } + + @Override + public Identifier getIdentifier() { + return identifier; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Container container = new Container(); + container.setName(getName()); + + if (getStory().countAllVisits) { + container.setVisitsShouldBeCounted(true); + } + + container.setCountingAtStartOnly(true); + + if (content != null) { + for (ParsedObject obj : content) { + RuntimeUtils.addContent(container, obj.getRuntimeObject()); + } + } + + return container; + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (identifier != null && identifier.name.length() > 0) { + context.checkForNamingCollisions(this, identifier, Story.SymbolType.SubFlowAndWeave, null); + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Glue.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Glue.java new file mode 100644 index 0000000..e5d20e9 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Glue.java @@ -0,0 +1,7 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class Glue extends Wrap { + public Glue(com.bladecoder.ink.runtime.Glue glue) { + super(glue); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/INamedContent.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/INamedContent.java new file mode 100644 index 0000000..71ec3f8 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/INamedContent.java @@ -0,0 +1,5 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public interface INamedContent { + String getName(); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IWeavePoint.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IWeavePoint.java new file mode 100644 index 0000000..cb50680 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IWeavePoint.java @@ -0,0 +1,15 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.Container; + +public interface IWeavePoint { + int getIndentationDepth(); + + Container getRuntimeContainer(); + + java.util.List getContent(); + + String getName(); + + Identifier getIdentifier(); +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Identifier.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Identifier.java new file mode 100644 index 0000000..1b7e66c --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Identifier.java @@ -0,0 +1,20 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.DebugMetadata; + +public class Identifier { + public String name; + public DebugMetadata debugMetadata; + + @Override + public String toString() { + return name; + } + + public static final Identifier Done = new Identifier(); + + static { + Done.name = "DONE"; + Done.debugMetadata = null; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncDecExpression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncDecExpression.java new file mode 100644 index 0000000..d7e5641 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncDecExpression.java @@ -0,0 +1,75 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.NativeFunctionCall; +import com.bladecoder.ink.runtime.VariableAssignment; +import com.bladecoder.ink.runtime.VariableReference; + +public class IncDecExpression extends Expression { + public Identifier varIdentifier; + public boolean isInc; + public Expression expression; + private VariableAssignment runtimeAssignment; + + public IncDecExpression(Identifier varIdentifier, boolean isInc) { + this.varIdentifier = varIdentifier; + this.isInc = isInc; + } + + public IncDecExpression(Identifier varIdentifier, Expression expression, boolean isInc) { + this(varIdentifier, isInc); + this.expression = expression; + addContent(expression); + } + + @Override + public void generateIntoContainer(Container container) { + RuntimeUtils.addContent(container, new VariableReference(varIdentifier != null ? varIdentifier.name : null)); + + if (expression != null) { + expression.generateIntoContainer(container); + } else { + RuntimeUtils.addContent(container, new com.bladecoder.ink.runtime.IntValue(1)); + } + + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName(isInc ? "+" : "-")); + + try { + runtimeAssignment = new VariableAssignment(varIdentifier != null ? varIdentifier.name : null, false); + RuntimeUtils.addContent(container, runtimeAssignment); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + FlowBase.VariableResolveResult varResolveResult = + context.resolveVariableWithName(varIdentifier != null ? varIdentifier.name : null, this); + if (!varResolveResult.found) { + error("variable for " + incrementDecrementWord() + " could not be found: '" + varIdentifier + + "' after searching: " + getDescriptionOfScope()); + } + + runtimeAssignment.setIsGlobal(varResolveResult.isGlobal); + + if (!(parent instanceof Weave) && !(parent instanceof FlowBase) && !(parent instanceof ContentList)) { + error("Can't use " + incrementDecrementWord() + " as sub-expression"); + } + } + + private String incrementDecrementWord() { + return isInc ? "increment" : "decrement"; + } + + @Override + public String toString() { + if (expression != null) { + return varIdentifier + (isInc ? " += " : " -= ") + expression; + } + return varIdentifier + (isInc ? "++" : "--"); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncludedFile.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncludedFile.java new file mode 100644 index 0000000..aba2a7a --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/IncludedFile.java @@ -0,0 +1,14 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class IncludedFile extends ParsedObject { + public final Story includedStory; + + public IncludedFile(Story includedStory) { + this.includedStory = includedStory; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + return null; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Knot.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Knot.java new file mode 100644 index 0000000..e7bc6d3 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Knot.java @@ -0,0 +1,33 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import java.util.List; + +public class Knot extends FlowBase { + @Override + public FlowLevel getFlowLevel() { + return FlowLevel.Knot; + } + + public Knot(Identifier name, List topLevelObjects, List arguments, boolean isFunction) { + super(name, topLevelObjects, arguments, isFunction, false); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + Story parentStory = getStory(); + + for (FlowBase stitch : getSubFlowsByName().values()) { + String stitchName = stitch.getIdentifier() != null ? stitch.getIdentifier().name : null; + + ParsedObject knotWithStitchName = parentStory.contentWithNameAtLevel(stitchName, FlowLevel.Knot, false); + if (knotWithStitchName != null) { + String errorMsg = String.format( + "Stitch '%s' has the same name as a knot (on %s)", + stitch.getIdentifier(), knotWithStitchName.getDebugMetadata()); + error(errorMsg, stitch, false); + } + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/List.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/List.java new file mode 100644 index 0000000..c2b96bd --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/List.java @@ -0,0 +1,59 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.InkList; +import com.bladecoder.ink.runtime.InkListItem; +import com.bladecoder.ink.runtime.ListValue; + +public class List extends Expression { + public java.util.List itemIdentifierList; + + public List(java.util.List itemIdentifierList) { + this.itemIdentifierList = itemIdentifierList; + } + + @Override + public void generateIntoContainer(Container container) { + InkList runtimeRawList = new InkList(); + + if (itemIdentifierList != null) { + for (Identifier itemIdentifier : itemIdentifierList) { + String[] nameParts = itemIdentifier != null ? itemIdentifier.name.split("\\.") : new String[0]; + + String listName = null; + String listItemName = null; + if (nameParts.length > 1) { + listName = nameParts[0]; + listItemName = nameParts[1]; + } else if (nameParts.length == 1) { + listItemName = nameParts[0]; + } + + ListElementDefinition listItem = getStory().resolveListItem(listName, listItemName, this); + if (listItem == null) { + if (listName == null) { + error("Could not find list definition that contains item '" + itemIdentifier + "'"); + } else { + error("Could not find list item " + itemIdentifier); + } + } else { + if (listName == null) { + listName = ((ListDefinition) listItem.parent).identifier != null + ? ((ListDefinition) listItem.parent).identifier.name + : null; + } + InkListItem item = new InkListItem(listName, listItem.getName()); + + if (runtimeRawList.containsKey(item)) { + warning("Duplicate of item '" + itemIdentifier + "' in list."); + } else { + runtimeRawList.put(item, listItem.seriesValue); + } + } + } + } + + RuntimeUtils.addContent(container, new ListValue(runtimeRawList)); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListDefinition.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListDefinition.java new file mode 100644 index 0000000..b08f7f2 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListDefinition.java @@ -0,0 +1,84 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.InkList; +import com.bladecoder.ink.runtime.InkListItem; +import com.bladecoder.ink.runtime.ListValue; +import java.util.HashMap; +import java.util.List; + +public class ListDefinition extends ParsedObject { + public Identifier identifier; + public List itemDefinitions; + + public VariableAssignment variableAssignment; + + private HashMap elementsByName; + + public ListDefinition(List elements) { + itemDefinitions = elements; + + int currentValue = 1; + for (ListElementDefinition e : itemDefinitions) { + if (e.explicitValue != null) { + currentValue = e.explicitValue; + } + + e.seriesValue = currentValue; + + currentValue++; + } + + addContent(elements); + } + + public com.bladecoder.ink.runtime.ListDefinition getRuntimeListDefinition() { + HashMap allItems = new HashMap<>(); + for (ListElementDefinition e : itemDefinitions) { + if (!allItems.containsKey(e.getName())) { + allItems.put(e.getName(), e.seriesValue); + } else { + error("List '" + identifier + "' contains duplicate items called '" + e.getName() + "'"); + } + } + + return new com.bladecoder.ink.runtime.ListDefinition(identifier != null ? identifier.name : null, allItems); + } + + public ListElementDefinition itemNamed(String itemName) { + if (elementsByName == null) { + elementsByName = new HashMap<>(); + for (ListElementDefinition el : itemDefinitions) { + elementsByName.put(el.getName(), el); + } + } + + return elementsByName.get(itemName); + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + InkList initialValues = new InkList(); + for (ListElementDefinition itemDef : itemDefinitions) { + if (itemDef.inInitialList) { + InkListItem item = new InkListItem(identifier != null ? identifier.name : null, itemDef.getName()); + initialValues.put(item, itemDef.seriesValue); + } + } + + initialValues.setInitialOriginName(identifier != null ? identifier.name : null); + + return new ListValue(initialValues); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + context.checkForNamingCollisions(this, identifier, Story.SymbolType.List, null); + } + + @Override + public String getTypeName() { + return "List definition"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListElementDefinition.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListElementDefinition.java new file mode 100644 index 0000000..1f33e79 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ListElementDefinition.java @@ -0,0 +1,48 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class ListElementDefinition extends ParsedObject { + public Identifier identifier; + public Integer explicitValue; + public int seriesValue; + public boolean inInitialList; + + public String getName() { + return identifier != null ? identifier.name : null; + } + + public String getFullName() { + ListDefinition parentList = parent instanceof ListDefinition ? (ListDefinition) parent : null; + if (parentList == null) { + throw new RuntimeException("Can't get full name without a parent list"); + } + + return parentList.identifier + "." + getName(); + } + + public ListElementDefinition(Identifier identifier, boolean inInitialList, Integer explicitValue) { + this.identifier = identifier; + this.inInitialList = inInitialList; + this.explicitValue = explicitValue; + } + + public ListElementDefinition(Identifier identifier, boolean inInitialList) { + this(identifier, inInitialList, null); + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + throw new UnsupportedOperationException(); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + context.checkForNamingCollisions(this, identifier, Story.SymbolType.ListItem, null); + } + + @Override + public String getTypeName() { + return "List element"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/MultipleConditionExpression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/MultipleConditionExpression.java new file mode 100644 index 0000000..c4f0896 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/MultipleConditionExpression.java @@ -0,0 +1,37 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.NativeFunctionCall; +import java.util.ArrayList; +import java.util.List; + +public class MultipleConditionExpression extends Expression { + public List getSubExpressions() { + List result = new ArrayList<>(); + if (content != null) { + for (ParsedObject obj : content) { + result.add((Expression) obj); + } + } + return result; + } + + public MultipleConditionExpression(List conditionExpressions) { + addContent(conditionExpressions); + } + + @Override + public void generateIntoContainer(Container container) { + boolean isFirst = true; + for (Expression conditionExpr : getSubExpressions()) { + conditionExpr.generateIntoContainer(container); + + if (!isFirst) { + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("&&")); + } + + isFirst = false; + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Number.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Number.java new file mode 100644 index 0000000..10f526b --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Number.java @@ -0,0 +1,53 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.BoolValue; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.FloatValue; +import com.bladecoder.ink.runtime.IntValue; + +public class Number extends Expression { + public java.lang.Object value; + + public Number(java.lang.Object value) { + if (value instanceof Integer || value instanceof Float || value instanceof Boolean) { + this.value = value; + } else { + throw new RuntimeException("Unexpected object type in Number"); + } + } + + @Override + public void generateIntoContainer(Container container) { + if (value instanceof Integer) { + RuntimeUtils.addContent(container, new IntValue((Integer) value)); + } else if (value instanceof Float) { + RuntimeUtils.addContent(container, new FloatValue((Float) value)); + } else if (value instanceof Boolean) { + RuntimeUtils.addContent(container, new BoolValue((Boolean) value)); + } + } + + @Override + public String toString() { + if (value instanceof Float) { + return Float.toString((Float) value); + } + return value.toString(); + } + + @Override + public boolean equals(java.lang.Object obj) { + Number otherNum = obj instanceof Number ? (Number) obj : null; + if (otherNum == null) { + return false; + } + + return value.equals(otherNum.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ParsedObject.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ParsedObject.java new file mode 100644 index 0000000..75b7f26 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/ParsedObject.java @@ -0,0 +1,312 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.DebugMetadata; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ParsedObject { + private DebugMetadata debugMetadata; + private boolean alreadyHadError; + private boolean alreadyHadWarning; + private com.bladecoder.ink.runtime.RTObject runtimeObject; + + public DebugMetadata getDebugMetadata() { + if (debugMetadata == null && parent != null) { + return parent.getDebugMetadata(); + } + + return debugMetadata; + } + + public void setDebugMetadata(DebugMetadata value) { + debugMetadata = value; + } + + public boolean hasOwnDebugMetadata() { + return debugMetadata != null; + } + + public String getTypeName() { + return getClass().getSimpleName(); + } + + public ParsedObject parent; + protected List content; + + public Story getStory() { + ParsedObject ancestor = this; + while (ancestor.parent != null) { + ancestor = ancestor.parent; + } + return (Story) ancestor; + } + + public com.bladecoder.ink.runtime.RTObject getRuntimeObject() { + if (runtimeObject == null) { + runtimeObject = generateRuntimeObject(); + if (runtimeObject != null) { + runtimeObject.setDebugMetadata(getDebugMetadata()); + } + } + return runtimeObject; + } + + public void setRuntimeObject(com.bladecoder.ink.runtime.RTObject value) { + runtimeObject = value; + } + + public com.bladecoder.ink.runtime.Path getRuntimePath() { + return getRuntimeObject().getPath(); + } + + public Container getContainerForCounting() { + return (Container) getRuntimeObject(); + } + + public Path pathRelativeTo(ParsedObject otherObj) { + List ownAncestry = getAncestry(); + List otherAncestry = otherObj.getAncestry(); + + ParsedObject highestCommonAncestor = null; + int minLength = Math.min(ownAncestry.size(), otherAncestry.size()); + for (int i = 0; i < minLength; ++i) { + ParsedObject a1 = ownAncestry.get(i); + ParsedObject a2 = otherAncestry.get(i); + if (a1 == a2) { + highestCommonAncestor = a1; + } else { + break; + } + } + + FlowBase commonFlowAncestor = + highestCommonAncestor instanceof FlowBase ? (FlowBase) highestCommonAncestor : null; + if (commonFlowAncestor == null && highestCommonAncestor != null) { + commonFlowAncestor = highestCommonAncestor.closestFlowBase(); + } + + List pathComponents = new ArrayList<>(); + boolean hasWeavePoint = false; + FlowLevel baseFlow = FlowLevel.WeavePoint; + + ParsedObject ancestor = this; + while (ancestor != null && ancestor != commonFlowAncestor && !(ancestor instanceof Story)) { + if (!hasWeavePoint) { + IWeavePoint weavePointAncestor = ancestor instanceof IWeavePoint ? (IWeavePoint) ancestor : null; + if (weavePointAncestor != null && weavePointAncestor.getIdentifier() != null) { + pathComponents.add(weavePointAncestor.getIdentifier()); + hasWeavePoint = true; + ancestor = ancestor.parent; + continue; + } + } + + if (ancestor instanceof FlowBase) { + FlowBase flowAncestor = (FlowBase) ancestor; + pathComponents.add(flowAncestor.getIdentifier()); + baseFlow = flowAncestor.getFlowLevel(); + } + + ancestor = ancestor.parent; + } + + Collections.reverse(pathComponents); + + if (!pathComponents.isEmpty()) { + return new Path(baseFlow, pathComponents); + } + + return null; + } + + public List getAncestry() { + List result = new ArrayList<>(); + + ParsedObject ancestor = parent; + while (ancestor != null) { + result.add(ancestor); + ancestor = ancestor.parent; + } + + Collections.reverse(result); + + return result; + } + + public String getDescriptionOfScope() { + List locationNames = new ArrayList<>(); + + ParsedObject ancestor = this; + while (ancestor != null) { + if (ancestor instanceof FlowBase) { + FlowBase ancestorFlow = (FlowBase) ancestor; + if (ancestorFlow.getIdentifier() != null) { + locationNames.add("'" + ancestorFlow.getIdentifier() + "'"); + } + } + ancestor = ancestor.parent; + } + + StringBuilder scopeSB = new StringBuilder(); + if (!locationNames.isEmpty()) { + scopeSB.append(String.join(", ", locationNames)); + scopeSB.append(" and "); + } + + scopeSB.append("at top scope"); + + return scopeSB.toString(); + } + + public T addContent(T subContent) { + if (content == null) { + content = new ArrayList<>(); + } + + if (subContent != null) { + subContent.parent = this; + content.add(subContent); + } + + return subContent; + } + + public void addContent(List listContent) { + for (T obj : listContent) { + addContent(obj); + } + } + + public T insertContent(int index, T subContent) { + if (content == null) { + content = new ArrayList<>(); + } + + subContent.parent = this; + content.add(index, subContent); + + return subContent; + } + + public interface FindQueryFunc { + boolean matches(T obj); + } + + public T find(Class type, FindQueryFunc queryFunc) { + if (type.isInstance(this)) { + T tObj = type.cast(this); + if (queryFunc == null || queryFunc.matches(tObj)) { + return tObj; + } + } + + if (content == null) { + return null; + } + + for (ParsedObject obj : content) { + T nestedResult = obj.find(type, queryFunc); + if (nestedResult != null) { + return nestedResult; + } + } + + return null; + } + + public T find(Class type) { + return find(type, null); + } + + public List findAll(Class type, FindQueryFunc queryFunc) { + List found = new ArrayList<>(); + findAll(type, queryFunc, found); + return found; + } + + public List findAll(Class type) { + return findAll(type, null); + } + + private void findAll(Class type, FindQueryFunc queryFunc, List foundSoFar) { + if (type.isInstance(this)) { + T tObj = type.cast(this); + if (queryFunc == null || queryFunc.matches(tObj)) { + foundSoFar.add(tObj); + } + } + + if (content == null) { + return; + } + + for (ParsedObject obj : content) { + obj.findAll(type, queryFunc, foundSoFar); + } + } + + public abstract com.bladecoder.ink.runtime.RTObject generateRuntimeObject(); + + public void resolveReferences(Story context) { + if (content != null) { + for (ParsedObject obj : content) { + obj.resolveReferences(context); + } + } + } + + public FlowBase closestFlowBase() { + ParsedObject ancestor = parent; + while (ancestor != null) { + if (ancestor instanceof FlowBase) { + return (FlowBase) ancestor; + } + ancestor = ancestor.parent; + } + + return null; + } + + public void error(String message, ParsedObject source, boolean isWarning) { + if (source == null) { + source = this; + } + + if (source.alreadyHadError && !isWarning) { + return; + } + if (source.alreadyHadWarning && isWarning) { + return; + } + + if (parent != null) { + parent.error(message, source, isWarning); + } else { + throw new RuntimeException("No parent object to send error to: " + message); + } + + if (isWarning) { + source.alreadyHadWarning = true; + } else { + source.alreadyHadError = true; + } + } + + public void error(String message) { + error(message, null, false); + } + + public void warning(String message, ParsedObject source) { + error(message, source, true); + } + + public void warning(String message) { + error(message, null, true); + } + + public List getContent() { + return content; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Path.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Path.java new file mode 100644 index 0000000..f293e16 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Path.java @@ -0,0 +1,147 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Path { + private FlowLevel baseTargetLevel; + private String dotSeparatedComponents; + public final List components; + + public FlowLevel getBaseTargetLevel() { + if (isBaseLevelAmbiguous()) { + return FlowLevel.Story; + } + return baseTargetLevel; + } + + public boolean isBaseLevelAmbiguous() { + return baseTargetLevel == null; + } + + public String getFirstComponent() { + if (components == null || components.isEmpty()) { + return null; + } + + return components.get(0).name; + } + + public int getNumberOfComponents() { + return components.size(); + } + + public String getDotSeparatedComponents() { + if (dotSeparatedComponents == null) { + dotSeparatedComponents = + components.stream().map(c -> c == null ? null : c.name).collect(Collectors.joining(".")); + } + + return dotSeparatedComponents; + } + + public Path(FlowLevel baseFlowLevel, List components) { + baseTargetLevel = baseFlowLevel; + this.components = components; + } + + public Path(List components) { + baseTargetLevel = null; + this.components = components; + } + + public Path(Identifier ambiguousName) { + baseTargetLevel = null; + components = new ArrayList<>(); + components.add(ambiguousName); + } + + @Override + public String toString() { + if (components == null || components.isEmpty()) { + if (getBaseTargetLevel() == FlowLevel.WeavePoint) { + return "-> "; + } + return ""; + } + + return "-> " + getDotSeparatedComponents(); + } + + public ParsedObject resolveFromContext(ParsedObject context) { + if (components == null || components.isEmpty()) { + return null; + } + + ParsedObject baseTargetObject = resolveBaseTarget(context); + if (baseTargetObject == null) { + return null; + } + + if (components.size() > 1) { + return resolveTailComponents(baseTargetObject); + } + + return baseTargetObject; + } + + private ParsedObject resolveBaseTarget(ParsedObject originalContext) { + String firstComp = getFirstComponent(); + + ParsedObject ancestorContext = originalContext; + while (ancestorContext != null) { + boolean deepSearch = ancestorContext == originalContext; + + ParsedObject foundBase = tryGetChildFromContext(ancestorContext, firstComp, null, deepSearch); + if (foundBase != null) { + return foundBase; + } + + ancestorContext = ancestorContext.parent; + } + + return null; + } + + private ParsedObject resolveTailComponents(ParsedObject rootTarget) { + ParsedObject foundComponent = rootTarget; + for (int i = 1; i < components.size(); ++i) { + String compName = components.get(i).name; + + FlowLevel minimumExpectedLevel; + FlowBase foundFlow = foundComponent instanceof FlowBase ? (FlowBase) foundComponent : null; + if (foundFlow != null) { + minimumExpectedLevel = + FlowLevel.values()[foundFlow.getFlowLevel().ordinal() + 1]; + } else { + minimumExpectedLevel = FlowLevel.WeavePoint; + } + + foundComponent = tryGetChildFromContext(foundComponent, compName, minimumExpectedLevel, false); + if (foundComponent == null) { + break; + } + } + + return foundComponent; + } + + private ParsedObject tryGetChildFromContext( + ParsedObject context, String childName, FlowLevel minimumLevel, boolean forceDeepSearch) { + boolean ambiguousChildLevel = minimumLevel == null; + + Weave weaveContext = context instanceof Weave ? (Weave) context : null; + if (weaveContext != null && (ambiguousChildLevel || minimumLevel == FlowLevel.WeavePoint)) { + return (ParsedObject) weaveContext.weavePointNamed(childName); + } + + FlowBase flowContext = context instanceof FlowBase ? (FlowBase) context : null; + if (flowContext != null) { + boolean shouldDeepSearch = forceDeepSearch || flowContext.getFlowLevel() == FlowLevel.Knot; + return flowContext.contentWithNameAtLevel(childName, minimumLevel, shouldDeepSearch); + } + + return null; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Return.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Return.java new file mode 100644 index 0000000..07ef41c --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Return.java @@ -0,0 +1,37 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.Void; + +public class Return extends ParsedObject { + public Expression returnedExpression; + + public Return(Expression returnedExpression) { + if (returnedExpression != null) { + this.returnedExpression = addContent(returnedExpression); + } + } + + public Return() { + this(null); + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + Container container = new Container(); + + if (returnedExpression != null) { + RuntimeUtils.addContent(container, returnedExpression.getRuntimeObject()); + } else { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + RuntimeUtils.addContent(container, new Void()); + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.PopFunction); + + return container; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Sequence.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Sequence.java new file mode 100644 index 0000000..16b5d06 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Sequence.java @@ -0,0 +1,173 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.Divert; +import com.bladecoder.ink.runtime.IntValue; +import com.bladecoder.ink.runtime.NativeFunctionCall; +import com.bladecoder.ink.runtime.RTObject; +import java.util.ArrayList; +import java.util.List; + +public class Sequence extends ParsedObject { + public List sequenceElements; + public int sequenceType; + + public Sequence(List elementContentLists, int sequenceType) { + this.sequenceType = sequenceType; + this.sequenceElements = new ArrayList<>(); + + for (ContentList elementContentList : elementContentLists) { + List contentObjs = elementContentList.getContent(); + + ParsedObject seqElObject; + + if (contentObjs == null || contentObjs.isEmpty()) { + seqElObject = elementContentList; + } else if (containsWeavePoint(contentObjs)) { + seqElObject = new Weave(contentObjs); + } else { + seqElObject = elementContentList; + } + + sequenceElements.add(seqElObject); + addContent(seqElObject); + } + } + + private boolean containsWeavePoint(List contentObjs) { + for (ParsedObject obj : contentObjs) { + if (obj instanceof IWeavePoint || obj instanceof Weave) { + return true; + } + } + return false; + } + + @Override + public RTObject generateRuntimeObject() { + Container container = new Container(); + container.setVisitsShouldBeCounted(true); + container.setCountingAtStartOnly(true); + + sequenceDivertsToResolve = new ArrayList<>(); + + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + RuntimeUtils.addContent(container, ControlCommand.CommandType.VisitIndex); + + boolean once = (sequenceType & SequenceType.Once) > 0; + boolean cycle = (sequenceType & SequenceType.Cycle) > 0; + boolean stopping = (sequenceType & SequenceType.Stopping) > 0; + boolean shuffle = (sequenceType & SequenceType.Shuffle) > 0; + + int seqBranchCount = sequenceElements.size(); + if (once) { + seqBranchCount++; + } + + if (stopping || once) { + RuntimeUtils.addContent(container, new IntValue(seqBranchCount - 1)); + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("MIN")); + } else if (cycle) { + RuntimeUtils.addContent(container, new IntValue(sequenceElements.size())); + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("%")); + } + + if (shuffle) { + ControlCommand postShuffleNoOp = new ControlCommand(ControlCommand.CommandType.NoOp); + + if (once || stopping) { + int lastIdx = stopping ? sequenceElements.size() - 1 : sequenceElements.size(); + RuntimeUtils.addContent(container, new ControlCommand(ControlCommand.CommandType.Duplicate)); + RuntimeUtils.addContent(container, new IntValue(lastIdx)); + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("==")); + + Divert skipShuffleDivert = new Divert(); + skipShuffleDivert.setConditional(true); + RuntimeUtils.addContent(container, skipShuffleDivert); + + addDivertToResolve(skipShuffleDivert, postShuffleNoOp); + } + + int elementCountToShuffle = sequenceElements.size(); + if (stopping) { + elementCountToShuffle--; + } + + RuntimeUtils.addContent(container, new IntValue(elementCountToShuffle)); + RuntimeUtils.addContent(container, ControlCommand.CommandType.SequenceShuffleIndex); + if (once || stopping) { + RuntimeUtils.addContent(container, postShuffleNoOp); + } + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + + ControlCommand postSequenceNoOp = new ControlCommand(ControlCommand.CommandType.NoOp); + + for (int elIndex = 0; elIndex < seqBranchCount; elIndex++) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalStart); + RuntimeUtils.addContent(container, ControlCommand.CommandType.Duplicate); + RuntimeUtils.addContent(container, new IntValue(elIndex)); + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName("==")); + RuntimeUtils.addContent(container, ControlCommand.CommandType.EvalEnd); + + Divert sequenceDivert = new Divert(); + sequenceDivert.setConditional(true); + RuntimeUtils.addContent(container, sequenceDivert); + + Container contentContainerForSequenceBranch; + if (elIndex < sequenceElements.size()) { + ParsedObject el = sequenceElements.get(elIndex); + contentContainerForSequenceBranch = (Container) el.getRuntimeObject(); + } else { + contentContainerForSequenceBranch = new Container(); + } + + contentContainerForSequenceBranch.setName("s" + elIndex); + try { + contentContainerForSequenceBranch.insertContent( + new ControlCommand(ControlCommand.CommandType.PopEvaluatedValue), 0); + } catch (Exception e) { + throw new RuntimeException(e); + } + + Divert seqBranchCompleteDivert = new Divert(); + RuntimeUtils.addContent(contentContainerForSequenceBranch, seqBranchCompleteDivert); + container.addToNamedContentOnly(contentContainerForSequenceBranch); + + addDivertToResolve(sequenceDivert, contentContainerForSequenceBranch); + addDivertToResolve(seqBranchCompleteDivert, postSequenceNoOp); + } + + RuntimeUtils.addContent(container, postSequenceNoOp); + + return container; + } + + private void addDivertToResolve(Divert divert, RTObject targetContent) { + sequenceDivertsToResolve.add(new SequenceDivertToResolve(divert, targetContent)); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + for (SequenceDivertToResolve toResolve : sequenceDivertsToResolve) { + toResolve.divert.setTargetPath(toResolve.targetContent.getPath()); + } + } + + private static class SequenceDivertToResolve { + public Divert divert; + public RTObject targetContent; + + public SequenceDivertToResolve(Divert divert, RTObject targetContent) { + this.divert = divert; + this.targetContent = targetContent; + } + } + + private List sequenceDivertsToResolve; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/SequenceType.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/SequenceType.java new file mode 100644 index 0000000..948c088 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/SequenceType.java @@ -0,0 +1,10 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public final class SequenceType { + public static final int Stopping = 1; + public static final int Cycle = 2; + public static final int Shuffle = 4; + public static final int Once = 8; + + private SequenceType() {} +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Stitch.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Stitch.java new file mode 100644 index 0000000..acff017 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Stitch.java @@ -0,0 +1,14 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import java.util.List; + +public class Stitch extends FlowBase { + @Override + public FlowLevel getFlowLevel() { + return FlowLevel.Stitch; + } + + public Stitch(Identifier name, List topLevelObjects, List arguments, boolean isFunction) { + super(name, topLevelObjects, arguments, isFunction, false); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Story.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Story.java new file mode 100644 index 0000000..c84adf3 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Story.java @@ -0,0 +1,458 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.Error.ErrorHandler; +import com.bladecoder.ink.runtime.Error.ErrorType; +import com.bladecoder.ink.runtime.ListDefinition; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +public class Story extends FlowBase { + public Map constants; + public Map externals; + + public boolean countAllVisits = false; + + private ErrorHandler errorHandler; + private boolean hadError; + private boolean hadWarning; + + private HashSet dontFlattenContainers = new HashSet<>(); + private Map listDefs; + + public Story(List topLevelObjects, boolean isInclude) { + super(null, topLevelObjects, null, false, isInclude); + } + + public Story() { + super(null, null, null, false, false); + } + + @Override + public FlowLevel getFlowLevel() { + return FlowLevel.Story; + } + + @Override + protected void preProcessTopLevelObjects(List topLevelContent) { + List flowsFromOtherFiles = new ArrayList<>(); + + int i = 0; + while (i < topLevelContent.size()) { + ParsedObject obj = topLevelContent.get(i); + if (obj instanceof IncludedFile) { + IncludedFile file = (IncludedFile) obj; + + topLevelContent.remove(i); + + if (file.includedStory != null) { + List nonFlowContent = new ArrayList<>(); + Story subStory = file.includedStory; + + if (subStory.content != null) { + for (ParsedObject subStoryObj : subStory.content) { + if (subStoryObj instanceof FlowBase) { + flowsFromOtherFiles.add((FlowBase) subStoryObj); + } else { + nonFlowContent.add(subStoryObj); + } + } + + nonFlowContent.add(new Text("\n")); + + topLevelContent.addAll(i, nonFlowContent); + + i += nonFlowContent.size(); + } + } + + continue; + } + + i++; + } + + topLevelContent.addAll(flowsFromOtherFiles); + } + + public com.bladecoder.ink.runtime.Story exportRuntime(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + + constants = new HashMap<>(); + for (ConstantDeclaration constDecl : findAll(ConstantDeclaration.class)) { + String constName = constDecl.getConstantName(); + Expression existingDefinition = constants.get(constName); + if (existingDefinition != null) { + if (!existingDefinition.equals(constDecl.expression)) { + String errorMsg = String.format( + "CONST '%s' has been redefined with a different value. Multiple definitions of the same CONST are valid so long as they contain the same value. Initial definition was on %s.", + constName, existingDefinition.getDebugMetadata()); + error(errorMsg, constDecl, false); + } + } + + constants.put(constName, constDecl.expression); + } + + listDefs = new HashMap<>(); + for (com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition listDef : + findAll(com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition.class)) { + listDefs.put(listDef.identifier != null ? listDef.identifier.name : null, listDef); + } + + externals = new HashMap<>(); + + resolveWeavePointNaming(); + + Container rootContainer = (Container) getRuntimeObject(); + + Container variableInitialisation = new Container(); + RuntimeUtils.addContent(variableInitialisation, ControlCommand.CommandType.EvalStart); + + List runtimeLists = new ArrayList<>(); + for (Map.Entry nameDeclPair : variableDeclarations.entrySet()) { + String varName = nameDeclPair.getKey(); + VariableAssignment varDecl = nameDeclPair.getValue(); + if (varDecl.isGlobalDeclaration) { + if (varDecl.listDefinition != null) { + listDefs.put(varName, varDecl.listDefinition); + RuntimeUtils.addContent(variableInitialisation, varDecl.listDefinition.getRuntimeObject()); + runtimeLists.add(varDecl.listDefinition.getRuntimeListDefinition()); + } else { + varDecl.expression.generateIntoContainer(variableInitialisation); + } + + try { + com.bladecoder.ink.runtime.VariableAssignment runtimeVarAss = + new com.bladecoder.ink.runtime.VariableAssignment(varName, true); + runtimeVarAss.setIsGlobal(true); + RuntimeUtils.addContent(variableInitialisation, runtimeVarAss); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + RuntimeUtils.addContent(variableInitialisation, ControlCommand.CommandType.EvalEnd); + RuntimeUtils.addContent(variableInitialisation, ControlCommand.CommandType.End); + + if (!variableDeclarations.isEmpty()) { + variableInitialisation.setName("global decl"); + rootContainer.addToNamedContentOnly(variableInitialisation); + } + + RuntimeUtils.addContent(rootContainer, ControlCommand.CommandType.Done); + + com.bladecoder.ink.runtime.Story runtimeStory = + new com.bladecoder.ink.runtime.Story(rootContainer, runtimeLists); + + if (hadError) { + return null; + } + + flattenContainersIn(rootContainer); + + resolveReferences(this); + + if (hadError) { + return null; + } + + try { + runtimeStory.resetState(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return runtimeStory; + } + + public com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition resolveList(String listName) { + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition list = listDefs.get(listName); + if (list == null) { + return null; + } + return list; + } + + public ListElementDefinition resolveListItem(String listName, String itemName, ParsedObject source) { + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition listDef = null; + + if (listName != null) { + listDef = listDefs.get(listName); + if (listDef == null) { + return null; + } + + return listDef.itemNamed(itemName); + } + + ListElementDefinition foundItem = null; + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition originalFoundList = null; + + for (Map.Entry namedList : + listDefs.entrySet()) { + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition listToSearch = namedList.getValue(); + ListElementDefinition itemInThisList = listToSearch.itemNamed(itemName); + if (itemInThisList != null) { + if (foundItem != null) { + error( + "Ambiguous item name '" + itemName + "' found in multiple sets, including " + + originalFoundList.identifier + " and " + listToSearch.identifier, + source, + false); + } else { + foundItem = itemInThisList; + originalFoundList = listToSearch; + } + } + } + + return foundItem; + } + + private void flattenContainersIn(Container container) { + HashSet innerContainers = new HashSet<>(); + + for (com.bladecoder.ink.runtime.RTObject c : container.getContent()) { + Container innerContainer = c instanceof Container ? (Container) c : null; + if (innerContainer != null) { + innerContainers.add(innerContainer); + } + } + + if (container.getNamedContent() != null) { + for (com.bladecoder.ink.runtime.INamedContent namedContent : + container.getNamedContent().values()) { + Container namedInnerContainer = namedContent instanceof Container ? (Container) namedContent : null; + if (namedInnerContainer != null) { + innerContainers.add(namedInnerContainer); + } + } + } + + for (Container innerContainer : innerContainers) { + tryFlattenContainer(innerContainer); + flattenContainersIn(innerContainer); + } + } + + private void tryFlattenContainer(Container container) { + if (!container.getNamedContent().isEmpty() + || container.hasValidName() + || dontFlattenContainers.contains(container)) { + return; + } + + Container parentContainer = container.getParent(); + if (parentContainer != null) { + int contentIdx = parentContainer.getContent().indexOf(container); + parentContainer.getContent().remove(contentIdx); + + com.bladecoder.ink.runtime.DebugMetadata dm = container.getOwnDebugMetadata(); + + for (com.bladecoder.ink.runtime.RTObject innerContent : container.getContent()) { + innerContent.setParent(null); + if (dm != null && innerContent.getOwnDebugMetadata() == null) { + innerContent.setDebugMetadata(dm); + } + try { + parentContainer.insertContent(innerContent, contentIdx); + } catch (Exception e) { + throw new RuntimeException(e); + } + contentIdx++; + } + } + } + + @Override + public void error(String message, ParsedObject source, boolean isWarning) { + ErrorType errorType = isWarning ? ErrorType.Warning : ErrorType.Error; + + StringBuilder sb = new StringBuilder(); + if (source instanceof AuthorWarning) { + sb.append("TODO: "); + errorType = ErrorType.Author; + } else if (isWarning) { + sb.append("WARNING: "); + } else { + sb.append("ERROR: "); + } + + if (source != null && source.getDebugMetadata() != null && source.getDebugMetadata().startLineNumber >= 1) { + if (source.getDebugMetadata().fileName != null) { + sb.append("'").append(source.getDebugMetadata().fileName).append("' "); + } + + sb.append("line ").append(source.getDebugMetadata().startLineNumber).append(": "); + } + + sb.append(message); + + String fullMessage = sb.toString(); + + if (errorHandler != null) { + hadError = errorType == ErrorType.Error; + hadWarning = errorType == ErrorType.Warning; + errorHandler.error(fullMessage, errorType); + } else { + throw new RuntimeException(fullMessage); + } + } + + public void resetError() { + hadError = false; + hadWarning = false; + } + + public boolean hadError() { + return hadError; + } + + public boolean hadWarning() { + return hadWarning; + } + + public boolean isExternal(String namedFuncTarget) { + return externals.containsKey(namedFuncTarget); + } + + public void addExternal(ExternalDeclaration decl) { + if (externals.containsKey(decl.getName())) { + error("Duplicate EXTERNAL definition of '" + decl.getName() + "'", decl, false); + } else { + externals.put(decl.getName(), decl); + } + } + + public void dontFlattenContainer(Container container) { + dontFlattenContainers.add(container); + } + + private void nameConflictError(ParsedObject obj, String name, ParsedObject existingObj, String typeNameToPrint) { + obj.error(typeNameToPrint + " '" + name + "': name has already been used for a " + + existingObj.getTypeName().toLowerCase() + " on " + existingObj.getDebugMetadata()); + } + + public static boolean isReservedKeyword(String name) { + if (name == null) { + return false; + } + switch (name) { + case "true": + case "false": + case "not": + case "return": + case "else": + case "VAR": + case "CONST": + case "temp": + case "LIST": + case "function": + return true; + default: + return false; + } + } + + public enum SymbolType { + Knot, + List, + ListItem, + Var, + SubFlowAndWeave, + Arg, + Temp + } + + public void checkForNamingCollisions( + ParsedObject obj, Identifier identifier, SymbolType symbolType, String typeNameOverride) { + String typeNameToPrint = typeNameOverride != null ? typeNameOverride : obj.getTypeName(); + if (isReservedKeyword(identifier != null ? identifier.name : null)) { + obj.error("'" + (identifier != null ? identifier.name : null) + "' cannot be used for the name of a " + + typeNameToPrint.toLowerCase() + " because it's a reserved keyword"); + return; + } + + if (FunctionCall.isBuiltIn(identifier != null ? identifier.name : null)) { + obj.error("'" + (identifier != null ? identifier.name : null) + "' cannot be used for the name of a " + + typeNameToPrint.toLowerCase() + " because it's a built in function"); + return; + } + + FlowBase knotOrFunction = + (FlowBase) contentWithNameAtLevel(identifier != null ? identifier.name : null, FlowLevel.Knot, false); + if (knotOrFunction != null && (knotOrFunction != obj || symbolType == SymbolType.Arg)) { + nameConflictError(obj, identifier != null ? identifier.name : null, knotOrFunction, typeNameToPrint); + return; + } + + if (symbolType.ordinal() < SymbolType.List.ordinal()) { + return; + } + + for (Map.Entry namedListDef : + listDefs.entrySet()) { + String listDefName = namedListDef.getKey(); + com.bladecoder.ink.compiler.ParsedHierarchy.ListDefinition listDef = namedListDef.getValue(); + if (identifier != null + && identifier.name.equals(listDefName) + && obj != listDef + && listDef.variableAssignment != obj) { + nameConflictError(obj, identifier.name, listDef, typeNameToPrint); + } + + if (!(obj instanceof ListElementDefinition)) { + for (ListElementDefinition item : listDef.itemDefinitions) { + if (identifier != null && identifier.name.equals(item.getName())) { + nameConflictError(obj, identifier.name, item, typeNameToPrint); + } + } + } + } + + if (symbolType.ordinal() <= SymbolType.Var.ordinal()) { + return; + } + + VariableAssignment varDecl = variableDeclarations.get(identifier != null ? identifier.name : null); + if (varDecl != null && varDecl != obj && varDecl.isGlobalDeclaration && varDecl.listDefinition == null) { + nameConflictError(obj, identifier.name, varDecl, typeNameToPrint); + } + + if (symbolType.ordinal() < SymbolType.SubFlowAndWeave.ordinal()) { + return; + } + + Path path = new Path(identifier); + ParsedObject targetContent = path.resolveFromContext(obj); + if (targetContent != null && targetContent != obj) { + nameConflictError(obj, identifier.name, targetContent, typeNameToPrint); + return; + } + + if (symbolType.ordinal() < SymbolType.Arg.ordinal()) { + return; + } + + if (symbolType != SymbolType.Arg) { + FlowBase flow = obj instanceof FlowBase ? (FlowBase) obj : obj.closestFlowBase(); + if (flow != null && flow.hasParameters()) { + for (FlowBase.Argument arg : flow.getArguments()) { + if (arg.identifier != null && identifier != null && identifier.name.equals(arg.identifier.name)) { + obj.error(typeNameToPrint + " '" + identifier.name + + "': Name has already been used for a argument to " + flow.getIdentifier() + " on " + + flow.getDebugMetadata()); + return; + } + } + } + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/StringExpression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/StringExpression.java new file mode 100644 index 0000000..796522d --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/StringExpression.java @@ -0,0 +1,59 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import java.util.List; + +public class StringExpression extends Expression { + public boolean isSingleString() { + if (content == null || content.size() != 1) { + return false; + } + + return content.get(0) instanceof Text; + } + + public StringExpression(List content) { + addContent(content); + } + + @Override + public void generateIntoContainer(Container container) { + RuntimeUtils.addContent(container, ControlCommand.CommandType.BeginString); + + for (ParsedObject c : content) { + RuntimeUtils.addContent(container, c.getRuntimeObject()); + } + + RuntimeUtils.addContent(container, ControlCommand.CommandType.EndString); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (ParsedObject c : content) { + sb.append(c.toString()); + } + return sb.toString(); + } + + @Override + public boolean equals(java.lang.Object obj) { + StringExpression otherStr = obj instanceof StringExpression ? (StringExpression) obj : null; + if (otherStr == null) { + return false; + } + + if (!isSingleString() || !otherStr.isSingleString()) { + return false; + } + + return toString().equals(otherStr.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Tag.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Tag.java new file mode 100644 index 0000000..8235442 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Tag.java @@ -0,0 +1,21 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.ControlCommand; + +public class Tag extends ParsedObject { + public boolean isStart; + public boolean inChoice; + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + if (isStart) { + return new ControlCommand(ControlCommand.CommandType.BeginTag); + } + return new ControlCommand(ControlCommand.CommandType.EndTag); + } + + @Override + public String toString() { + return isStart ? "#StartTag" : "#EndTag"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Text.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Text.java new file mode 100644 index 0000000..b0f2473 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Text.java @@ -0,0 +1,29 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.runtime.StringValue; + +public class Text extends ParsedObject { + private String text; + + public Text(String str) { + text = str; + } + + public String getText() { + return text; + } + + public void setText(String value) { + text = value; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + return new StringValue(text); + } + + @Override + public String toString() { + return text; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/TunnelOnwards.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/TunnelOnwards.java new file mode 100644 index 0000000..538d042 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/TunnelOnwards.java @@ -0,0 +1,63 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class TunnelOnwards extends ParsedObject { + public Divert divertAfter; + private com.bladecoder.ink.runtime.Divert runtimeDivert; + private com.bladecoder.ink.runtime.DivertTargetValue runtimeDivertTargetValue; + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + com.bladecoder.ink.runtime.Container container = new com.bladecoder.ink.runtime.Container(); + + com.bladecoder.ink.compiler.RuntimeUtils.addContent( + container, com.bladecoder.ink.runtime.ControlCommand.CommandType.EvalStart); + + if (divertAfter != null) { + divertAfter.generateRuntimeObject(); + runtimeDivert = divertAfter.runtimeDivert; + runtimeDivertTargetValue = new com.bladecoder.ink.runtime.DivertTargetValue(); + com.bladecoder.ink.compiler.RuntimeUtils.addContent(container, runtimeDivertTargetValue); + } else { + com.bladecoder.ink.compiler.RuntimeUtils.addContent(container, new com.bladecoder.ink.runtime.Void()); + } + + com.bladecoder.ink.compiler.RuntimeUtils.addContent( + container, com.bladecoder.ink.runtime.ControlCommand.CommandType.EvalEnd); + com.bladecoder.ink.compiler.RuntimeUtils.addContent( + container, com.bladecoder.ink.runtime.ControlCommand.CommandType.PopTunnel); + return container; + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (divertAfter == null) { + return; + } + + divertAfter.resolveReferences(context); + + if (divertAfter.isDone() || divertAfter.isEnd()) { + error("Can't use -> DONE or -> END as tunnel onwards targets", this, false); + return; + } + + if (runtimeDivert != null && runtimeDivert.hasVariableTarget()) { + error( + "Since '" + divertAfter.target.getDotSeparatedComponents() + + "' is a variable, it shouldn't be preceded by '->' here.", + this, + false); + return; + } + + if (runtimeDivertTargetValue != null && runtimeDivert != null) { + try { + runtimeDivertTargetValue.setTargetPath(runtimeDivert.getTargetPath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/UnaryExpression.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/UnaryExpression.java new file mode 100644 index 0000000..e624e9f --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/UnaryExpression.java @@ -0,0 +1,63 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.NativeFunctionCall; + +public class UnaryExpression extends Expression { + public Expression innerExpression; + public String op; + + public static Expression withInner(Expression inner, String op) { + Number innerNumber = inner instanceof Number ? (Number) inner : null; + if (innerNumber != null) { + java.lang.Object innerValue = innerNumber.value; + + if (op.equals("-")) { + if (innerValue instanceof Integer) { + return new Number(-((Integer) innerValue)); + } else if (innerValue instanceof Float) { + return new Number(-((Float) innerValue)); + } + } else if (op.equals("!") || op.equals("not")) { + if (innerValue instanceof Integer) { + return new Number(((Integer) innerValue) == 0); + } else if (innerValue instanceof Float) { + return new Number(((Float) innerValue) == 0.0f); + } else if (innerValue instanceof Boolean) { + return new Number(!((Boolean) innerValue)); + } + } + + throw new RuntimeException("Unexpected operation or number type"); + } + + return new UnaryExpression(inner, op); + } + + public UnaryExpression(Expression inner, String op) { + this.innerExpression = addContent(inner); + this.op = op; + } + + @Override + public void generateIntoContainer(Container container) { + innerExpression.generateIntoContainer(container); + RuntimeUtils.addContent(container, NativeFunctionCall.callWithName(nativeNameForOp())); + } + + @Override + public String toString() { + return nativeNameForOp() + innerExpression; + } + + private String nativeNameForOp() { + if (op.equals("-")) { + return "_"; + } + if (op.equals("not")) { + return "!"; + } + return op; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableAssignment.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableAssignment.java new file mode 100644 index 0000000..a1bba93 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableAssignment.java @@ -0,0 +1,130 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; + +public class VariableAssignment extends ParsedObject { + public Identifier variableIdentifier; + public Expression expression; + public ListDefinition listDefinition; + + public boolean isGlobalDeclaration; + public boolean isNewTemporaryDeclaration; + + private com.bladecoder.ink.runtime.VariableAssignment runtimeAssignment; + + public VariableAssignment(Identifier identifier, Expression assignedExpression) { + this.variableIdentifier = identifier; + + if (assignedExpression != null) { + this.expression = addContent(assignedExpression); + } + } + + public VariableAssignment(Identifier identifier, ListDefinition listDef) { + this.variableIdentifier = identifier; + + if (listDef != null) { + this.listDefinition = addContent(listDef); + this.listDefinition.variableAssignment = this; + } + + isGlobalDeclaration = true; + } + + public String getVariableName() { + return variableIdentifier.name; + } + + public boolean isDeclaration() { + return isGlobalDeclaration || isNewTemporaryDeclaration; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + FlowBase newDeclScope = null; + if (isGlobalDeclaration) { + newDeclScope = getStory(); + } else if (isNewTemporaryDeclaration) { + newDeclScope = closestFlowBase(); + } + + if (newDeclScope != null) { + newDeclScope.tryAddNewVariableDeclaration(this); + } + + if (isGlobalDeclaration) { + return null; + } + + Container container = new Container(); + + if (expression != null) { + RuntimeUtils.addContent(container, expression.getRuntimeObject()); + } else if (listDefinition != null) { + RuntimeUtils.addContent(container, listDefinition.getRuntimeObject()); + } + + try { + runtimeAssignment = + new com.bladecoder.ink.runtime.VariableAssignment(getVariableName(), isNewTemporaryDeclaration); + RuntimeUtils.addContent(container, runtimeAssignment); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return container; + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (isDeclaration() && listDefinition == null) { + context.checkForNamingCollisions( + this, variableIdentifier, isGlobalDeclaration ? Story.SymbolType.Var : Story.SymbolType.Temp, null); + } + + if (isGlobalDeclaration) { + VariableReference variableReference = + expression instanceof VariableReference ? (VariableReference) expression : null; + if (variableReference != null + && !variableReference.isConstantReference + && !variableReference.isListItemReference) { + error( + "global variable assignments cannot refer to other variables, only literal values, constants and list items"); + } + } + + if (!isNewTemporaryDeclaration) { + FlowBase.VariableResolveResult resolvedVarAssignment = + context.resolveVariableWithName(getVariableName(), this); + if (!resolvedVarAssignment.found) { + if (getStory().constants.containsKey(getVariableName())) { + error( + "Can't re-assign to a constant (do you need to use VAR when declaring '" + getVariableName() + + "'?)", + this, + false); + } else { + error("Variable could not be found to assign to: '" + getVariableName() + "'", this, false); + } + } + + if (runtimeAssignment != null) { + runtimeAssignment.setIsGlobal(resolvedVarAssignment.isGlobal); + } + } + } + + @Override + public String getTypeName() { + if (isNewTemporaryDeclaration) { + return "temp"; + } + if (isGlobalDeclaration) { + return "VAR"; + } + return "variable assignment"; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableReference.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableReference.java new file mode 100644 index 0000000..830d264 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/VariableReference.java @@ -0,0 +1,144 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import java.util.ArrayList; +import java.util.List; + +public class VariableReference extends Expression { + private final String name; + private Identifier singleIdentifier; + + public List pathIdentifiers; + public List path; + + public boolean isConstantReference; + public boolean isListItemReference; + + private com.bladecoder.ink.runtime.VariableReference runtimeVarRef; + + public VariableReference(List pathIdentifiers) { + this.pathIdentifiers = pathIdentifiers; + this.path = new ArrayList<>(); + for (Identifier id : pathIdentifiers) { + this.path.add(id != null ? id.name : null); + } + this.name = String.join(".", this.path); + } + + public String getName() { + return name; + } + + public Identifier getIdentifier() { + if (pathIdentifiers == null || pathIdentifiers.isEmpty()) { + return null; + } + + if (singleIdentifier == null) { + String joinedName = String.join(".", path); + Identifier first = pathIdentifiers.get(0); + com.bladecoder.ink.runtime.DebugMetadata debugMetadata = first != null ? first.debugMetadata : null; + for (Identifier id : pathIdentifiers) { + if (id != null && id.debugMetadata != null && debugMetadata != null) { + debugMetadata = debugMetadata.merge(id.debugMetadata); + } else if (debugMetadata == null && id != null) { + debugMetadata = id.debugMetadata; + } + } + singleIdentifier = new Identifier(); + singleIdentifier.name = joinedName; + singleIdentifier.debugMetadata = debugMetadata; + } + + return singleIdentifier; + } + + public com.bladecoder.ink.runtime.VariableReference getRuntimeVarRef() { + return runtimeVarRef; + } + + @Override + public void generateIntoContainer(com.bladecoder.ink.runtime.Container container) { + Expression constantValue = getStory().constants.get(name); + + if (constantValue != null) { + constantValue.generateConstantIntoContainer(container); + isConstantReference = true; + return; + } + + runtimeVarRef = new com.bladecoder.ink.runtime.VariableReference(name); + + if (path.size() == 1 || path.size() == 2) { + String listItemName; + String listName = null; + + if (path.size() == 1) { + listItemName = path.get(0); + } else { + listName = path.get(0); + listItemName = path.get(1); + } + + ListElementDefinition listItem = getStory().resolveListItem(listName, listItemName, this); + if (listItem != null) { + isListItemReference = true; + } + } + + RuntimeUtils.addContent(container, runtimeVarRef); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (isConstantReference || isListItemReference) { + return; + } + + Path parsedPath = new Path(pathIdentifiers); + ParsedObject targetForCount = parsedPath.resolveFromContext(this); + if (targetForCount != null) { + targetForCount.getContainerForCounting().setVisitsShouldBeCounted(true); + + if (runtimeVarRef == null) { + return; + } + + runtimeVarRef.setPathForCount(targetForCount.getRuntimePath()); + runtimeVarRef.setName(null); + + FlowBase targetFlow = targetForCount instanceof FlowBase ? (FlowBase) targetForCount : null; + if (targetFlow != null && targetFlow.isFunction()) { + if (parent instanceof Weave || parent instanceof ContentList || parent instanceof FlowBase) { + warning( + "'" + targetFlow.getIdentifier() + + "' being used as read count rather than being called as function. Perhaps you intended to write " + + targetFlow.getName() + "()", + this); + } + } + + return; + } + + if (path.size() > 1) { + String errorMsg = "Could not find target for read count: " + parsedPath; + if (path.size() <= 2) { + errorMsg += ", or couldn't find list item with the name " + String.join(",", path); + } + error(errorMsg); + return; + } + + if (!context.resolveVariableWithName(name, this).found) { + error("Unresolved variable: " + toString(), this, false); + } + } + + @Override + public String toString() { + return String.join(".", path); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Weave.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Weave.java new file mode 100644 index 0000000..732a6fa --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Weave.java @@ -0,0 +1,579 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +import com.bladecoder.ink.compiler.RuntimeUtils; +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.Divert; +import com.bladecoder.ink.runtime.RTObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Used by the FlowBase when constructing the weave flow from a flat list of content objects. +public class Weave extends ParsedObject { + public Container rootContainer; + private Container currentContainer; + + public int baseIndentIndex; + + public List looseEnds; + + public List gatherPointsToResolve; + + public static class GatherPointToResolve { + public Divert divert; + public RTObject targetRuntimeObj; + } + + public ParsedObject getLastParsedSignificantObject() { + if (content == null || content.isEmpty()) { + return null; + } + + ParsedObject lastObject = null; + for (int i = content.size() - 1; i >= 0; --i) { + lastObject = content.get(i); + + Text lastText = lastObject instanceof Text ? (Text) lastObject : null; + if (lastText != null && "\n".equals(lastText.getText())) { + continue; + } + + if (isGlobalDeclaration(lastObject)) { + continue; + } + + break; + } + + Weave lastWeave = lastObject instanceof Weave ? (Weave) lastObject : null; + if (lastWeave != null) { + lastObject = lastWeave.getLastParsedSignificantObject(); + } + + return lastObject; + } + + public Weave(List cont, int indentIndex) { + if (indentIndex == -1) { + baseIndentIndex = determineBaseIndentationFromContent(cont); + } else { + baseIndentIndex = indentIndex; + } + + addContent(cont); + + constructWeaveHierarchyFromIndentation(); + } + + public Weave(List cont) { + this(cont, -1); + } + + public void resolveWeavePointNaming() { + List namedWeavePoints = findAll( + IWeavePoint.class, w -> w.getName() != null && !w.getName().isEmpty()); + + namedWeavePointsByName = new HashMap<>(); + + for (IWeavePoint weavePoint : namedWeavePoints) { + IWeavePoint existingWeavePoint = namedWeavePointsByName.get(weavePoint.getName()); + if (existingWeavePoint != null) { + String typeName = existingWeavePoint instanceof Gather ? "gather" : "choice"; + ParsedObject existingObj = (ParsedObject) existingWeavePoint; + + error( + "A " + typeName + " with the same label name '" + weavePoint.getName() + + "' already exists in this context on line " + + existingObj.getDebugMetadata().startLineNumber, + (ParsedObject) weavePoint, + false); + } + + namedWeavePointsByName.put(weavePoint.getName(), weavePoint); + } + } + + private void constructWeaveHierarchyFromIndentation() { + if (content == null) { + return; + } + + int contentIdx = 0; + while (contentIdx < content.size()) { + ParsedObject obj = content.get(contentIdx); + + if (obj instanceof IWeavePoint) { + IWeavePoint weavePoint = (IWeavePoint) obj; + int weaveIndentIdx = weavePoint.getIndentationDepth() - 1; + + if (weaveIndentIdx > baseIndentIndex) { + int innerWeaveStartIdx = contentIdx; + while (contentIdx < content.size()) { + IWeavePoint innerWeaveObj = content.get(contentIdx) instanceof IWeavePoint + ? (IWeavePoint) content.get(contentIdx) + : null; + if (innerWeaveObj != null) { + int innerIndentIdx = innerWeaveObj.getIndentationDepth() - 1; + if (innerIndentIdx <= baseIndentIndex) { + break; + } + } + + contentIdx++; + } + + int weaveContentCount = contentIdx - innerWeaveStartIdx; + + List weaveContent = new ArrayList<>( + content.subList(innerWeaveStartIdx, innerWeaveStartIdx + weaveContentCount)); + content.subList(innerWeaveStartIdx, innerWeaveStartIdx + weaveContentCount) + .clear(); + + Weave weave = new Weave(weaveContent, weaveIndentIdx); + insertContent(innerWeaveStartIdx, weave); + + contentIdx = innerWeaveStartIdx; + } + } + + contentIdx++; + } + } + + public int determineBaseIndentationFromContent(List contentList) { + for (ParsedObject obj : contentList) { + if (obj instanceof IWeavePoint) { + return ((IWeavePoint) obj).getIndentationDepth() - 1; + } + } + + return 0; + } + + @Override + public RTObject generateRuntimeObject() { + rootContainer = currentContainer = new Container(); + looseEnds = new ArrayList<>(); + + gatherPointsToResolve = new ArrayList<>(); + + if (content != null) { + for (ParsedObject obj : content) { + if (obj instanceof IWeavePoint) { + addRuntimeForWeavePoint((IWeavePoint) obj); + } else { + if (obj instanceof Weave) { + Weave weave = (Weave) obj; + addRuntimeForNestedWeave(weave); + if (weave.gatherPointsToResolve != null) { + gatherPointsToResolve.addAll(weave.gatherPointsToResolve); + } + } else { + addGeneralRuntimeContent(obj.getRuntimeObject()); + } + } + } + } + + passLooseEndsToAncestors(); + + return rootContainer; + } + + private void addRuntimeForGather(Gather gather) { + boolean autoEnter = !hasSeenChoiceInSection; + hasSeenChoiceInSection = false; + + Container gatherContainer = gather.getRuntimeContainer(); + + if (gather.getName() == null) { + gatherContainer.setName("g-" + unnamedGatherCount); + unnamedGatherCount++; + } + + if (autoEnter) { + RuntimeUtils.addContent(currentContainer, gatherContainer); + } else { + rootContainer.addToNamedContentOnly(gatherContainer); + } + + for (IWeavePoint looseEndWeavePoint : looseEnds) { + ParsedObject looseEnd = (ParsedObject) looseEndWeavePoint; + + if (looseEnd instanceof Gather) { + Gather prevGather = (Gather) looseEnd; + if (prevGather.getIndentationDepth() == gather.getIndentationDepth()) { + continue; + } + } + + Divert divert; + + if (looseEnd instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert) { + divert = (Divert) looseEnd.getRuntimeObject(); + } else { + divert = new Divert(); + RuntimeUtils.addContent(looseEndWeavePoint.getRuntimeContainer(), divert); + } + + GatherPointToResolve gatherPoint = new GatherPointToResolve(); + gatherPoint.divert = divert; + gatherPoint.targetRuntimeObj = gatherContainer; + gatherPointsToResolve.add(gatherPoint); + } + looseEnds.clear(); + + currentContainer = gatherContainer; + } + + private void addRuntimeForWeavePoint(IWeavePoint weavePoint) { + if (weavePoint instanceof Gather) { + addRuntimeForGather((Gather) weavePoint); + } else if (weavePoint instanceof Choice) { + if (previousWeavePoint instanceof Gather) { + looseEnds.remove(previousWeavePoint); + } + + Choice choice = (Choice) weavePoint; + RuntimeUtils.addContent(currentContainer, choice.getRuntimeObject()); + + choice.getInnerContentContainer().setName("c-" + choiceCount); + currentContainer.addToNamedContentOnly(choice.getInnerContentContainer()); + choiceCount++; + + hasSeenChoiceInSection = true; + } + + addContentToPreviousWeavePoint = false; + if (weavePointHasLooseEnd(weavePoint)) { + looseEnds.add(weavePoint); + + Choice looseChoice = weavePoint instanceof Choice ? (Choice) weavePoint : null; + if (looseChoice != null) { + addContentToPreviousWeavePoint = true; + } + } + previousWeavePoint = weavePoint; + } + + public void addRuntimeForNestedWeave(Weave nestedResult) { + RTObject nestedRuntime = nestedResult.getRuntimeObject(); + addGeneralRuntimeContent(nestedRuntime); + + if (previousWeavePoint != null) { + looseEnds.remove(previousWeavePoint); + addContentToPreviousWeavePoint = false; + } + } + + private void addGeneralRuntimeContent(RTObject contentObj) { + if (contentObj == null) { + return; + } + + if (addContentToPreviousWeavePoint) { + RuntimeUtils.addContent(previousWeavePoint.getRuntimeContainer(), contentObj); + } else { + RuntimeUtils.addContent(currentContainer, contentObj); + } + } + + private void passLooseEndsToAncestors() { + if (looseEnds.isEmpty()) { + return; + } + + Weave closestInnerWeaveAncestor = null; + Weave closestOuterWeaveAncestor = null; + + boolean nested = false; + for (ParsedObject ancestor = parent; ancestor != null; ancestor = ancestor.parent) { + Weave weaveAncestor = ancestor instanceof Weave ? (Weave) ancestor : null; + if (weaveAncestor != null) { + if (!nested && closestInnerWeaveAncestor == null) { + closestInnerWeaveAncestor = weaveAncestor; + } + + if (nested && closestOuterWeaveAncestor == null) { + closestOuterWeaveAncestor = weaveAncestor; + } + } + + if (ancestor instanceof Sequence || ancestor instanceof Conditional) { + nested = true; + } + } + + if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null) { + return; + } + + for (int i = looseEnds.size() - 1; i >= 0; i--) { + IWeavePoint looseEnd = looseEnds.get(i); + + boolean received = false; + + if (nested) { + if (looseEnd instanceof Choice && closestInnerWeaveAncestor != null) { + closestInnerWeaveAncestor.receiveLooseEnd(looseEnd); + received = true; + } else if (!(looseEnd instanceof Choice)) { + Weave receivingWeave = + closestInnerWeaveAncestor != null ? closestInnerWeaveAncestor : closestOuterWeaveAncestor; + if (receivingWeave != null) { + receivingWeave.receiveLooseEnd(looseEnd); + received = true; + } + } + } else { + closestInnerWeaveAncestor.receiveLooseEnd(looseEnd); + received = true; + } + + if (received) { + looseEnds.remove(i); + } + } + } + + private void receiveLooseEnd(IWeavePoint childWeaveLooseEnd) { + looseEnds.add(childWeaveLooseEnd); + } + + @Override + public void resolveReferences(Story context) { + super.resolveReferences(context); + + if (looseEnds != null && !looseEnds.isEmpty()) { + boolean isNestedWeave = false; + for (ParsedObject ancestor = parent; ancestor != null; ancestor = ancestor.parent) { + if (ancestor instanceof Sequence || ancestor instanceof Conditional) { + isNestedWeave = true; + break; + } + } + if (isNestedWeave) { + validateTermination(this::badNestedTerminationHandler); + } + } + + for (GatherPointToResolve gatherPoint : gatherPointsToResolve) { + gatherPoint.divert.setTargetPath(gatherPoint.targetRuntimeObj.getPath()); + } + + checkForWeavePointNamingCollisions(); + } + + public IWeavePoint weavePointNamed(String name) { + if (namedWeavePointsByName == null) { + return null; + } + + return namedWeavePointsByName.get(name); + } + + private boolean isGlobalDeclaration(ParsedObject obj) { + VariableAssignment varAss = obj instanceof VariableAssignment ? (VariableAssignment) obj : null; + if (varAss != null && varAss.isGlobalDeclaration && varAss.isDeclaration()) { + return true; + } + + ConstantDeclaration constDecl = obj instanceof ConstantDeclaration ? (ConstantDeclaration) obj : null; + if (constDecl != null) { + return true; + } + + return false; + } + + private List contentThatFollowsWeavePoint(IWeavePoint weavePoint) { + ParsedObject obj = (ParsedObject) weavePoint; + + List result = new ArrayList<>(); + + if (obj.getContent() != null) { + for (ParsedObject contentObj : obj.getContent()) { + if (isGlobalDeclaration(contentObj)) { + continue; + } + result.add(contentObj); + } + } + + Weave parentWeave = obj.parent instanceof Weave ? (Weave) obj.parent : null; + if (parentWeave == null) { + throw new RuntimeException("Expected weave point parent to be weave?"); + } + + int weavePointIdx = parentWeave.content.indexOf(obj); + + for (int i = weavePointIdx + 1; i < parentWeave.content.size(); i++) { + ParsedObject laterObj = parentWeave.content.get(i); + + if (isGlobalDeclaration(laterObj)) { + continue; + } + + if (laterObj instanceof IWeavePoint) { + break; + } + + if (laterObj instanceof Weave) { + break; + } + + result.add(laterObj); + } + + return result; + } + + public interface BadTerminationHandler { + void onBadTermination(ParsedObject terminatingObj); + } + + public void validateTermination(BadTerminationHandler badTerminationHandler) { + if (getLastParsedSignificantObject() instanceof AuthorWarning) { + return; + } + + boolean hasLooseEnds = looseEnds != null && !looseEnds.isEmpty(); + + if (hasLooseEnds) { + for (IWeavePoint looseEnd : looseEnds) { + List looseEndFlow = contentThatFollowsWeavePoint(looseEnd); + validateFlowOfObjectsTerminates(looseEndFlow, (ParsedObject) looseEnd, badTerminationHandler); + } + } else { + if (content != null) { + for (ParsedObject obj : content) { + if (obj instanceof IWeavePoint) { + return; + } + } + } + + validateFlowOfObjectsTerminates(content, this, badTerminationHandler); + } + } + + private void badNestedTerminationHandler(ParsedObject terminatingObj) { + Conditional conditional = null; + for (ParsedObject ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) { + if (ancestor instanceof Sequence || ancestor instanceof Conditional) { + conditional = ancestor instanceof Conditional ? (Conditional) ancestor : null; + break; + } + } + + String errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards."; + + if (conditional != null) { + int numChoices = conditional.findAll(Choice.class).size(); + if (numChoices == 1) { + errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, " + + errorMsg.toLowerCase(); + } + } + + error(errorMsg, terminatingObj, false); + } + + private void validateFlowOfObjectsTerminates( + List objFlow, ParsedObject defaultObj, BadTerminationHandler badTerminationHandler) { + boolean terminated = false; + ParsedObject terminatingObj = defaultObj; + if (objFlow != null) { + for (ParsedObject flowObj : objFlow) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert divert = flowObj.find( + com.bladecoder.ink.compiler.ParsedHierarchy.Divert.class, + d -> !d.isThread && !d.isTunnel && !d.isFunctionCall && !(d.parent instanceof DivertTarget)); + if (divert != null) { + terminated = true; + } + + if (flowObj.find(TunnelOnwards.class) != null) { + terminated = true; + break; + } + + terminatingObj = flowObj; + } + } + + if (!terminated) { + if (terminatingObj instanceof AuthorWarning) { + return; + } + + badTerminationHandler.onBadTermination(terminatingObj); + } + } + + private boolean weavePointHasLooseEnd(IWeavePoint weavePoint) { + if (weavePoint.getContent() == null) { + return true; + } + + List weaveContent = weavePoint.getContent(); + for (int i = weaveContent.size() - 1; i >= 0; --i) { + com.bladecoder.ink.compiler.ParsedHierarchy.Divert innerDivert = + weaveContent.get(i) instanceof com.bladecoder.ink.compiler.ParsedHierarchy.Divert + ? (com.bladecoder.ink.compiler.ParsedHierarchy.Divert) weaveContent.get(i) + : null; + if (innerDivert != null) { + boolean willReturn = innerDivert.isThread || innerDivert.isTunnel || innerDivert.isFunctionCall; + if (!willReturn) { + return false; + } + } + } + + return true; + } + + private void checkForWeavePointNamingCollisions() { + if (namedWeavePointsByName == null) { + return; + } + + List ancestorFlows = new ArrayList<>(); + for (ParsedObject obj : getAncestry()) { + FlowBase flow = obj instanceof FlowBase ? (FlowBase) obj : null; + if (flow != null) { + ancestorFlows.add(flow); + } else { + break; + } + } + + for (Map.Entry namedWeavePointPair : namedWeavePointsByName.entrySet()) { + String weavePointName = namedWeavePointPair.getKey(); + ParsedObject weavePoint = (ParsedObject) namedWeavePointPair.getValue(); + + for (FlowBase flow : ancestorFlows) { + ParsedObject otherContentWithName = flow.contentWithNameAtLevel(weavePointName, null, false); + + if (otherContentWithName != null && otherContentWithName != weavePoint) { + String errorMsg = String.format( + "%s '%s' has the same label name as a %s (on %s)", + weavePoint.getClass().getSimpleName(), + weavePointName, + otherContentWithName.getClass().getSimpleName(), + otherContentWithName.getDebugMetadata()); + + error(errorMsg, weavePoint, false); + } + } + } + } + + private IWeavePoint previousWeavePoint; + private boolean addContentToPreviousWeavePoint; + private boolean hasSeenChoiceInSection; + private int unnamedGatherCount; + private int choiceCount; + + private Map namedWeavePointsByName; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Wrap.java b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Wrap.java new file mode 100644 index 0000000..fe01658 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/ParsedHierarchy/Wrap.java @@ -0,0 +1,26 @@ +package com.bladecoder.ink.compiler.ParsedHierarchy; + +public class Wrap extends ParsedObject { + private final T objToWrap; + + public Wrap(T objToWrap) { + this.objToWrap = objToWrap; + } + + @Override + public com.bladecoder.ink.runtime.RTObject generateRuntimeObject() { + return objToWrap; + } + + public static class Glue extends Wrap { + public Glue(com.bladecoder.ink.runtime.Glue glue) { + super(glue); + } + } + + public static class LegacyTag extends Wrap { + public LegacyTag(com.bladecoder.ink.runtime.Tag tag) { + super(tag); + } + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/PluginManager.java b/compiler/src/main/java/com/bladecoder/ink/compiler/PluginManager.java new file mode 100644 index 0000000..7c73632 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/PluginManager.java @@ -0,0 +1,25 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.compiler.ParsedHierarchy.Story; +import java.util.List; + +public class PluginManager { + public PluginManager(List pluginDirectories) { + this.pluginDirectories = pluginDirectories; + } + + public String preParse(String input) { + return input; + } + + public Story postParse(Story story) { + return story; + } + + public com.bladecoder.ink.runtime.Story postExport( + Story parsedStory, com.bladecoder.ink.runtime.Story runtimeStory) { + return runtimeStory; + } + + private final List pluginDirectories; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/RuntimeUtils.java b/compiler/src/main/java/com/bladecoder/ink/compiler/RuntimeUtils.java new file mode 100644 index 0000000..0a13db5 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/RuntimeUtils.java @@ -0,0 +1,21 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.runtime.Container; +import com.bladecoder.ink.runtime.ControlCommand; +import com.bladecoder.ink.runtime.RTObject; + +public final class RuntimeUtils { + private RuntimeUtils() {} + + public static void addContent(Container container, RTObject obj) { + try { + container.addContent(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void addContent(Container container, ControlCommand.CommandType commandType) { + addContent(container, new ControlCommand(commandType)); + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/Stats.java b/compiler/src/main/java/com/bladecoder/ink/compiler/Stats.java new file mode 100644 index 0000000..2aac441 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/Stats.java @@ -0,0 +1,66 @@ +package com.bladecoder.ink.compiler; + +import com.bladecoder.ink.compiler.ParsedHierarchy.Choice; +import com.bladecoder.ink.compiler.ParsedHierarchy.Divert; +import com.bladecoder.ink.compiler.ParsedHierarchy.Gather; +import com.bladecoder.ink.compiler.ParsedHierarchy.Knot; +import com.bladecoder.ink.compiler.ParsedHierarchy.Stitch; +import com.bladecoder.ink.compiler.ParsedHierarchy.Story; +import com.bladecoder.ink.compiler.ParsedHierarchy.Text; +import java.util.List; + +public class Stats { + public int words; + public int knots; + public int stitches; + public int functions; + public int choices; + public int gathers; + public int diverts; + + public static Stats generate(Story story) { + Stats stats = new Stats(); + + List allText = story.findAll(Text.class); + + stats.words = 0; + for (Text text : allText) { + int wordsInThisStr = 0; + boolean wasWhiteSpace = true; + for (char c : text.getText().toCharArray()) { + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + wasWhiteSpace = true; + } else if (wasWhiteSpace) { + wordsInThisStr++; + wasWhiteSpace = false; + } + } + + stats.words += wordsInThisStr; + } + + List knots = story.findAll(Knot.class); + stats.knots = knots.size(); + + stats.functions = 0; + for (Knot knot : knots) { + if (knot.isFunction()) { + stats.functions++; + } + } + + List stitches = story.findAll(Stitch.class); + stats.stitches = stitches.size(); + + List choices = story.findAll(Choice.class); + stats.choices = choices.size(); + + List gathers = story.findAll(Gather.class, gather -> gather.getDebugMetadata() != null); + stats.gathers = gathers.size(); + + List diverts = story.findAll(Divert.class); + stats.diverts = diverts.size() - 1; + + return stats; + } +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParser.java b/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParser.java new file mode 100644 index 0000000..967541b --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParser.java @@ -0,0 +1,597 @@ +package com.bladecoder.ink.compiler.StringParser; + +import com.bladecoder.ink.compiler.CharacterSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class StringParser { + @FunctionalInterface + public interface ParseRule { + Object parse(); + } + + @FunctionalInterface + public interface SpecificParseRule { + T parse(); + } + + @FunctionalInterface + public interface ErrorHandler { + void onError(String message, int index, int lineIndex, boolean isWarning); + } + + public StringParser(String str) { + str = preProcessInputString(str); + + state = new StringParserState(); + + if (str != null) { + _chars = str.toCharArray(); + } else { + _chars = new char[0]; + } + + inputString = str; + } + + public static class ParseSuccessStruct {} + + public static final ParseSuccessStruct ParseSuccess = new ParseSuccessStruct(); + + public static final CharacterSet numbersCharacterSet = new CharacterSet("0123456789"); + + protected ErrorHandler errorHandler; + + public char getCurrentCharacter() { + if (getIndex() >= 0 && remainingLength() > 0) { + return _chars[getIndex()]; + } + return 0; + } + + public StringParserState state; + + public boolean hadError; + + protected String preProcessInputString(String str) { + return str; + } + + protected int beginRule() { + return state.push(); + } + + protected Object failRule(int expectedRuleId) { + state.pop(expectedRuleId); + return null; + } + + protected void cancelRule(int expectedRuleId) { + state.pop(expectedRuleId); + } + + protected Object succeedRule(int expectedRuleId, Object result) { + StringParserState.Element stateAtSucceedRule = state.peek(expectedRuleId); + StringParserState.Element stateAtBeginRule = state.peekPenultimate(); + + ruleDidSucceed(result, stateAtBeginRule, stateAtSucceedRule); + + state.squash(); + + if (result == null) { + result = ParseSuccess; + } + + return result; + } + + protected void ruleDidSucceed( + Object result, StringParserState.Element startState, StringParserState.Element endState) {} + + protected Object expect(ParseRule rule, String message, ParseRule recoveryRule) { + Object result = parseObject(rule); + if (result == null) { + if (message == null) { + message = rule.getClass().getName(); + } + + String butSaw; + String lineRemainder = lineRemainder(); + if (lineRemainder == null || lineRemainder.isEmpty()) { + butSaw = "end of line"; + } else { + butSaw = "'" + lineRemainder + "'"; + } + + error("Expected " + message + " but saw " + butSaw, false); + + if (recoveryRule != null) { + result = recoveryRule.parse(); + } + } + return result; + } + + protected Object expect(ParseRule rule, String message) { + return expect(rule, message, null); + } + + protected Object expect(ParseRule rule) { + return expect(rule, null, null); + } + + protected void error(String message, boolean isWarning) { + errorOnLine(message, getLineIndex() + 1, isWarning); + } + + protected void errorWithParsedObject( + String message, com.bladecoder.ink.compiler.ParsedHierarchy.ParsedObject result, boolean isWarning) { + errorOnLine(message, result.getDebugMetadata().startLineNumber, isWarning); + } + + protected void errorOnLine(String message, int lineNumber, boolean isWarning) { + if (!state.isErrorReportedAlreadyInScope()) { + String errorType = isWarning ? "Warning" : "Error"; + + if (errorHandler == null) { + throw new RuntimeException(errorType + " on line " + lineNumber + ": " + message); + } else { + errorHandler.onError(message, getIndex(), lineNumber - 1, isWarning); + } + + state.noteErrorReported(); + } + + if (!isWarning) { + hadError = true; + } + } + + protected void warning(String message) { + error(message, true); + } + + public boolean isEndOfInput() { + return getIndex() >= _chars.length; + } + + public String remainingString() { + return new String(_chars, getIndex(), remainingLength()); + } + + public String lineRemainder() { + return (String) peek(() -> parseUntilCharactersFromString("\n\r")); + } + + public int remainingLength() { + return _chars.length - getIndex(); + } + + public String inputString; + + public int getLineIndex() { + return state.getLineIndex(); + } + + public void setLineIndex(int value) { + state.setLineIndex(value); + } + + public int getCharacterInLineIndex() { + return state.getCharacterInLineIndex(); + } + + public void setCharacterInLineIndex(int value) { + state.setCharacterInLineIndex(value); + } + + public int getIndex() { + return state.getCharacterIndex(); + } + + public void setIndex(int value) { + state.setCharacterIndex(value); + } + + public void setFlag(long flag, boolean trueOrFalse) { + if (trueOrFalse) { + state.setCustomFlags(state.getCustomFlags() | flag); + } else { + state.setCustomFlags(state.getCustomFlags() & ~flag); + } + } + + public boolean getFlag(long flag) { + return (state.getCustomFlags() & flag) != 0; + } + + public Object parseObject(ParseRule rule) { + int ruleId = beginRule(); + + int stackHeightBefore = state.getStackHeight(); + + Object result = rule.parse(); + + if (stackHeightBefore != state.getStackHeight()) { + throw new RuntimeException("Mismatched Begin/Fail/Succeed rules"); + } + + if (result == null) { + return failRule(ruleId); + } + + return succeedRule(ruleId, result); + } + + public T parse(SpecificParseRule rule) { + int ruleId = beginRule(); + + T result = rule.parse(); + if (result == null) { + failRule(ruleId); + return null; + } + + succeedRule(ruleId, result); + return result; + } + + public Object oneOf(ParseRule... rules) { + for (ParseRule rule : rules) { + Object result = parseObject(rule); + if (result != null) { + return result; + } + } + + return null; + } + + public List oneOrMore(ParseRule rule) { + List results = new ArrayList<>(); + + Object result; + do { + result = parseObject(rule); + if (result != null) { + results.add(result); + } + } while (result != null); + + if (!results.isEmpty()) { + return results; + } + + return null; + } + + public ParseRule optional(ParseRule rule) { + return () -> { + Object result = parseObject(rule); + if (result == null) { + result = ParseSuccess; + } + return result; + }; + } + + public ParseRule exclude(ParseRule rule) { + return () -> { + Object result = parseObject(rule); + if (result == null) { + return null; + } + return ParseSuccess; + }; + } + + public ParseRule optionalExclude(ParseRule rule) { + return () -> { + parseObject(rule); + return ParseSuccess; + }; + } + + protected ParseRule stringRule(String str) { + return () -> parseString(str); + } + + private void tryAddResultToList(Object result, List list, boolean flatten) { + if (result == ParseSuccess) { + return; + } + + if (flatten && result instanceof Collection) { + for (Object obj : (Collection) result) { + list.add((T) obj); + } + return; + } + + list.add((T) result); + } + + public List interleave(ParseRule ruleA, ParseRule ruleB, ParseRule untilTerminator, boolean flatten) { + int ruleId = beginRule(); + + List results = new ArrayList<>(); + + Object firstA = parseObject(ruleA); + if (firstA == null) { + return (List) failRule(ruleId); + } + tryAddResultToList(firstA, results, flatten); + + Object lastMainResult; + Object outerResult; + do { + if (untilTerminator != null && peek(untilTerminator) != null) { + break; + } + + lastMainResult = parseObject(ruleB); + if (lastMainResult == null) { + break; + } + tryAddResultToList(lastMainResult, results, flatten); + + outerResult = null; + if (lastMainResult != null) { + outerResult = parseObject(ruleA); + if (outerResult == null) { + break; + } + tryAddResultToList(outerResult, results, flatten); + } + + } while ((lastMainResult != null || outerResult != null) + && !(lastMainResult == ParseSuccess && outerResult == ParseSuccess) + && remainingLength() > 0); + + if (results.isEmpty()) { + return (List) failRule(ruleId); + } + + return (List) succeedRule(ruleId, results); + } + + public List interleave(ParseRule ruleA, ParseRule ruleB) { + return interleave(ruleA, ruleB, null, true); + } + + public String parseString(String str) { + if (str.length() > remainingLength()) { + return null; + } + + int ruleId = beginRule(); + + int i = getIndex(); + int cli = getCharacterInLineIndex(); + int li = getLineIndex(); + + boolean success = true; + for (char c : str.toCharArray()) { + if (_chars[i] != c) { + success = false; + break; + } + if (c == '\n') { + li++; + cli = -1; + } + i++; + cli++; + } + + setIndex(i); + setCharacterInLineIndex(cli); + setLineIndex(li); + + if (success) { + return (String) succeedRule(ruleId, str); + } + + return (String) failRule(ruleId); + } + + public char parseSingleCharacter() { + if (remainingLength() > 0) { + char c = _chars[getIndex()]; + if (c == '\n') { + setLineIndex(getLineIndex() + 1); + setCharacterInLineIndex(-1); + } + setIndex(getIndex() + 1); + setCharacterInLineIndex(getCharacterInLineIndex() + 1); + return c; + } + + return 0; + } + + public String parseUntilCharactersFromString(String str, int maxCount) { + return parseCharactersFromString(str, false, maxCount); + } + + public String parseUntilCharactersFromString(String str) { + return parseUntilCharactersFromString(str, -1); + } + + public String parseUntilCharactersFromCharSet(CharacterSet charSet, int maxCount) { + return parseCharactersFromCharSet(charSet, false, maxCount); + } + + public String parseUntilCharactersFromCharSet(CharacterSet charSet) { + return parseUntilCharactersFromCharSet(charSet, -1); + } + + public String parseCharactersFromString(String str, int maxCount) { + return parseCharactersFromString(str, true, maxCount); + } + + public String parseCharactersFromString(String str) { + return parseCharactersFromString(str, true, -1); + } + + public String parseCharactersFromString(String str, boolean shouldIncludeStrChars, int maxCount) { + return parseCharactersFromCharSet(new CharacterSet(str), shouldIncludeStrChars, maxCount); + } + + public String parseCharactersFromCharSet(CharacterSet charSet, boolean shouldIncludeChars, int maxCount) { + if (maxCount == -1) { + maxCount = Integer.MAX_VALUE; + } + + int startIndex = getIndex(); + + int i = getIndex(); + int cli = getCharacterInLineIndex(); + int li = getLineIndex(); + + int count = 0; + while (i < _chars.length && charSet.contains(_chars[i]) == shouldIncludeChars && count < maxCount) { + if (_chars[i] == '\n') { + li++; + cli = -1; + } + i++; + cli++; + count++; + } + + setIndex(i); + setCharacterInLineIndex(cli); + setLineIndex(li); + + int lastCharIndex = getIndex(); + if (lastCharIndex > startIndex) { + return new String(_chars, startIndex, getIndex() - startIndex); + } + + return null; + } + + public String parseCharactersFromCharSet(CharacterSet charSet) { + return parseCharactersFromCharSet(charSet, true, -1); + } + + public Object peek(ParseRule rule) { + int ruleId = beginRule(); + Object result = rule.parse(); + cancelRule(ruleId); + return result; + } + + public String parseUntil(ParseRule stopRule, CharacterSet pauseCharacters, CharacterSet endCharacters) { + int ruleId = beginRule(); + + CharacterSet pauseAndEnd = new CharacterSet(); + if (pauseCharacters != null) { + pauseAndEnd.addCharacters(pauseCharacters); + } + if (endCharacters != null) { + pauseAndEnd.addCharacters(endCharacters); + } + + StringBuilder parsedString = new StringBuilder(); + Object ruleResultAtPause; + + do { + String partialParsedString = parseUntilCharactersFromCharSet(pauseAndEnd); + if (partialParsedString != null) { + parsedString.append(partialParsedString); + } + + ruleResultAtPause = peek(stopRule); + + if (ruleResultAtPause != null) { + break; + } + + if (isEndOfInput()) { + break; + } + + char pauseCharacter = getCurrentCharacter(); + if (pauseCharacters != null && pauseCharacters.contains(pauseCharacter)) { + parsedString.append(pauseCharacter); + if (pauseCharacter == '\n') { + setLineIndex(getLineIndex() + 1); + setCharacterInLineIndex(-1); + } + setIndex(getIndex() + 1); + setCharacterInLineIndex(getCharacterInLineIndex() + 1); + } else { + break; + } + + } while (true); + + if (parsedString.length() > 0) { + return (String) succeedRule(ruleId, parsedString.toString()); + } + + return (String) failRule(ruleId); + } + + public Integer parseInt() { + int oldIndex = getIndex(); + int oldCharacterInLineIndex = getCharacterInLineIndex(); + + boolean negative = parseString("-") != null; + + parseCharactersFromString(" \t"); + + String parsedString = parseCharactersFromCharSet(numbersCharacterSet); + if (parsedString == null) { + setIndex(oldIndex); + setCharacterInLineIndex(oldCharacterInLineIndex); + return null; + } + + try { + int parsedInt = Integer.parseInt(parsedString); + return negative ? -parsedInt : parsedInt; + } catch (NumberFormatException e) { + error( + "Failed to read integer value: " + parsedString + + ". Perhaps it's out of the range of acceptable numbers ink supports? (" + + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE + ")", + false); + return null; + } + } + + public Float parseFloat() { + int oldIndex = getIndex(); + int oldCharacterInLineIndex = getCharacterInLineIndex(); + + Integer leadingInt = parseInt(); + if (leadingInt != null) { + if (parseString(".") != null) { + String afterDecimalPointStr = parseCharactersFromCharSet(numbersCharacterSet); + return Float.parseFloat(leadingInt + "." + afterDecimalPointStr); + } + } + + setIndex(oldIndex); + setCharacterInLineIndex(oldCharacterInLineIndex); + return null; + } + + protected String parseNewline() { + int ruleId = beginRule(); + + parseString("\r"); + + if (parseString("\n") == null) { + return (String) failRule(ruleId); + } + + return (String) succeedRule(ruleId, "\n"); + } + + private char[] _chars; +} diff --git a/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParserState.java b/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParserState.java new file mode 100644 index 0000000..1b8c272 --- /dev/null +++ b/compiler/src/main/java/com/bladecoder/ink/compiler/StringParser/StringParserState.java @@ -0,0 +1,154 @@ +package com.bladecoder.ink.compiler.StringParser; + +public class StringParserState { + public int getLineIndex() { + return currentElement().lineIndex; + } + + public void setLineIndex(int value) { + currentElement().lineIndex = value; + } + + public int getCharacterIndex() { + return currentElement().characterIndex; + } + + public void setCharacterIndex(int value) { + currentElement().characterIndex = value; + } + + public int getCharacterInLineIndex() { + return currentElement().characterInLineIndex; + } + + public void setCharacterInLineIndex(int value) { + currentElement().characterInLineIndex = value; + } + + public long getCustomFlags() { + return currentElement().customFlags; + } + + public void setCustomFlags(long value) { + currentElement().customFlags = value; + } + + public boolean isErrorReportedAlreadyInScope() { + return currentElement().reportedErrorInScope; + } + + public int getStackHeight() { + return _numElements; + } + + public static class Element { + public int characterIndex; + public int characterInLineIndex; + public int lineIndex; + public boolean reportedErrorInScope; + public int uniqueId; + public long customFlags; + + public Element() {} + + public void copyFrom(Element fromElement) { + _uniqueIdCounter++; + this.uniqueId = _uniqueIdCounter; + this.characterIndex = fromElement.characterIndex; + this.characterInLineIndex = fromElement.characterInLineIndex; + this.lineIndex = fromElement.lineIndex; + this.customFlags = fromElement.customFlags; + this.reportedErrorInScope = false; + } + + public void squashFrom(Element fromElement) { + this.characterIndex = fromElement.characterIndex; + this.characterInLineIndex = fromElement.characterInLineIndex; + this.lineIndex = fromElement.lineIndex; + this.reportedErrorInScope = fromElement.reportedErrorInScope; + this.customFlags = fromElement.customFlags; + } + + private static int _uniqueIdCounter; + } + + public StringParserState() { + final int expectedMaxStackDepth = 200; + _stack = new Element[expectedMaxStackDepth]; + + for (int i = 0; i < expectedMaxStackDepth; ++i) { + _stack[i] = new Element(); + } + + _numElements = 1; + } + + public int push() { + if (_numElements >= _stack.length) { + throw new RuntimeException("Stack overflow in parser state"); + } + + Element prevElement = _stack[_numElements - 1]; + Element newElement = _stack[_numElements]; + _numElements++; + + newElement.copyFrom(prevElement); + + return newElement.uniqueId; + } + + public void pop(int expectedRuleId) { + if (_numElements == 1) { + throw new RuntimeException( + "Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?"); + } + + if (currentElement().uniqueId != expectedRuleId) { + throw new RuntimeException("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?"); + } + + _numElements--; + } + + public Element peek(int expectedRuleId) { + if (currentElement().uniqueId != expectedRuleId) { + throw new RuntimeException("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?"); + } + + return _stack[_numElements - 1]; + } + + public Element peekPenultimate() { + if (_numElements >= 2) { + return _stack[_numElements - 2]; + } + return null; + } + + public void squash() { + if (_numElements < 2) { + throw new RuntimeException( + "Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?"); + } + + Element penultimateEl = _stack[_numElements - 2]; + Element lastEl = _stack[_numElements - 1]; + + penultimateEl.squashFrom(lastEl); + + _numElements--; + } + + public void noteErrorReported() { + for (Element el : _stack) { + el.reportedErrorInScope = true; + } + } + + protected Element currentElement() { + return _stack[_numElements - 1]; + } + + private final Element[] _stack; + private int _numElements; +} diff --git a/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java similarity index 74% rename from src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java index 982015e..fd4673b 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class BasicTextSpecTest { public void oneline() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/basictext/oneline.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/basictext/oneline.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -30,7 +33,9 @@ public void oneline() throws Exception { public void twolines() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/basictext/twolines.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/basictext/twolines.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java similarity index 86% rename from src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java index b364721..03248ef 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.Arrays; @@ -13,7 +14,7 @@ public class ChoiceSpecTest { public void noChoice() throws Exception { List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/no-choice-text.ink.json", null, errors); + List text = TestUtils.runStory("inkfiles/choices/no-choice-text.ink", null, errors); Assert.assertEquals(0, errors.size()); Assert.assertEquals("Hello world!\nHello back!\n", TestUtils.joinText(text)); @@ -23,7 +24,7 @@ public void noChoice() throws Exception { public void one() throws Exception { List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/one.ink.json", null, errors); + List text = TestUtils.runStory("inkfiles/choices/one.ink", null, errors); Assert.assertEquals(0, errors.size()); Assert.assertEquals("Hello world!\nHello back!\nHello back!\n", TestUtils.joinText(text)); @@ -33,14 +34,14 @@ public void one() throws Exception { public void multiChoice() throws Exception { List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(0), errors); + List text = TestUtils.runStory("inkfiles/choices/multi-choice.ink", Arrays.asList(0), errors); Assert.assertEquals(0, errors.size()); Assert.assertEquals( "Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", TestUtils.joinText(text)); // Select second choice - text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(1), errors); + text = TestUtils.runStory("inkfiles/choices/multi-choice.ink", Arrays.asList(1), errors); Assert.assertEquals(0, errors.size()); Assert.assertEquals("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", TestUtils.joinText(text)); @@ -53,7 +54,7 @@ public void multiChoice() throws Exception { public void singleChoice1() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/single-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -69,7 +70,7 @@ public void singleChoice1() throws Exception { public void singleChoice2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/single-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -90,7 +91,7 @@ public void singleChoice2() throws Exception { public void suppressChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/suppress-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/suppress-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -111,7 +112,7 @@ public void suppressChoice() throws Exception { public void mixedChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/mixed-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/mixed-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -133,7 +134,7 @@ public void mixedChoice() throws Exception { public void varyingChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/varying-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/varying-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -155,7 +156,7 @@ public void varyingChoice() throws Exception { public void stickyChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/sticky-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/sticky-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -175,7 +176,7 @@ public void stickyChoice() throws Exception { public void fallbackChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/fallback-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -190,7 +191,7 @@ public void fallbackChoice() throws Exception { public void fallbackChoice2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/fallback-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -214,7 +215,7 @@ public void fallbackChoice2() throws Exception { public void conditionalChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/conditional-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/conditional-choice.ink"); Story story = new Story(json); System.out.println(story.buildStringOfHierarchy()); @@ -231,7 +232,7 @@ public void conditionalChoice() throws Exception { public void labelFlow() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/label-flow.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -251,7 +252,7 @@ public void labelFlow() throws Exception { public void labelFlow2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/label-flow.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -270,7 +271,7 @@ public void labelFlow2() throws Exception { public void labelScope() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/label-scope.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/label-scope.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -292,7 +293,9 @@ public void labelScope() throws Exception { public void labelScopeError() throws Exception { // List text = new ArrayList(); // - // String json = TestUtils.getJsonString("inkfiles/choices/label-scope-error.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/choices/label-scope-error.ink")); // Story story = new Story(json); // // try { @@ -312,7 +315,7 @@ public void labelScopeError() throws Exception { public void divertChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/divert-choice.ink.json"); + String json = TestUtils.compileInkFile("inkfiles/choices/divert-choice.ink"); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java similarity index 81% rename from src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java index 1d301b9..2bad62e 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class ConditionalSpecTest { public void ifTrue() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/iftrue.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/iftrue.ink")); Story story = new Story(json); System.out.println(story.buildStringOfHierarchy()); @@ -33,7 +36,9 @@ public void ifTrue() throws Exception { public void ifFalse() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/iffalse.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/iffalse.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -50,7 +55,9 @@ public void ifFalse() throws Exception { public void ifElse() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/ifelse.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -67,7 +74,9 @@ public void ifElse() throws Exception { public void ifElseExt() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/ifelse-ext.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -83,7 +92,9 @@ public void ifElseExt() throws Exception { public void ifElseExtText1() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text1.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/ifelse-ext-text1.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -105,7 +116,9 @@ public void ifElseExtText1() throws Exception { public void ifElseExtText2() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text2.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/ifelse-ext-text2.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -127,7 +140,9 @@ public void ifElseExtText2() throws Exception { public void ifElseExtText3() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text3.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/ifelse-ext-text3.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -149,7 +164,9 @@ public void ifElseExtText3() throws Exception { public void condText1() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/condtext.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -168,7 +185,9 @@ public void condText1() throws Exception { public void condText2() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/condtext.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -188,7 +207,9 @@ public void condText2() throws Exception { public void condOpt1() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/condopt.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -206,7 +227,9 @@ public void condOpt1() throws Exception { public void condOpt2() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/condopt.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -225,7 +248,9 @@ public void condOpt2() throws Exception { public void stopping() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/stopping.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/stopping.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -259,7 +284,9 @@ public void stopping() throws Exception { public void cycle() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/cycle.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/cycle.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -294,7 +321,9 @@ public void cycle() throws Exception { public void once() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/once.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/once.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -330,7 +359,9 @@ public void once() throws Exception { public void shuffle() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/shuffle.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -358,7 +389,9 @@ public void shuffle() throws Exception { public void shuffleStopping() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_stopping.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/shuffle_stopping.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -386,7 +419,9 @@ public void shuffleStopping() throws Exception { public void shuffleOnce() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_once.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/shuffle_once.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -415,7 +450,9 @@ public void shuffleOnce() throws Exception { public void multiline() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/multiline.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/multiline.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -444,7 +481,9 @@ public void multiline() throws Exception { public void multilineDivert() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/multiline-divert.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/multiline-divert.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -473,7 +512,9 @@ public void multilineDivert() throws Exception { public void multilineChoice() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/conditional/multiline-choice.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/conditional/multiline-choice.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java similarity index 78% rename from src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java index 61df85f..7ebf0d4 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class DivertSpec { public void simpleDivert() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/divert/simple-divert.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/divert/simple-divert.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -32,7 +35,9 @@ public void simpleDivert() throws Exception { public void invisibleDivert() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/divert/invisible-divert.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/divert/invisible-divert.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -48,7 +53,9 @@ public void invisibleDivert() throws Exception { public void divertOnChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/divert/divert-on-choice.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/divert/divert-on-choice.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -68,7 +75,9 @@ public void divertOnChoice() throws Exception { public void complexBranching1() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/divert/complex-branching.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -89,7 +98,9 @@ public void complexBranching1() throws Exception { public void complexBranching2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/divert/complex-branching.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java similarity index 74% rename from src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java index c6c6811..f115d25 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class FunctionSpecTest { public void funcBasic() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/func-basic.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/func-basic.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -31,7 +34,9 @@ public void funcBasic() throws Exception { public void funcNone() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/func-none.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/func-none.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -46,7 +51,9 @@ public void funcNone() throws Exception { public void funcInline() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/func-inline.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/func-inline.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -62,7 +69,9 @@ public void funcInline() throws Exception { public void setVarFunc() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/setvar-func.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/setvar-func.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -78,7 +87,9 @@ public void setVarFunc() throws Exception { public void complexFunc1() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/complex-func1.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/complex-func1.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -94,7 +105,9 @@ public void complexFunc1() throws Exception { public void complexFunc2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/complex-func2.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/complex-func2.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -110,7 +123,9 @@ public void complexFunc2() throws Exception { public void complexFunc3() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/complex-func3.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/complex-func3.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -128,7 +143,9 @@ public void complexFunc3() throws Exception { public void rnd() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/function/rnd-func.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/function/rnd-func.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -146,7 +163,10 @@ public void rnd() throws Exception { @Test public void evaluatingFunctionVariableStateBug() throws Exception { - String json = TestUtils.getJsonString("inkfiles/function/evaluating-function-variablestate-bug.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile( + TestUtils.readFileAsString("inkfiles/function/evaluating-function-variablestate-bug.ink")); Story story = new Story(json); Assert.assertEquals("Start\n", story.Continue()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java similarity index 84% rename from src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java index b412d4b..632ff69 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class GatherSpecTest { public void gatherBasic() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/gather-basic.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/gather-basic.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -36,7 +39,9 @@ public void gatherBasic() throws Exception { public void gatherChain() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/gather-chain.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/gather-chain.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -66,7 +71,9 @@ public void gatherChain() throws Exception { public void nestedFlow() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/nested-flow.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/nested-flow.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -92,7 +99,9 @@ public void nestedFlow() throws Exception { public void deepNesting() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/deep-nesting.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/deep-nesting.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -128,7 +137,9 @@ public void deepNesting() throws Exception { public void complexFlow1() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/complex-flow.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -146,7 +157,9 @@ public void complexFlow1() throws Exception { public void complexFlow2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/gather/complex-flow.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java similarity index 74% rename from src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java index 6bf1702..7140c37 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class GlueSpecTest { public void simpleGlue() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/glue/simple-glue.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/glue/simple-glue.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -30,7 +33,9 @@ public void simpleGlue() throws Exception { public void glueWithDivert() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/glue/glue-with-divert.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/glue/glue-with-divert.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -45,7 +50,9 @@ public void glueWithDivert() throws Exception { public void testLeftRightGlueMatching() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/glue/left-right-glue-matching.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/glue/left-right-glue-matching.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -61,7 +68,9 @@ public void testLeftRightGlueMatching() throws Exception { public void testBugfix1() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/glue/testbugfix1.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/glue/testbugfix1.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -77,7 +86,9 @@ public void testBugfix1() throws Exception { public void testBugfix2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/glue/testbugfix2.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/glue/testbugfix2.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java similarity index 76% rename from src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java index 282a79e..b8a343a 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class KnotSpecTest { public void testSingleLine() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/single-line.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/single-line.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -30,7 +33,9 @@ public void testSingleLine() throws Exception { public void testMultiLine() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/multi-line.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/multi-line.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -47,7 +52,9 @@ public void testMultiLine() throws Exception { public void stripEmptyLines() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/strip-empty-lines.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/strip-empty-lines.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -64,7 +71,9 @@ public void stripEmptyLines() throws Exception { public void paramStrings() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-strings.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-strings.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -83,7 +92,9 @@ public void paramStrings() throws Exception { public void paramInts() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-ints.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-ints.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -104,7 +115,9 @@ public void paramInts() throws Exception { public void paramFloats() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-floats.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-floats.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -123,7 +136,9 @@ public void paramFloats() throws Exception { public void paramVars() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-vars.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-vars.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -142,7 +157,9 @@ public void paramVars() throws Exception { public void paramMulti() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-multi.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-multi.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -161,7 +178,9 @@ public void paramMulti() throws Exception { public void paramRecurse() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/knot/param-recurse.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/knot/param-recurse.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java similarity index 67% rename from src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java index fcdfe40..debedde 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -12,7 +13,9 @@ public class ListSpecTest { @Test public void testListBasicOperations() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/basic-operations.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/basic-operations.ink")); Story story = new Story(json); Assert.assertEquals("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", story.continueMaximally()); @@ -24,7 +27,9 @@ public void testListBasicOperations() throws Exception { @Test public void testListMixedItems() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/list-mixed-items.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/list-mixed-items.ink")); Story story = new Story(json); Assert.assertEquals("a, y, c\n", story.continueMaximally()); @@ -36,7 +41,9 @@ public void testListMixedItems() throws Exception { @Test public void testMoreListOperations() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/more-list-operations.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/more-list-operations.ink")); Story story = new Story(json); Assert.assertEquals("1\nl\nn\nl, m\nn\n", story.continueMaximally()); @@ -48,7 +55,9 @@ public void testMoreListOperations() throws Exception { @Test public void testEmptyListOrigin() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/empty-list-origin.ink")); Story story = new Story(json); Assert.assertEquals("a, b\n", story.continueMaximally()); @@ -60,7 +69,9 @@ public void testEmptyListOrigin() throws Exception { @Test public void testListSaveLoad() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/list-save-load.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/list-save-load.ink")); Story story = new Story(json); Assert.assertEquals("a, x, c\n", story.continueMaximally()); @@ -86,7 +97,10 @@ public void testListSaveLoad() throws Exception { @Test public void testEmptyListOriginAfterAssignment() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin-after-assignment.ink.json"); + Compiler compiler = new Compiler(); + + String json = + compiler.compile(TestUtils.readFileAsString("inkfiles/lists/empty-list-origin-after-assignment.ink")); Story story = new Story(json); Assert.assertEquals("a, b, c\n", story.continueMaximally()); @@ -95,7 +109,9 @@ public void testEmptyListOriginAfterAssignment() throws Exception { @Test public void testListRange() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/list-range.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/list-range.ink")); Story story = new Story(json); Assert.assertEquals( @@ -106,7 +122,9 @@ public void testListRange() throws Exception { @Test public void testBugAddingElement() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/bug-adding-element.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/bug-adding-element.ink")); Story story = new Story(json); String s = story.continueMaximally(); @@ -124,7 +142,9 @@ public void testBugAddingElement() throws Exception { @Test public void testMoreListOperations2() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/more-list-operations2.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/more-list-operations2.ink")); Story story = new Story(json); Assert.assertEquals( @@ -135,7 +155,9 @@ public void testMoreListOperations2() throws Exception { @Test public void testListAllBug() throws Exception { - String json = TestUtils.getJsonString("inkfiles/lists/list-all.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/lists/list-all.ink")); Story story = new Story(json); Assert.assertEquals("A, B\n", story.continueMaximally()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java similarity index 80% rename from src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java index 4a2414c..d78178b 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -11,7 +12,9 @@ public class MiscTest { */ @Test public void issue15() throws Exception { - String json = TestUtils.getJsonString("inkfiles/misc/issue15.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/misc/issue15.ink")); Story story = new Story(json); Assert.assertEquals("This is a test\n", story.Continue()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java similarity index 89% rename from src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java index 087959e..5902eaf 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -9,7 +10,9 @@ public class MultiFlowSpecTest { @Test public void basics() throws Exception { - String json = TestUtils.getJsonString("inkfiles/runtime/multiflow-basics.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/multiflow-basics.ink")); Story story = new Story(json); story.switchFlow("First"); @@ -30,7 +33,9 @@ public void basics() throws Exception { @Test public void testMultiFlowSaveLoadThreads() throws Exception { - String json = TestUtils.getJsonString("inkfiles/runtime/multiflow-saveloadthreads.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/multiflow-saveloadthreads.ink")); Story story = new Story(json); // Default flow diff --git a/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java similarity index 83% rename from src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java index d81d1d4..e00d1f5 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java @@ -1,13 +1,9 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Profiler; import com.bladecoder.ink.runtime.Story; -import com.bladecoder.ink.runtime.Story.ExternalFunction; -import com.bladecoder.ink.runtime.Story.ExternalFunction0; -import com.bladecoder.ink.runtime.Story.ExternalFunction1; -import com.bladecoder.ink.runtime.Story.ExternalFunction2; -import com.bladecoder.ink.runtime.Story.ExternalFunction3; -import com.bladecoder.ink.runtime.Story.VariableObserver; +import com.bladecoder.ink.runtime.Story.*; import com.bladecoder.ink.runtime.StoryException; import java.util.ArrayList; import java.util.List; @@ -23,7 +19,9 @@ public class RuntimeSpecTest { public void externalFunction() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-2-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction() { @@ -48,7 +46,9 @@ public Integer call(Object[] args) throws Exception { public void externalFunctionZeroArguments() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-0-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-0-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction0() { @@ -71,7 +71,9 @@ protected String call() { public void externalFunctionOneArgument() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-1-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction1() { @@ -94,7 +96,9 @@ protected Boolean call(Integer arg) { public void externalFunctionOneArgumentCoerceOverride() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-1-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction1() { @@ -122,7 +126,9 @@ protected Boolean call(Boolean arg) { public void externalFunctionTwoArguments() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-2-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction2() { @@ -145,7 +151,9 @@ protected Integer call(Integer x, Float y) { public void externalFunctionTwoArgumentsCoerceOverride() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-2-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction2() { @@ -178,7 +186,9 @@ protected Integer call(Integer x, Integer y) { public void externalFunctionThreeArguments() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-3-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction3() { @@ -201,7 +211,9 @@ protected Integer call(Integer x, Integer y, Integer z) { public void externalFunctionThreeArgumentsCoerceOverride() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-3-arg.ink")); final Story story = new Story(json); story.bindExternalFunction("externalFunction", new ExternalFunction3() { @@ -239,7 +251,9 @@ protected Integer call(Integer x, Integer y, Integer z) { public void externalFunctionFallback() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/external-function-2-arg.ink")); Story story = new Story(json); story.setAllowExternalFunctionFallbacks(true); @@ -258,7 +272,9 @@ public void externalFunctionFallback() throws Exception { public void variableObservers() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/variable-observers.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/variable-observers.ink")); Story story = new Story(json); story.observeVariable("x", new VariableObserver() { @@ -290,7 +306,9 @@ public void call(String variableName, Object newValue) { public void setAndGetVariable() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/set-get-variables.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -315,7 +333,9 @@ public void setAndGetVariable() throws Exception { public void testSetNonExistantVariable() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/set-get-variables.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -347,7 +367,9 @@ public void testSetNonExistantVariable() throws Exception { public void jumpKnot() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/jump-knot.ink")); Story story = new Story(json); story.choosePathString("two"); @@ -377,7 +399,9 @@ public void jumpKnot() throws Exception { public void profiler() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/jump-knot.ink")); Story story = new Story(json); Profiler profiler = story.startProfiling(); @@ -408,7 +432,9 @@ public void profiler() throws Exception { public void jumpStitch() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/jump-stitch.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/jump-stitch.ink")); Story story = new Story(json); story.choosePathString("two.sthree"); @@ -440,7 +466,9 @@ public void jumpStitch() throws Exception { public void readVisitCounts() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/runtime/read-visit-counts.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/read-visit-counts.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -450,7 +478,9 @@ public void readVisitCounts() throws Exception { @Test public void testLoadSave() throws Exception { - String json = TestUtils.getJsonString("inkfiles/runtime/load-save.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/runtime/load-save.ink")); Story story = new Story(json); List text = new ArrayList<>(); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java similarity index 78% rename from src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java index 62815f2..a752e53 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,9 @@ public class StitchSpecTest { public void autoStitch() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/stitch/auto-stitch.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -30,7 +33,9 @@ public void autoStitch() throws Exception { public void autoStitch2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/stitch/auto-stitch.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -49,7 +54,9 @@ public void autoStitch2() throws Exception { public void manualStitch() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/stitch/manual-stitch.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -70,7 +77,9 @@ public void manualStitch() throws Exception { public void manualStitch2() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/stitch/manual-stitch.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java similarity index 79% rename from src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java index 904feba..df1abe3 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -12,7 +13,9 @@ public class TagSpecTest { @Test public void testTags() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tags.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tags.ink")); Story story = new Story(json); String[] globalTags = {"author: Joe", "title: My Great Story"}; @@ -41,7 +44,9 @@ public void testTags() throws Exception { @Test public void testTagsInSeq() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tagsInSeq.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tagsInSeq.ink")); Story story = new Story(json); Assert.assertEquals("A red sequence.\n", story.Continue()); @@ -54,7 +59,9 @@ public void testTagsInSeq() throws Exception { @Test public void testTagsInChoice() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tagsInChoice.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tagsInChoice.ink")); Story story = new Story(json); story.Continue(); @@ -74,7 +81,9 @@ public void testTagsInChoice() throws Exception { @Test public void testTagsInChoiceDynamicContent() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tagsInChoiceDynamic.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tagsInChoiceDynamic.ink")); Story story = new Story(json); story.Continue(); @@ -94,7 +103,9 @@ public void testTagsInChoiceDynamicContent() throws Exception { @Test public void testTagsDynamicContent() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tagsDynamicContent.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tagsDynamicContent.ink")); Story story = new Story(json); Assert.assertEquals("tag\n", story.Continue()); @@ -105,7 +116,9 @@ public void testTagsDynamicContent() throws Exception { @Test public void testTagsInLines() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tagsInLines.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/tags/tagsInLines.ink")); Story story = new Story(json); Assert.assertEquals("í\n", story.Continue()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java similarity index 86% rename from src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java index 1da2a19..0f7f00f 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java @@ -2,6 +2,7 @@ import static org.junit.Assert.fail; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Choice; import com.bladecoder.ink.runtime.Story; import java.io.BufferedReader; @@ -14,7 +15,7 @@ public class TestUtils { - public static String getJsonString(String filename) throws IOException { + public static String readFileAsString(String filename) throws IOException { InputStream systemResourceAsStream = ClassLoader.getSystemResourceAsStream(filename); @@ -33,12 +34,22 @@ public static String getJsonString(String filename) throws IOException { } } + public static String compileInkFile(String filename) throws Exception { + String source = readFileAsString(filename); + Compiler compiler = new Compiler(); + return compiler.compile(source); + } + public static List runStory(String filename, List choiceList, List errors) throws Exception { // 1) Load story - String json = getJsonString(filename); + String source = readFileAsString(filename); + + Compiler compiler = new Compiler(); + + String compiled = compiler.compile(source); - Story story = new Story(json); + Story story = new Story(compiled); List text = new ArrayList<>(); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java similarity index 87% rename from src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java index 5b8b8a7..f38ee6d 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -12,7 +13,9 @@ public class ThreadSpecTest { @Test public void testThread() throws Exception { - String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/threads/thread-bug.ink")); Story story = new Story(json); Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); @@ -42,7 +45,9 @@ public void testThread() throws Exception { @Test public void testThreadBug() throws Exception { - String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/threads/thread-bug.ink")); Story story = new Story(json); Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java similarity index 65% rename from src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java index 0584f62..00be9ce 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; @@ -12,7 +13,10 @@ public class TunnelSpecTest { @Test public void testTunnelOnwardsDivertOverride() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tunnels/tunnel-onwards-divert-override.ink.json"); + Compiler compiler = new Compiler(); + + String json = + compiler.compile(TestUtils.readFileAsString("inkfiles/tunnels/tunnel-onwards-divert-override.ink")); Story story = new Story(json); Assert.assertEquals("This is A\nNow in B.\n", story.continueMaximally()); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java similarity index 77% rename from src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java index 8c79c50..dc196f9 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -16,7 +17,9 @@ public class VariableSpecTest { public void variableDeclaration() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/variable/variable-declaration.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variable/variable-declaration.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -33,7 +36,9 @@ public void variableDeclaration() throws Exception { public void varCalc() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/variable/varcalc.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variable/varcalc.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -45,7 +50,9 @@ public void varCalc() throws Exception { public void varStringIncBug() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/variable/varstringinc.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variable/varstringinc.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -67,7 +74,9 @@ public void varStringIncBug() throws Exception { public void varDivert() throws Exception { List text = new ArrayList<>(); - String json = TestUtils.getJsonString("inkfiles/variable/var-divert.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variable/var-divert.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); diff --git a/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java b/compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java similarity index 85% rename from src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java rename to compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java index 2346fd8..80b7e71 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java +++ b/compiler/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.compiler.Compiler; import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; @@ -14,7 +15,9 @@ public class VariableTextSpecTest { public void sequence() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/variabletext/sequence.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/sequence.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -55,7 +58,9 @@ public void sequence() throws Exception { public void cycle() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/variabletext/cycle.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/cycle.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -94,7 +99,9 @@ public void cycle() throws Exception { public void once() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/variabletext/once.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/once.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -133,7 +140,9 @@ public void once() throws Exception { public void emptyElements() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/variabletext/empty-elements.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/empty-elements.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -160,7 +169,9 @@ public void emptyElements() throws Exception { public void listInChoice() throws Exception { List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/variabletext/list-in-choice.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/list-in-choice.ink")); Story story = new Story(json); TestUtils.nextAll(story, text); @@ -188,7 +199,9 @@ public void listInChoice() throws Exception { public void one() throws Exception { // List text = new ArrayList(); // - // String json = TestUtils.getJsonString("inkfiles/variabletext/one.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/one.ink")); // Story story = new Story(json); // // TestUtils.nextAll(story, text); @@ -204,7 +217,9 @@ public void one() throws Exception { public void minusOne() throws Exception { // List text = new ArrayList(); // - // String json = TestUtils.getJsonString("inkfiles/variabletext/minus-one.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/minus-one.ink")); // Story story = new Story(json); // // TestUtils.nextAll(story, text); @@ -220,7 +235,9 @@ public void minusOne() throws Exception { public void ten() throws Exception { // List text = new ArrayList(); // - // String json = TestUtils.getJsonString("inkfiles/variabletext/ten.ink.json"); + Compiler compiler = new Compiler(); + + String json = compiler.compile(TestUtils.readFileAsString("inkfiles/variabletext/ten.ink")); // Story story = new Story(json); // // TestUtils.nextAll(story, text); diff --git a/src/test/resources/inkfiles/basictext/oneline.ink b/compiler/src/test/resources/inkfiles/basictext/oneline.ink similarity index 100% rename from src/test/resources/inkfiles/basictext/oneline.ink rename to compiler/src/test/resources/inkfiles/basictext/oneline.ink diff --git a/src/test/resources/inkfiles/basictext/oneline.ink.json b/compiler/src/test/resources/inkfiles/basictext/oneline.ink.json similarity index 100% rename from src/test/resources/inkfiles/basictext/oneline.ink.json rename to compiler/src/test/resources/inkfiles/basictext/oneline.ink.json diff --git a/src/test/resources/inkfiles/basictext/twolines.ink b/compiler/src/test/resources/inkfiles/basictext/twolines.ink similarity index 100% rename from src/test/resources/inkfiles/basictext/twolines.ink rename to compiler/src/test/resources/inkfiles/basictext/twolines.ink diff --git a/src/test/resources/inkfiles/basictext/twolines.ink.json b/compiler/src/test/resources/inkfiles/basictext/twolines.ink.json similarity index 100% rename from src/test/resources/inkfiles/basictext/twolines.ink.json rename to compiler/src/test/resources/inkfiles/basictext/twolines.ink.json diff --git a/src/test/resources/inkfiles/choices/conditional-choice.ink b/compiler/src/test/resources/inkfiles/choices/conditional-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/conditional-choice.ink rename to compiler/src/test/resources/inkfiles/choices/conditional-choice.ink diff --git a/src/test/resources/inkfiles/choices/conditional-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/conditional-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/conditional-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/conditional-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/divert-choice.ink b/compiler/src/test/resources/inkfiles/choices/divert-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/divert-choice.ink rename to compiler/src/test/resources/inkfiles/choices/divert-choice.ink diff --git a/src/test/resources/inkfiles/choices/divert-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/divert-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/divert-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/divert-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/fallback-choice.ink b/compiler/src/test/resources/inkfiles/choices/fallback-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/fallback-choice.ink rename to compiler/src/test/resources/inkfiles/choices/fallback-choice.ink diff --git a/src/test/resources/inkfiles/choices/fallback-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/fallback-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/fallback-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/fallback-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/label-flow.ink b/compiler/src/test/resources/inkfiles/choices/label-flow.ink similarity index 100% rename from src/test/resources/inkfiles/choices/label-flow.ink rename to compiler/src/test/resources/inkfiles/choices/label-flow.ink diff --git a/src/test/resources/inkfiles/choices/label-flow.ink.json b/compiler/src/test/resources/inkfiles/choices/label-flow.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/label-flow.ink.json rename to compiler/src/test/resources/inkfiles/choices/label-flow.ink.json diff --git a/src/test/resources/inkfiles/choices/label-scope-error.ink b/compiler/src/test/resources/inkfiles/choices/label-scope-error.ink similarity index 100% rename from src/test/resources/inkfiles/choices/label-scope-error.ink rename to compiler/src/test/resources/inkfiles/choices/label-scope-error.ink diff --git a/src/test/resources/inkfiles/choices/label-scope-error.ink.json b/compiler/src/test/resources/inkfiles/choices/label-scope-error.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/label-scope-error.ink.json rename to compiler/src/test/resources/inkfiles/choices/label-scope-error.ink.json diff --git a/src/test/resources/inkfiles/choices/label-scope.ink b/compiler/src/test/resources/inkfiles/choices/label-scope.ink similarity index 100% rename from src/test/resources/inkfiles/choices/label-scope.ink rename to compiler/src/test/resources/inkfiles/choices/label-scope.ink diff --git a/src/test/resources/inkfiles/choices/label-scope.ink.json b/compiler/src/test/resources/inkfiles/choices/label-scope.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/label-scope.ink.json rename to compiler/src/test/resources/inkfiles/choices/label-scope.ink.json diff --git a/src/test/resources/inkfiles/choices/mixed-choice.ink b/compiler/src/test/resources/inkfiles/choices/mixed-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/mixed-choice.ink rename to compiler/src/test/resources/inkfiles/choices/mixed-choice.ink diff --git a/src/test/resources/inkfiles/choices/mixed-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/mixed-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/mixed-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/mixed-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/multi-choice.ink b/compiler/src/test/resources/inkfiles/choices/multi-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/multi-choice.ink rename to compiler/src/test/resources/inkfiles/choices/multi-choice.ink diff --git a/src/test/resources/inkfiles/choices/multi-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/multi-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/multi-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/multi-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/no-choice-text.ink b/compiler/src/test/resources/inkfiles/choices/no-choice-text.ink similarity index 100% rename from src/test/resources/inkfiles/choices/no-choice-text.ink rename to compiler/src/test/resources/inkfiles/choices/no-choice-text.ink diff --git a/src/test/resources/inkfiles/choices/no-choice-text.ink.json b/compiler/src/test/resources/inkfiles/choices/no-choice-text.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/no-choice-text.ink.json rename to compiler/src/test/resources/inkfiles/choices/no-choice-text.ink.json diff --git a/src/test/resources/inkfiles/choices/one.ink b/compiler/src/test/resources/inkfiles/choices/one.ink similarity index 100% rename from src/test/resources/inkfiles/choices/one.ink rename to compiler/src/test/resources/inkfiles/choices/one.ink diff --git a/src/test/resources/inkfiles/choices/one.ink.json b/compiler/src/test/resources/inkfiles/choices/one.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/one.ink.json rename to compiler/src/test/resources/inkfiles/choices/one.ink.json diff --git a/src/test/resources/inkfiles/choices/single-choice.ink b/compiler/src/test/resources/inkfiles/choices/single-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/single-choice.ink rename to compiler/src/test/resources/inkfiles/choices/single-choice.ink diff --git a/src/test/resources/inkfiles/choices/single-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/single-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/single-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/single-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/sticky-choice.ink b/compiler/src/test/resources/inkfiles/choices/sticky-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/sticky-choice.ink rename to compiler/src/test/resources/inkfiles/choices/sticky-choice.ink diff --git a/src/test/resources/inkfiles/choices/sticky-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/sticky-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/sticky-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/sticky-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/suppress-choice.ink b/compiler/src/test/resources/inkfiles/choices/suppress-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/suppress-choice.ink rename to compiler/src/test/resources/inkfiles/choices/suppress-choice.ink diff --git a/src/test/resources/inkfiles/choices/suppress-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/suppress-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/suppress-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/suppress-choice.ink.json diff --git a/src/test/resources/inkfiles/choices/varying-choice.ink b/compiler/src/test/resources/inkfiles/choices/varying-choice.ink similarity index 100% rename from src/test/resources/inkfiles/choices/varying-choice.ink rename to compiler/src/test/resources/inkfiles/choices/varying-choice.ink diff --git a/src/test/resources/inkfiles/choices/varying-choice.ink.json b/compiler/src/test/resources/inkfiles/choices/varying-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/choices/varying-choice.ink.json rename to compiler/src/test/resources/inkfiles/choices/varying-choice.ink.json diff --git a/src/test/resources/inkfiles/conditional/condopt.ink b/compiler/src/test/resources/inkfiles/conditional/condopt.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/condopt.ink rename to compiler/src/test/resources/inkfiles/conditional/condopt.ink diff --git a/src/test/resources/inkfiles/conditional/condopt.ink.json b/compiler/src/test/resources/inkfiles/conditional/condopt.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/condopt.ink.json rename to compiler/src/test/resources/inkfiles/conditional/condopt.ink.json diff --git a/src/test/resources/inkfiles/conditional/condtext.ink b/compiler/src/test/resources/inkfiles/conditional/condtext.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/condtext.ink rename to compiler/src/test/resources/inkfiles/conditional/condtext.ink diff --git a/src/test/resources/inkfiles/conditional/condtext.ink.json b/compiler/src/test/resources/inkfiles/conditional/condtext.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/condtext.ink.json rename to compiler/src/test/resources/inkfiles/conditional/condtext.ink.json diff --git a/src/test/resources/inkfiles/conditional/cycle.ink b/compiler/src/test/resources/inkfiles/conditional/cycle.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/cycle.ink rename to compiler/src/test/resources/inkfiles/conditional/cycle.ink diff --git a/src/test/resources/inkfiles/conditional/cycle.ink.json b/compiler/src/test/resources/inkfiles/conditional/cycle.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/cycle.ink.json rename to compiler/src/test/resources/inkfiles/conditional/cycle.ink.json diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext.ink b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext.ink rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext.ink diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json b/compiler/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse-ext.ink.json rename to compiler/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json diff --git a/src/test/resources/inkfiles/conditional/ifelse.ink b/compiler/src/test/resources/inkfiles/conditional/ifelse.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse.ink rename to compiler/src/test/resources/inkfiles/conditional/ifelse.ink diff --git a/src/test/resources/inkfiles/conditional/ifelse.ink.json b/compiler/src/test/resources/inkfiles/conditional/ifelse.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/ifelse.ink.json rename to compiler/src/test/resources/inkfiles/conditional/ifelse.ink.json diff --git a/src/test/resources/inkfiles/conditional/iffalse.ink b/compiler/src/test/resources/inkfiles/conditional/iffalse.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/iffalse.ink rename to compiler/src/test/resources/inkfiles/conditional/iffalse.ink diff --git a/src/test/resources/inkfiles/conditional/iffalse.ink.json b/compiler/src/test/resources/inkfiles/conditional/iffalse.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/iffalse.ink.json rename to compiler/src/test/resources/inkfiles/conditional/iffalse.ink.json diff --git a/src/test/resources/inkfiles/conditional/iftrue.ink b/compiler/src/test/resources/inkfiles/conditional/iftrue.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/iftrue.ink rename to compiler/src/test/resources/inkfiles/conditional/iftrue.ink diff --git a/src/test/resources/inkfiles/conditional/iftrue.ink.json b/compiler/src/test/resources/inkfiles/conditional/iftrue.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/iftrue.ink.json rename to compiler/src/test/resources/inkfiles/conditional/iftrue.ink.json diff --git a/src/test/resources/inkfiles/conditional/multiline-choice.ink b/compiler/src/test/resources/inkfiles/conditional/multiline-choice.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline-choice.ink rename to compiler/src/test/resources/inkfiles/conditional/multiline-choice.ink diff --git a/src/test/resources/inkfiles/conditional/multiline-choice.ink.json b/compiler/src/test/resources/inkfiles/conditional/multiline-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline-choice.ink.json rename to compiler/src/test/resources/inkfiles/conditional/multiline-choice.ink.json diff --git a/src/test/resources/inkfiles/conditional/multiline-divert.ink b/compiler/src/test/resources/inkfiles/conditional/multiline-divert.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline-divert.ink rename to compiler/src/test/resources/inkfiles/conditional/multiline-divert.ink diff --git a/src/test/resources/inkfiles/conditional/multiline-divert.ink.json b/compiler/src/test/resources/inkfiles/conditional/multiline-divert.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline-divert.ink.json rename to compiler/src/test/resources/inkfiles/conditional/multiline-divert.ink.json diff --git a/src/test/resources/inkfiles/conditional/multiline.ink b/compiler/src/test/resources/inkfiles/conditional/multiline.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline.ink rename to compiler/src/test/resources/inkfiles/conditional/multiline.ink diff --git a/src/test/resources/inkfiles/conditional/multiline.ink.json b/compiler/src/test/resources/inkfiles/conditional/multiline.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/multiline.ink.json rename to compiler/src/test/resources/inkfiles/conditional/multiline.ink.json diff --git a/src/test/resources/inkfiles/conditional/once.ink b/compiler/src/test/resources/inkfiles/conditional/once.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/once.ink rename to compiler/src/test/resources/inkfiles/conditional/once.ink diff --git a/src/test/resources/inkfiles/conditional/once.ink.json b/compiler/src/test/resources/inkfiles/conditional/once.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/once.ink.json rename to compiler/src/test/resources/inkfiles/conditional/once.ink.json diff --git a/src/test/resources/inkfiles/conditional/shuffle.ink b/compiler/src/test/resources/inkfiles/conditional/shuffle.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle.ink rename to compiler/src/test/resources/inkfiles/conditional/shuffle.ink diff --git a/src/test/resources/inkfiles/conditional/shuffle.ink.json b/compiler/src/test/resources/inkfiles/conditional/shuffle.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle.ink.json rename to compiler/src/test/resources/inkfiles/conditional/shuffle.ink.json diff --git a/src/test/resources/inkfiles/conditional/shuffle_once.ink b/compiler/src/test/resources/inkfiles/conditional/shuffle_once.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle_once.ink rename to compiler/src/test/resources/inkfiles/conditional/shuffle_once.ink diff --git a/src/test/resources/inkfiles/conditional/shuffle_once.ink.json b/compiler/src/test/resources/inkfiles/conditional/shuffle_once.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle_once.ink.json rename to compiler/src/test/resources/inkfiles/conditional/shuffle_once.ink.json diff --git a/src/test/resources/inkfiles/conditional/shuffle_stopping.ink b/compiler/src/test/resources/inkfiles/conditional/shuffle_stopping.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle_stopping.ink rename to compiler/src/test/resources/inkfiles/conditional/shuffle_stopping.ink diff --git a/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json b/compiler/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json rename to compiler/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json diff --git a/src/test/resources/inkfiles/conditional/stopping.ink b/compiler/src/test/resources/inkfiles/conditional/stopping.ink similarity index 100% rename from src/test/resources/inkfiles/conditional/stopping.ink rename to compiler/src/test/resources/inkfiles/conditional/stopping.ink diff --git a/src/test/resources/inkfiles/conditional/stopping.ink.json b/compiler/src/test/resources/inkfiles/conditional/stopping.ink.json similarity index 100% rename from src/test/resources/inkfiles/conditional/stopping.ink.json rename to compiler/src/test/resources/inkfiles/conditional/stopping.ink.json diff --git a/src/test/resources/inkfiles/divert/complex-branching.ink b/compiler/src/test/resources/inkfiles/divert/complex-branching.ink similarity index 100% rename from src/test/resources/inkfiles/divert/complex-branching.ink rename to compiler/src/test/resources/inkfiles/divert/complex-branching.ink diff --git a/src/test/resources/inkfiles/divert/complex-branching.ink.json b/compiler/src/test/resources/inkfiles/divert/complex-branching.ink.json similarity index 100% rename from src/test/resources/inkfiles/divert/complex-branching.ink.json rename to compiler/src/test/resources/inkfiles/divert/complex-branching.ink.json diff --git a/src/test/resources/inkfiles/divert/divert-on-choice.ink b/compiler/src/test/resources/inkfiles/divert/divert-on-choice.ink similarity index 100% rename from src/test/resources/inkfiles/divert/divert-on-choice.ink rename to compiler/src/test/resources/inkfiles/divert/divert-on-choice.ink diff --git a/src/test/resources/inkfiles/divert/divert-on-choice.ink.json b/compiler/src/test/resources/inkfiles/divert/divert-on-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/divert/divert-on-choice.ink.json rename to compiler/src/test/resources/inkfiles/divert/divert-on-choice.ink.json diff --git a/src/test/resources/inkfiles/divert/invisible-divert.ink b/compiler/src/test/resources/inkfiles/divert/invisible-divert.ink similarity index 100% rename from src/test/resources/inkfiles/divert/invisible-divert.ink rename to compiler/src/test/resources/inkfiles/divert/invisible-divert.ink diff --git a/src/test/resources/inkfiles/divert/invisible-divert.ink.json b/compiler/src/test/resources/inkfiles/divert/invisible-divert.ink.json similarity index 100% rename from src/test/resources/inkfiles/divert/invisible-divert.ink.json rename to compiler/src/test/resources/inkfiles/divert/invisible-divert.ink.json diff --git a/src/test/resources/inkfiles/divert/simple-divert.ink b/compiler/src/test/resources/inkfiles/divert/simple-divert.ink similarity index 100% rename from src/test/resources/inkfiles/divert/simple-divert.ink rename to compiler/src/test/resources/inkfiles/divert/simple-divert.ink diff --git a/src/test/resources/inkfiles/divert/simple-divert.ink.json b/compiler/src/test/resources/inkfiles/divert/simple-divert.ink.json similarity index 100% rename from src/test/resources/inkfiles/divert/simple-divert.ink.json rename to compiler/src/test/resources/inkfiles/divert/simple-divert.ink.json diff --git a/src/test/resources/inkfiles/function/complex-func1.ink b/compiler/src/test/resources/inkfiles/function/complex-func1.ink similarity index 94% rename from src/test/resources/inkfiles/function/complex-func1.ink rename to compiler/src/test/resources/inkfiles/function/complex-func1.ink index fdab4c9..d9f6d0b 100644 --- a/src/test/resources/inkfiles/function/complex-func1.ink +++ b/compiler/src/test/resources/inkfiles/function/complex-func1.ink @@ -1,12 +1,12 @@ -~ derp(2, 3, 4) - The values are {x} and {y}. - -> END - - === function derp(a, b, c) === - VAR x = 0 - ~ x = a + b - VAR y = 3 - { x == 5: - ~ x = 6 - } +~ derp(2, 3, 4) + The values are {x} and {y}. + -> END + + === function derp(a, b, c) === + VAR x = 0 + ~ x = a + b + VAR y = 3 + { x == 5: + ~ x = 6 + } ~ y = x + c \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/complex-func1.ink.json b/compiler/src/test/resources/inkfiles/function/complex-func1.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/complex-func1.ink.json rename to compiler/src/test/resources/inkfiles/function/complex-func1.ink.json diff --git a/src/test/resources/inkfiles/function/complex-func2.ink b/compiler/src/test/resources/inkfiles/function/complex-func2.ink similarity index 94% rename from src/test/resources/inkfiles/function/complex-func2.ink rename to compiler/src/test/resources/inkfiles/function/complex-func2.ink index 769f438..0e85c63 100644 --- a/src/test/resources/inkfiles/function/complex-func2.ink +++ b/compiler/src/test/resources/inkfiles/function/complex-func2.ink @@ -1,17 +1,17 @@ -~ derp(2, 3) - The values are {x} and {y} and {z}. - -> END - - === function derp(a, b) === - VAR x = 0 - ~ x = a - b - VAR y = 3 - { - - x == 0: - ~ y = 0 - - x > 0: - ~ y = x - 1 - - else: - ~ y = x + 1 - } +~ derp(2, 3) + The values are {x} and {y} and {z}. + -> END + + === function derp(a, b) === + VAR x = 0 + ~ x = a - b + VAR y = 3 + { + - x == 0: + ~ y = 0 + - x > 0: + ~ y = x - 1 + - else: + ~ y = x + 1 + } VAR z = 1 \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/complex-func2.ink.json b/compiler/src/test/resources/inkfiles/function/complex-func2.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/complex-func2.ink.json rename to compiler/src/test/resources/inkfiles/function/complex-func2.ink.json diff --git a/src/test/resources/inkfiles/function/complex-func3.ink b/compiler/src/test/resources/inkfiles/function/complex-func3.ink similarity index 95% rename from src/test/resources/inkfiles/function/complex-func3.ink rename to compiler/src/test/resources/inkfiles/function/complex-func3.ink index 7451530..c291dfc 100644 --- a/src/test/resources/inkfiles/function/complex-func3.ink +++ b/compiler/src/test/resources/inkfiles/function/complex-func3.ink @@ -1,27 +1,27 @@ - - ~ merchant_init() - "I will pay you {fee} reales if you get the goods to their destination. The goods will take up {weight} cargo spaces." - -> END - - === function merchant_init() - VAR weight = 20 - VAR roll = 0 - VAR mult = 1 - - { roll == 0: - ~ mult = 2 - } - - { mult == 2: - ~ roll = 1 - } - - { roll == 0: - ~ mult = 3 - } - - VAR dst = 5 - VAR deadline = 0 - ~ deadline = (dst * (100)) / 100 - VAR fee = 0 - ~ fee = (1 + dst) * 10 * mult + + ~ merchant_init() + "I will pay you {fee} reales if you get the goods to their destination. The goods will take up {weight} cargo spaces." + -> END + + === function merchant_init() + VAR weight = 20 + VAR roll = 0 + VAR mult = 1 + + { roll == 0: + ~ mult = 2 + } + + { mult == 2: + ~ roll = 1 + } + + { roll == 0: + ~ mult = 3 + } + + VAR dst = 5 + VAR deadline = 0 + ~ deadline = (dst * (100)) / 100 + VAR fee = 0 + ~ fee = (1 + dst) * 10 * mult diff --git a/src/test/resources/inkfiles/function/complex-func3.ink.json b/compiler/src/test/resources/inkfiles/function/complex-func3.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/complex-func3.ink.json rename to compiler/src/test/resources/inkfiles/function/complex-func3.ink.json diff --git a/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink b/compiler/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink similarity index 93% rename from src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink rename to compiler/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink index b64649e..44567f2 100644 --- a/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink +++ b/compiler/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink @@ -1,22 +1,22 @@ -Start - -> tunnel -> - End - -> END - - == tunnel == - In tunnel. - ->-> - - === function function_to_evaluate() === - { zero_equals_(1): - ~ return "WRONG" - - else: - ~ return "RIGHT" - } - - === function zero_equals_(k) === - ~ do_nothing(0) - ~ return (0 == k) - - === function do_nothing(k) - ~ return 0 +Start + -> tunnel -> + End + -> END + + == tunnel == + In tunnel. + ->-> + + === function function_to_evaluate() === + { zero_equals_(1): + ~ return "WRONG" + - else: + ~ return "RIGHT" + } + + === function zero_equals_(k) === + ~ do_nothing(0) + ~ return (0 == k) + + === function do_nothing(k) + ~ return 0 diff --git a/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json b/compiler/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json rename to compiler/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json diff --git a/src/test/resources/inkfiles/function/func-basic.ink b/compiler/src/test/resources/inkfiles/function/func-basic.ink similarity index 95% rename from src/test/resources/inkfiles/function/func-basic.ink rename to compiler/src/test/resources/inkfiles/function/func-basic.ink index c185eb3..00f86a0 100644 --- a/src/test/resources/inkfiles/function/func-basic.ink +++ b/compiler/src/test/resources/inkfiles/function/func-basic.ink @@ -1,7 +1,7 @@ -VAR x = 0.0 -~ x = lerp(2, 8, 0.4) - The value of x is {x}. - -> END - - === function lerp(a, b, k) === +VAR x = 0.0 +~ x = lerp(2, 8, 0.4) + The value of x is {x}. + -> END + + === function lerp(a, b, k) === ~ return ((b - a) * k) + a \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-basic.ink.json b/compiler/src/test/resources/inkfiles/function/func-basic.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/func-basic.ink.json rename to compiler/src/test/resources/inkfiles/function/func-basic.ink.json diff --git a/src/test/resources/inkfiles/function/func-inline.ink b/compiler/src/test/resources/inkfiles/function/func-inline.ink similarity index 96% rename from src/test/resources/inkfiles/function/func-inline.ink rename to compiler/src/test/resources/inkfiles/function/func-inline.ink index a6e6c43..f398571 100644 --- a/src/test/resources/inkfiles/function/func-inline.ink +++ b/compiler/src/test/resources/inkfiles/function/func-inline.ink @@ -1,5 +1,5 @@ -The value of x is {lerp(2, 8, 0.4)}. - -> END - - === function lerp(a, b, k) === +The value of x is {lerp(2, 8, 0.4)}. + -> END + + === function lerp(a, b, k) === ~ return ((b - a) * k) + a \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-inline.ink.json b/compiler/src/test/resources/inkfiles/function/func-inline.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/func-inline.ink.json rename to compiler/src/test/resources/inkfiles/function/func-inline.ink.json diff --git a/src/test/resources/inkfiles/function/func-none.ink b/compiler/src/test/resources/inkfiles/function/func-none.ink similarity index 94% rename from src/test/resources/inkfiles/function/func-none.ink rename to compiler/src/test/resources/inkfiles/function/func-none.ink index 33cfae1..b7b8227 100644 --- a/src/test/resources/inkfiles/function/func-none.ink +++ b/compiler/src/test/resources/inkfiles/function/func-none.ink @@ -1,7 +1,7 @@ -VAR x = 0.0 -~ x = f() - The value of x is {x}. - -> END - - === function f() === +VAR x = 0.0 +~ x = f() + The value of x is {x}. + -> END + + === function f() === ~ return 3.8 \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-none.ink.json b/compiler/src/test/resources/inkfiles/function/func-none.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/func-none.ink.json rename to compiler/src/test/resources/inkfiles/function/func-none.ink.json diff --git a/src/test/resources/inkfiles/function/rnd-func.ink b/compiler/src/test/resources/inkfiles/function/rnd-func.ink similarity index 96% rename from src/test/resources/inkfiles/function/rnd-func.ink rename to compiler/src/test/resources/inkfiles/function/rnd-func.ink index 67aa98f..b3987ca 100644 --- a/src/test/resources/inkfiles/function/rnd-func.ink +++ b/compiler/src/test/resources/inkfiles/function/rnd-func.ink @@ -1,6 +1,6 @@ -~ SEED_RANDOM(10) - -Rolling dice 1: {RANDOM(1,6)}. -Rolling dice 2: {RANDOM(1,6)}. -Rolling dice 3: {RANDOM(1,6)}. +~ SEED_RANDOM(10) + +Rolling dice 1: {RANDOM(1,6)}. +Rolling dice 2: {RANDOM(1,6)}. +Rolling dice 3: {RANDOM(1,6)}. Rolling dice 4: {RANDOM(1,6)}. \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/rnd-func.ink.json b/compiler/src/test/resources/inkfiles/function/rnd-func.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/rnd-func.ink.json rename to compiler/src/test/resources/inkfiles/function/rnd-func.ink.json diff --git a/src/test/resources/inkfiles/function/setvar-func.ink b/compiler/src/test/resources/inkfiles/function/setvar-func.ink similarity index 94% rename from src/test/resources/inkfiles/function/setvar-func.ink rename to compiler/src/test/resources/inkfiles/function/setvar-func.ink index d655942..777ad25 100644 --- a/src/test/resources/inkfiles/function/setvar-func.ink +++ b/compiler/src/test/resources/inkfiles/function/setvar-func.ink @@ -1,7 +1,7 @@ -~ herp(2, 3) - The value is {x}. - -> END - - === function herp(a, b) === - VAR x = 0.0 +~ herp(2, 3) + The value is {x}. + -> END + + === function herp(a, b) === + VAR x = 0.0 ~x = a * b \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/setvar-func.ink.json b/compiler/src/test/resources/inkfiles/function/setvar-func.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/setvar-func.ink.json rename to compiler/src/test/resources/inkfiles/function/setvar-func.ink.json diff --git a/src/test/resources/inkfiles/function/test-error.ink b/compiler/src/test/resources/inkfiles/function/test-error.ink similarity index 90% rename from src/test/resources/inkfiles/function/test-error.ink rename to compiler/src/test/resources/inkfiles/function/test-error.ink index 4c3a391..099a88d 100644 --- a/src/test/resources/inkfiles/function/test-error.ink +++ b/compiler/src/test/resources/inkfiles/function/test-error.ink @@ -1,9 +1,9 @@ -VAR roll = 0 - - { roll == 0: - ~ roll = 2 - } - - { roll == 0: - ~ roll = 1 - } +VAR roll = 0 + + { roll == 0: + ~ roll = 2 + } + + { roll == 0: + ~ roll = 1 + } diff --git a/src/test/resources/inkfiles/function/test-error.ink.json b/compiler/src/test/resources/inkfiles/function/test-error.ink.json similarity index 100% rename from src/test/resources/inkfiles/function/test-error.ink.json rename to compiler/src/test/resources/inkfiles/function/test-error.ink.json diff --git a/src/test/resources/inkfiles/gather/complex-flow.ink b/compiler/src/test/resources/inkfiles/gather/complex-flow.ink similarity index 100% rename from src/test/resources/inkfiles/gather/complex-flow.ink rename to compiler/src/test/resources/inkfiles/gather/complex-flow.ink diff --git a/src/test/resources/inkfiles/gather/complex-flow.ink.json b/compiler/src/test/resources/inkfiles/gather/complex-flow.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/complex-flow.ink.json rename to compiler/src/test/resources/inkfiles/gather/complex-flow.ink.json diff --git a/src/test/resources/inkfiles/gather/deep-nesting.ink b/compiler/src/test/resources/inkfiles/gather/deep-nesting.ink similarity index 100% rename from src/test/resources/inkfiles/gather/deep-nesting.ink rename to compiler/src/test/resources/inkfiles/gather/deep-nesting.ink diff --git a/src/test/resources/inkfiles/gather/deep-nesting.ink.json b/compiler/src/test/resources/inkfiles/gather/deep-nesting.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/deep-nesting.ink.json rename to compiler/src/test/resources/inkfiles/gather/deep-nesting.ink.json diff --git a/src/test/resources/inkfiles/gather/gather-basic.ink b/compiler/src/test/resources/inkfiles/gather/gather-basic.ink similarity index 100% rename from src/test/resources/inkfiles/gather/gather-basic.ink rename to compiler/src/test/resources/inkfiles/gather/gather-basic.ink diff --git a/src/test/resources/inkfiles/gather/gather-basic.ink.json b/compiler/src/test/resources/inkfiles/gather/gather-basic.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/gather-basic.ink.json rename to compiler/src/test/resources/inkfiles/gather/gather-basic.ink.json diff --git a/src/test/resources/inkfiles/gather/gather-chain.ink b/compiler/src/test/resources/inkfiles/gather/gather-chain.ink similarity index 100% rename from src/test/resources/inkfiles/gather/gather-chain.ink rename to compiler/src/test/resources/inkfiles/gather/gather-chain.ink diff --git a/src/test/resources/inkfiles/gather/gather-chain.ink.json b/compiler/src/test/resources/inkfiles/gather/gather-chain.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/gather-chain.ink.json rename to compiler/src/test/resources/inkfiles/gather/gather-chain.ink.json diff --git a/src/test/resources/inkfiles/gather/nested-flow.ink b/compiler/src/test/resources/inkfiles/gather/nested-flow.ink similarity index 100% rename from src/test/resources/inkfiles/gather/nested-flow.ink rename to compiler/src/test/resources/inkfiles/gather/nested-flow.ink diff --git a/src/test/resources/inkfiles/gather/nested-flow.ink.json b/compiler/src/test/resources/inkfiles/gather/nested-flow.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/nested-flow.ink.json rename to compiler/src/test/resources/inkfiles/gather/nested-flow.ink.json diff --git a/src/test/resources/inkfiles/gather/nested-gather.ink b/compiler/src/test/resources/inkfiles/gather/nested-gather.ink similarity index 100% rename from src/test/resources/inkfiles/gather/nested-gather.ink rename to compiler/src/test/resources/inkfiles/gather/nested-gather.ink diff --git a/src/test/resources/inkfiles/gather/nested-gather.ink.json b/compiler/src/test/resources/inkfiles/gather/nested-gather.ink.json similarity index 100% rename from src/test/resources/inkfiles/gather/nested-gather.ink.json rename to compiler/src/test/resources/inkfiles/gather/nested-gather.ink.json diff --git a/src/test/resources/inkfiles/glue/glue-with-divert.ink b/compiler/src/test/resources/inkfiles/glue/glue-with-divert.ink similarity index 100% rename from src/test/resources/inkfiles/glue/glue-with-divert.ink rename to compiler/src/test/resources/inkfiles/glue/glue-with-divert.ink diff --git a/src/test/resources/inkfiles/glue/glue-with-divert.ink.json b/compiler/src/test/resources/inkfiles/glue/glue-with-divert.ink.json similarity index 100% rename from src/test/resources/inkfiles/glue/glue-with-divert.ink.json rename to compiler/src/test/resources/inkfiles/glue/glue-with-divert.ink.json diff --git a/src/test/resources/inkfiles/glue/left-right-glue-matching.ink b/compiler/src/test/resources/inkfiles/glue/left-right-glue-matching.ink similarity index 100% rename from src/test/resources/inkfiles/glue/left-right-glue-matching.ink rename to compiler/src/test/resources/inkfiles/glue/left-right-glue-matching.ink diff --git a/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json b/compiler/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json similarity index 100% rename from src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json rename to compiler/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json diff --git a/src/test/resources/inkfiles/glue/simple-glue.ink b/compiler/src/test/resources/inkfiles/glue/simple-glue.ink similarity index 100% rename from src/test/resources/inkfiles/glue/simple-glue.ink rename to compiler/src/test/resources/inkfiles/glue/simple-glue.ink diff --git a/src/test/resources/inkfiles/glue/simple-glue.ink.json b/compiler/src/test/resources/inkfiles/glue/simple-glue.ink.json similarity index 100% rename from src/test/resources/inkfiles/glue/simple-glue.ink.json rename to compiler/src/test/resources/inkfiles/glue/simple-glue.ink.json diff --git a/src/test/resources/inkfiles/glue/testbugfix1.ink b/compiler/src/test/resources/inkfiles/glue/testbugfix1.ink similarity index 100% rename from src/test/resources/inkfiles/glue/testbugfix1.ink rename to compiler/src/test/resources/inkfiles/glue/testbugfix1.ink diff --git a/src/test/resources/inkfiles/glue/testbugfix1.ink.json b/compiler/src/test/resources/inkfiles/glue/testbugfix1.ink.json similarity index 100% rename from src/test/resources/inkfiles/glue/testbugfix1.ink.json rename to compiler/src/test/resources/inkfiles/glue/testbugfix1.ink.json diff --git a/src/test/resources/inkfiles/glue/testbugfix2.ink b/compiler/src/test/resources/inkfiles/glue/testbugfix2.ink similarity index 100% rename from src/test/resources/inkfiles/glue/testbugfix2.ink rename to compiler/src/test/resources/inkfiles/glue/testbugfix2.ink diff --git a/src/test/resources/inkfiles/glue/testbugfix2.ink.json b/compiler/src/test/resources/inkfiles/glue/testbugfix2.ink.json similarity index 100% rename from src/test/resources/inkfiles/glue/testbugfix2.ink.json rename to compiler/src/test/resources/inkfiles/glue/testbugfix2.ink.json diff --git a/src/test/resources/inkfiles/knot/multi-line.ink b/compiler/src/test/resources/inkfiles/knot/multi-line.ink similarity index 100% rename from src/test/resources/inkfiles/knot/multi-line.ink rename to compiler/src/test/resources/inkfiles/knot/multi-line.ink diff --git a/src/test/resources/inkfiles/knot/multi-line.ink.json b/compiler/src/test/resources/inkfiles/knot/multi-line.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/multi-line.ink.json rename to compiler/src/test/resources/inkfiles/knot/multi-line.ink.json diff --git a/src/test/resources/inkfiles/knot/param-floats.ink b/compiler/src/test/resources/inkfiles/knot/param-floats.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-floats.ink rename to compiler/src/test/resources/inkfiles/knot/param-floats.ink diff --git a/src/test/resources/inkfiles/knot/param-floats.ink.json b/compiler/src/test/resources/inkfiles/knot/param-floats.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-floats.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-floats.ink.json diff --git a/src/test/resources/inkfiles/knot/param-ints.ink b/compiler/src/test/resources/inkfiles/knot/param-ints.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-ints.ink rename to compiler/src/test/resources/inkfiles/knot/param-ints.ink diff --git a/src/test/resources/inkfiles/knot/param-ints.ink.json b/compiler/src/test/resources/inkfiles/knot/param-ints.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-ints.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-ints.ink.json diff --git a/src/test/resources/inkfiles/knot/param-multi.ink b/compiler/src/test/resources/inkfiles/knot/param-multi.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-multi.ink rename to compiler/src/test/resources/inkfiles/knot/param-multi.ink diff --git a/src/test/resources/inkfiles/knot/param-multi.ink.json b/compiler/src/test/resources/inkfiles/knot/param-multi.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-multi.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-multi.ink.json diff --git a/src/test/resources/inkfiles/knot/param-recurse.ink b/compiler/src/test/resources/inkfiles/knot/param-recurse.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-recurse.ink rename to compiler/src/test/resources/inkfiles/knot/param-recurse.ink diff --git a/src/test/resources/inkfiles/knot/param-recurse.ink.json b/compiler/src/test/resources/inkfiles/knot/param-recurse.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-recurse.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-recurse.ink.json diff --git a/src/test/resources/inkfiles/knot/param-strings.ink b/compiler/src/test/resources/inkfiles/knot/param-strings.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-strings.ink rename to compiler/src/test/resources/inkfiles/knot/param-strings.ink diff --git a/src/test/resources/inkfiles/knot/param-strings.ink.json b/compiler/src/test/resources/inkfiles/knot/param-strings.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-strings.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-strings.ink.json diff --git a/src/test/resources/inkfiles/knot/param-vars.ink b/compiler/src/test/resources/inkfiles/knot/param-vars.ink similarity index 100% rename from src/test/resources/inkfiles/knot/param-vars.ink rename to compiler/src/test/resources/inkfiles/knot/param-vars.ink diff --git a/src/test/resources/inkfiles/knot/param-vars.ink.json b/compiler/src/test/resources/inkfiles/knot/param-vars.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/param-vars.ink.json rename to compiler/src/test/resources/inkfiles/knot/param-vars.ink.json diff --git a/src/test/resources/inkfiles/knot/single-line.ink b/compiler/src/test/resources/inkfiles/knot/single-line.ink similarity index 100% rename from src/test/resources/inkfiles/knot/single-line.ink rename to compiler/src/test/resources/inkfiles/knot/single-line.ink diff --git a/src/test/resources/inkfiles/knot/single-line.ink.json b/compiler/src/test/resources/inkfiles/knot/single-line.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/single-line.ink.json rename to compiler/src/test/resources/inkfiles/knot/single-line.ink.json diff --git a/src/test/resources/inkfiles/knot/strip-empty-lines.ink b/compiler/src/test/resources/inkfiles/knot/strip-empty-lines.ink similarity index 100% rename from src/test/resources/inkfiles/knot/strip-empty-lines.ink rename to compiler/src/test/resources/inkfiles/knot/strip-empty-lines.ink diff --git a/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json b/compiler/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json similarity index 100% rename from src/test/resources/inkfiles/knot/strip-empty-lines.ink.json rename to compiler/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json diff --git a/src/test/resources/inkfiles/lists/basic-operations.ink b/compiler/src/test/resources/inkfiles/lists/basic-operations.ink similarity index 100% rename from src/test/resources/inkfiles/lists/basic-operations.ink rename to compiler/src/test/resources/inkfiles/lists/basic-operations.ink diff --git a/src/test/resources/inkfiles/lists/basic-operations.ink.json b/compiler/src/test/resources/inkfiles/lists/basic-operations.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/basic-operations.ink.json rename to compiler/src/test/resources/inkfiles/lists/basic-operations.ink.json diff --git a/src/test/resources/inkfiles/lists/bug-adding-element.ink b/compiler/src/test/resources/inkfiles/lists/bug-adding-element.ink similarity index 100% rename from src/test/resources/inkfiles/lists/bug-adding-element.ink rename to compiler/src/test/resources/inkfiles/lists/bug-adding-element.ink diff --git a/src/test/resources/inkfiles/lists/bug-adding-element.ink.json b/compiler/src/test/resources/inkfiles/lists/bug-adding-element.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/bug-adding-element.ink.json rename to compiler/src/test/resources/inkfiles/lists/bug-adding-element.ink.json diff --git a/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink b/compiler/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink similarity index 100% rename from src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink rename to compiler/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink diff --git a/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json b/compiler/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json rename to compiler/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json diff --git a/src/test/resources/inkfiles/lists/empty-list-origin.ink b/compiler/src/test/resources/inkfiles/lists/empty-list-origin.ink similarity index 100% rename from src/test/resources/inkfiles/lists/empty-list-origin.ink rename to compiler/src/test/resources/inkfiles/lists/empty-list-origin.ink diff --git a/src/test/resources/inkfiles/lists/empty-list-origin.ink.json b/compiler/src/test/resources/inkfiles/lists/empty-list-origin.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/empty-list-origin.ink.json rename to compiler/src/test/resources/inkfiles/lists/empty-list-origin.ink.json diff --git a/src/test/resources/inkfiles/lists/list-all.ink b/compiler/src/test/resources/inkfiles/lists/list-all.ink similarity index 100% rename from src/test/resources/inkfiles/lists/list-all.ink rename to compiler/src/test/resources/inkfiles/lists/list-all.ink diff --git a/src/test/resources/inkfiles/lists/list-all.ink.json b/compiler/src/test/resources/inkfiles/lists/list-all.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/list-all.ink.json rename to compiler/src/test/resources/inkfiles/lists/list-all.ink.json diff --git a/src/test/resources/inkfiles/lists/list-mixed-items.ink b/compiler/src/test/resources/inkfiles/lists/list-mixed-items.ink similarity index 100% rename from src/test/resources/inkfiles/lists/list-mixed-items.ink rename to compiler/src/test/resources/inkfiles/lists/list-mixed-items.ink diff --git a/src/test/resources/inkfiles/lists/list-mixed-items.ink.json b/compiler/src/test/resources/inkfiles/lists/list-mixed-items.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/list-mixed-items.ink.json rename to compiler/src/test/resources/inkfiles/lists/list-mixed-items.ink.json diff --git a/src/test/resources/inkfiles/lists/list-range.ink b/compiler/src/test/resources/inkfiles/lists/list-range.ink similarity index 100% rename from src/test/resources/inkfiles/lists/list-range.ink rename to compiler/src/test/resources/inkfiles/lists/list-range.ink diff --git a/src/test/resources/inkfiles/lists/list-range.ink.json b/compiler/src/test/resources/inkfiles/lists/list-range.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/list-range.ink.json rename to compiler/src/test/resources/inkfiles/lists/list-range.ink.json diff --git a/src/test/resources/inkfiles/lists/list-save-load.ink b/compiler/src/test/resources/inkfiles/lists/list-save-load.ink similarity index 100% rename from src/test/resources/inkfiles/lists/list-save-load.ink rename to compiler/src/test/resources/inkfiles/lists/list-save-load.ink diff --git a/src/test/resources/inkfiles/lists/list-save-load.ink.json b/compiler/src/test/resources/inkfiles/lists/list-save-load.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/list-save-load.ink.json rename to compiler/src/test/resources/inkfiles/lists/list-save-load.ink.json diff --git a/src/test/resources/inkfiles/lists/more-list-operations.ink b/compiler/src/test/resources/inkfiles/lists/more-list-operations.ink similarity index 100% rename from src/test/resources/inkfiles/lists/more-list-operations.ink rename to compiler/src/test/resources/inkfiles/lists/more-list-operations.ink diff --git a/src/test/resources/inkfiles/lists/more-list-operations.ink.json b/compiler/src/test/resources/inkfiles/lists/more-list-operations.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/more-list-operations.ink.json rename to compiler/src/test/resources/inkfiles/lists/more-list-operations.ink.json diff --git a/src/test/resources/inkfiles/lists/more-list-operations2.ink b/compiler/src/test/resources/inkfiles/lists/more-list-operations2.ink similarity index 100% rename from src/test/resources/inkfiles/lists/more-list-operations2.ink rename to compiler/src/test/resources/inkfiles/lists/more-list-operations2.ink diff --git a/src/test/resources/inkfiles/lists/more-list-operations2.ink.json b/compiler/src/test/resources/inkfiles/lists/more-list-operations2.ink.json similarity index 100% rename from src/test/resources/inkfiles/lists/more-list-operations2.ink.json rename to compiler/src/test/resources/inkfiles/lists/more-list-operations2.ink.json diff --git a/src/test/resources/inkfiles/misc/issue15.ink b/compiler/src/test/resources/inkfiles/misc/issue15.ink similarity index 100% rename from src/test/resources/inkfiles/misc/issue15.ink rename to compiler/src/test/resources/inkfiles/misc/issue15.ink diff --git a/src/test/resources/inkfiles/misc/issue15.ink.json b/compiler/src/test/resources/inkfiles/misc/issue15.ink.json similarity index 100% rename from src/test/resources/inkfiles/misc/issue15.ink.json rename to compiler/src/test/resources/inkfiles/misc/issue15.ink.json diff --git a/src/test/resources/inkfiles/runtime/external-function-0-arg.ink b/compiler/src/test/resources/inkfiles/runtime/external-function-0-arg.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-0-arg.ink rename to compiler/src/test/resources/inkfiles/runtime/external-function-0-arg.ink diff --git a/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json b/compiler/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json rename to compiler/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json diff --git a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink b/compiler/src/test/resources/inkfiles/runtime/external-function-1-arg.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-1-arg.ink rename to compiler/src/test/resources/inkfiles/runtime/external-function-1-arg.ink diff --git a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json b/compiler/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json rename to compiler/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json diff --git a/src/test/resources/inkfiles/runtime/external-function-2-arg.ink b/compiler/src/test/resources/inkfiles/runtime/external-function-2-arg.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-2-arg.ink rename to compiler/src/test/resources/inkfiles/runtime/external-function-2-arg.ink diff --git a/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json b/compiler/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json rename to compiler/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json diff --git a/src/test/resources/inkfiles/runtime/external-function-3-arg.ink b/compiler/src/test/resources/inkfiles/runtime/external-function-3-arg.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-3-arg.ink rename to compiler/src/test/resources/inkfiles/runtime/external-function-3-arg.ink diff --git a/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json b/compiler/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json rename to compiler/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json diff --git a/src/test/resources/inkfiles/runtime/jump-knot.ink b/compiler/src/test/resources/inkfiles/runtime/jump-knot.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/jump-knot.ink rename to compiler/src/test/resources/inkfiles/runtime/jump-knot.ink diff --git a/src/test/resources/inkfiles/runtime/jump-knot.ink.json b/compiler/src/test/resources/inkfiles/runtime/jump-knot.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/jump-knot.ink.json rename to compiler/src/test/resources/inkfiles/runtime/jump-knot.ink.json diff --git a/src/test/resources/inkfiles/runtime/jump-stitch.ink b/compiler/src/test/resources/inkfiles/runtime/jump-stitch.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/jump-stitch.ink rename to compiler/src/test/resources/inkfiles/runtime/jump-stitch.ink diff --git a/src/test/resources/inkfiles/runtime/jump-stitch.ink.json b/compiler/src/test/resources/inkfiles/runtime/jump-stitch.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/jump-stitch.ink.json rename to compiler/src/test/resources/inkfiles/runtime/jump-stitch.ink.json diff --git a/src/test/resources/inkfiles/runtime/load-save.ink b/compiler/src/test/resources/inkfiles/runtime/load-save.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/load-save.ink rename to compiler/src/test/resources/inkfiles/runtime/load-save.ink diff --git a/src/test/resources/inkfiles/runtime/load-save.ink.json b/compiler/src/test/resources/inkfiles/runtime/load-save.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/load-save.ink.json rename to compiler/src/test/resources/inkfiles/runtime/load-save.ink.json diff --git a/src/test/resources/inkfiles/runtime/multiflow-basics.ink b/compiler/src/test/resources/inkfiles/runtime/multiflow-basics.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/multiflow-basics.ink rename to compiler/src/test/resources/inkfiles/runtime/multiflow-basics.ink diff --git a/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json b/compiler/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/multiflow-basics.ink.json rename to compiler/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json diff --git a/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink b/compiler/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink rename to compiler/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink diff --git a/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json b/compiler/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json rename to compiler/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json diff --git a/src/test/resources/inkfiles/runtime/read-visit-counts.ink b/compiler/src/test/resources/inkfiles/runtime/read-visit-counts.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/read-visit-counts.ink rename to compiler/src/test/resources/inkfiles/runtime/read-visit-counts.ink diff --git a/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json b/compiler/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/read-visit-counts.ink.json rename to compiler/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json diff --git a/src/test/resources/inkfiles/runtime/saving-loading.ink b/compiler/src/test/resources/inkfiles/runtime/saving-loading.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/saving-loading.ink rename to compiler/src/test/resources/inkfiles/runtime/saving-loading.ink diff --git a/src/test/resources/inkfiles/runtime/saving-loading.ink.json b/compiler/src/test/resources/inkfiles/runtime/saving-loading.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/saving-loading.ink.json rename to compiler/src/test/resources/inkfiles/runtime/saving-loading.ink.json diff --git a/src/test/resources/inkfiles/runtime/set-get-variables.ink b/compiler/src/test/resources/inkfiles/runtime/set-get-variables.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/set-get-variables.ink rename to compiler/src/test/resources/inkfiles/runtime/set-get-variables.ink diff --git a/src/test/resources/inkfiles/runtime/set-get-variables.ink.json b/compiler/src/test/resources/inkfiles/runtime/set-get-variables.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/set-get-variables.ink.json rename to compiler/src/test/resources/inkfiles/runtime/set-get-variables.ink.json diff --git a/src/test/resources/inkfiles/runtime/variable-observers.ink b/compiler/src/test/resources/inkfiles/runtime/variable-observers.ink similarity index 100% rename from src/test/resources/inkfiles/runtime/variable-observers.ink rename to compiler/src/test/resources/inkfiles/runtime/variable-observers.ink diff --git a/src/test/resources/inkfiles/runtime/variable-observers.ink.json b/compiler/src/test/resources/inkfiles/runtime/variable-observers.ink.json similarity index 100% rename from src/test/resources/inkfiles/runtime/variable-observers.ink.json rename to compiler/src/test/resources/inkfiles/runtime/variable-observers.ink.json diff --git a/src/test/resources/inkfiles/stitch/auto-stitch.ink b/compiler/src/test/resources/inkfiles/stitch/auto-stitch.ink similarity index 100% rename from src/test/resources/inkfiles/stitch/auto-stitch.ink rename to compiler/src/test/resources/inkfiles/stitch/auto-stitch.ink diff --git a/src/test/resources/inkfiles/stitch/auto-stitch.ink.json b/compiler/src/test/resources/inkfiles/stitch/auto-stitch.ink.json similarity index 100% rename from src/test/resources/inkfiles/stitch/auto-stitch.ink.json rename to compiler/src/test/resources/inkfiles/stitch/auto-stitch.ink.json diff --git a/src/test/resources/inkfiles/stitch/manual-stitch.ink b/compiler/src/test/resources/inkfiles/stitch/manual-stitch.ink similarity index 100% rename from src/test/resources/inkfiles/stitch/manual-stitch.ink rename to compiler/src/test/resources/inkfiles/stitch/manual-stitch.ink diff --git a/src/test/resources/inkfiles/stitch/manual-stitch.ink.json b/compiler/src/test/resources/inkfiles/stitch/manual-stitch.ink.json similarity index 100% rename from src/test/resources/inkfiles/stitch/manual-stitch.ink.json rename to compiler/src/test/resources/inkfiles/stitch/manual-stitch.ink.json diff --git a/src/test/resources/inkfiles/tags/tags.ink b/compiler/src/test/resources/inkfiles/tags/tags.ink similarity index 100% rename from src/test/resources/inkfiles/tags/tags.ink rename to compiler/src/test/resources/inkfiles/tags/tags.ink diff --git a/src/test/resources/inkfiles/tags/tags.ink.json b/compiler/src/test/resources/inkfiles/tags/tags.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tags.ink.json rename to compiler/src/test/resources/inkfiles/tags/tags.ink.json diff --git a/src/test/resources/inkfiles/tags/tagsDynamicContent.ink b/compiler/src/test/resources/inkfiles/tags/tagsDynamicContent.ink similarity index 100% rename from src/test/resources/inkfiles/tags/tagsDynamicContent.ink rename to compiler/src/test/resources/inkfiles/tags/tagsDynamicContent.ink diff --git a/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json b/compiler/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json rename to compiler/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json diff --git a/src/test/resources/inkfiles/tags/tagsInChoice.ink b/compiler/src/test/resources/inkfiles/tags/tagsInChoice.ink similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInChoice.ink rename to compiler/src/test/resources/inkfiles/tags/tagsInChoice.ink diff --git a/src/test/resources/inkfiles/tags/tagsInChoice.ink.json b/compiler/src/test/resources/inkfiles/tags/tagsInChoice.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInChoice.ink.json rename to compiler/src/test/resources/inkfiles/tags/tagsInChoice.ink.json diff --git a/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink b/compiler/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink rename to compiler/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink diff --git a/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json b/compiler/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json rename to compiler/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json diff --git a/compiler/src/test/resources/inkfiles/tags/tagsInLines.ink b/compiler/src/test/resources/inkfiles/tags/tagsInLines.ink new file mode 100644 index 0000000..f86afbd --- /dev/null +++ b/compiler/src/test/resources/inkfiles/tags/tagsInLines.ink @@ -0,0 +1,2 @@ +í +a diff --git a/src/test/resources/inkfiles/tags/tagsInLines.ink.json b/compiler/src/test/resources/inkfiles/tags/tagsInLines.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInLines.ink.json rename to compiler/src/test/resources/inkfiles/tags/tagsInLines.ink.json diff --git a/src/test/resources/inkfiles/tags/tagsInSeq.ink b/compiler/src/test/resources/inkfiles/tags/tagsInSeq.ink similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInSeq.ink rename to compiler/src/test/resources/inkfiles/tags/tagsInSeq.ink diff --git a/src/test/resources/inkfiles/tags/tagsInSeq.ink.json b/compiler/src/test/resources/inkfiles/tags/tagsInSeq.ink.json similarity index 100% rename from src/test/resources/inkfiles/tags/tagsInSeq.ink.json rename to compiler/src/test/resources/inkfiles/tags/tagsInSeq.ink.json diff --git a/src/test/resources/inkfiles/threads/thread-bug.ink b/compiler/src/test/resources/inkfiles/threads/thread-bug.ink similarity index 93% rename from src/test/resources/inkfiles/threads/thread-bug.ink rename to compiler/src/test/resources/inkfiles/threads/thread-bug.ink index 4fa40b0..b42b69b 100644 --- a/src/test/resources/inkfiles/threads/thread-bug.ink +++ b/compiler/src/test/resources/inkfiles/threads/thread-bug.ink @@ -1,14 +1,14 @@ --> start - -=== start === -Here is some gold. Do you want it? -- (top) - <- choices(-> top) - + Yes - You win! - -> END - -=== choices(-> goback) === -+ No - Try again! +-> start + +=== start === +Here is some gold. Do you want it? +- (top) + <- choices(-> top) + + Yes + You win! + -> END + +=== choices(-> goback) === ++ No + Try again! -> goback \ No newline at end of file diff --git a/src/test/resources/inkfiles/threads/thread-bug.ink.json b/compiler/src/test/resources/inkfiles/threads/thread-bug.ink.json similarity index 100% rename from src/test/resources/inkfiles/threads/thread-bug.ink.json rename to compiler/src/test/resources/inkfiles/threads/thread-bug.ink.json diff --git a/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink b/compiler/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink similarity index 100% rename from src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink rename to compiler/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink diff --git a/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json b/compiler/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json similarity index 100% rename from src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json rename to compiler/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json diff --git a/src/test/resources/inkfiles/variable/var-divert.ink b/compiler/src/test/resources/inkfiles/variable/var-divert.ink similarity index 100% rename from src/test/resources/inkfiles/variable/var-divert.ink rename to compiler/src/test/resources/inkfiles/variable/var-divert.ink diff --git a/src/test/resources/inkfiles/variable/var-divert.ink.json b/compiler/src/test/resources/inkfiles/variable/var-divert.ink.json similarity index 100% rename from src/test/resources/inkfiles/variable/var-divert.ink.json rename to compiler/src/test/resources/inkfiles/variable/var-divert.ink.json diff --git a/src/test/resources/inkfiles/variable/varcalc.ink b/compiler/src/test/resources/inkfiles/variable/varcalc.ink similarity index 100% rename from src/test/resources/inkfiles/variable/varcalc.ink rename to compiler/src/test/resources/inkfiles/variable/varcalc.ink diff --git a/src/test/resources/inkfiles/variable/varcalc.ink.json b/compiler/src/test/resources/inkfiles/variable/varcalc.ink.json similarity index 100% rename from src/test/resources/inkfiles/variable/varcalc.ink.json rename to compiler/src/test/resources/inkfiles/variable/varcalc.ink.json diff --git a/src/test/resources/inkfiles/variable/variable-declaration.ink b/compiler/src/test/resources/inkfiles/variable/variable-declaration.ink similarity index 100% rename from src/test/resources/inkfiles/variable/variable-declaration.ink rename to compiler/src/test/resources/inkfiles/variable/variable-declaration.ink diff --git a/src/test/resources/inkfiles/variable/variable-declaration.ink.json b/compiler/src/test/resources/inkfiles/variable/variable-declaration.ink.json similarity index 100% rename from src/test/resources/inkfiles/variable/variable-declaration.ink.json rename to compiler/src/test/resources/inkfiles/variable/variable-declaration.ink.json diff --git a/src/test/resources/inkfiles/variable/varstringinc.ink b/compiler/src/test/resources/inkfiles/variable/varstringinc.ink similarity index 100% rename from src/test/resources/inkfiles/variable/varstringinc.ink rename to compiler/src/test/resources/inkfiles/variable/varstringinc.ink diff --git a/src/test/resources/inkfiles/variable/varstringinc.ink.json b/compiler/src/test/resources/inkfiles/variable/varstringinc.ink.json similarity index 100% rename from src/test/resources/inkfiles/variable/varstringinc.ink.json rename to compiler/src/test/resources/inkfiles/variable/varstringinc.ink.json diff --git a/src/test/resources/inkfiles/variabletext/cycle.ink b/compiler/src/test/resources/inkfiles/variabletext/cycle.ink similarity index 100% rename from src/test/resources/inkfiles/variabletext/cycle.ink rename to compiler/src/test/resources/inkfiles/variabletext/cycle.ink diff --git a/src/test/resources/inkfiles/variabletext/cycle.ink.json b/compiler/src/test/resources/inkfiles/variabletext/cycle.ink.json similarity index 100% rename from src/test/resources/inkfiles/variabletext/cycle.ink.json rename to compiler/src/test/resources/inkfiles/variabletext/cycle.ink.json diff --git a/src/test/resources/inkfiles/variabletext/empty-elements.ink b/compiler/src/test/resources/inkfiles/variabletext/empty-elements.ink similarity index 100% rename from src/test/resources/inkfiles/variabletext/empty-elements.ink rename to compiler/src/test/resources/inkfiles/variabletext/empty-elements.ink diff --git a/src/test/resources/inkfiles/variabletext/empty-elements.ink.json b/compiler/src/test/resources/inkfiles/variabletext/empty-elements.ink.json similarity index 100% rename from src/test/resources/inkfiles/variabletext/empty-elements.ink.json rename to compiler/src/test/resources/inkfiles/variabletext/empty-elements.ink.json diff --git a/src/test/resources/inkfiles/variabletext/list-in-choice.ink b/compiler/src/test/resources/inkfiles/variabletext/list-in-choice.ink similarity index 100% rename from src/test/resources/inkfiles/variabletext/list-in-choice.ink rename to compiler/src/test/resources/inkfiles/variabletext/list-in-choice.ink diff --git a/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json b/compiler/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json similarity index 100% rename from src/test/resources/inkfiles/variabletext/list-in-choice.ink.json rename to compiler/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json diff --git a/compiler/src/test/resources/inkfiles/variabletext/minus-one.ink b/compiler/src/test/resources/inkfiles/variabletext/minus-one.ink new file mode 100644 index 0000000..3e7f053 --- /dev/null +++ b/compiler/src/test/resources/inkfiles/variabletext/minus-one.ink @@ -0,0 +1 @@ +We needed to find nothing. diff --git a/src/test/resources/inkfiles/variabletext/once.ink b/compiler/src/test/resources/inkfiles/variabletext/once.ink similarity index 100% rename from src/test/resources/inkfiles/variabletext/once.ink rename to compiler/src/test/resources/inkfiles/variabletext/once.ink diff --git a/src/test/resources/inkfiles/variabletext/once.ink.json b/compiler/src/test/resources/inkfiles/variabletext/once.ink.json similarity index 100% rename from src/test/resources/inkfiles/variabletext/once.ink.json rename to compiler/src/test/resources/inkfiles/variabletext/once.ink.json diff --git a/compiler/src/test/resources/inkfiles/variabletext/one.ink b/compiler/src/test/resources/inkfiles/variabletext/one.ink new file mode 100644 index 0000000..0627959 --- /dev/null +++ b/compiler/src/test/resources/inkfiles/variabletext/one.ink @@ -0,0 +1 @@ +We needed to find one apple. diff --git a/src/test/resources/inkfiles/variabletext/sequence.ink b/compiler/src/test/resources/inkfiles/variabletext/sequence.ink similarity index 100% rename from src/test/resources/inkfiles/variabletext/sequence.ink rename to compiler/src/test/resources/inkfiles/variabletext/sequence.ink diff --git a/src/test/resources/inkfiles/variabletext/sequence.ink.json b/compiler/src/test/resources/inkfiles/variabletext/sequence.ink.json similarity index 100% rename from src/test/resources/inkfiles/variabletext/sequence.ink.json rename to compiler/src/test/resources/inkfiles/variabletext/sequence.ink.json diff --git a/compiler/src/test/resources/inkfiles/variabletext/ten.ink b/compiler/src/test/resources/inkfiles/variabletext/ten.ink new file mode 100644 index 0000000..4a43227 --- /dev/null +++ b/compiler/src/test/resources/inkfiles/variabletext/ten.ink @@ -0,0 +1 @@ +We needed to find many oranges. diff --git a/inklecate/build.gradle b/inklecate/build.gradle new file mode 100644 index 0000000..8b9aaa9 --- /dev/null +++ b/inklecate/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'application' +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(":compiler") + implementation project(":runtime") +} + +application { + mainClass = 'com.bladecoder.ink.inklecate.CommandLineTool' +} + +jar { + manifest.attributes += [ + 'Main-Class': application.mainClass + ] + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLinePlayer.java b/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLinePlayer.java new file mode 100644 index 0000000..1459ccd --- /dev/null +++ b/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLinePlayer.java @@ -0,0 +1,276 @@ +package com.bladecoder.ink.inklecate; + +import com.bladecoder.ink.compiler.CommandLineInput; +import com.bladecoder.ink.compiler.Compiler; +import com.bladecoder.ink.compiler.InkParser; +import com.bladecoder.ink.runtime.Choice; +import com.bladecoder.ink.runtime.Error.ErrorType; +import com.bladecoder.ink.runtime.SimpleJson; +import com.bladecoder.ink.runtime.Story; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class CommandLinePlayer { + private final Story story; + private boolean autoPlay; + private boolean keepOpenAfterStoryFinish; + private final Compiler compiler; + private final boolean jsonOutput; + private final List errors = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + + public CommandLinePlayer( + Story story, boolean autoPlay, Compiler compiler, boolean keepOpenAfterStoryFinish, boolean jsonOutput) { + this.story = story; + this.story.onError = this::onStoryError; + this.autoPlay = autoPlay; + this.compiler = compiler; + this.keepOpenAfterStoryFinish = keepOpenAfterStoryFinish; + this.jsonOutput = jsonOutput; + } + + public void begin() throws Exception { + evaluateStory(); + + Random rand = new Random(); + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + + while (!story.getCurrentChoices().isEmpty() || keepOpenAfterStoryFinish) { + List choices = story.getCurrentChoices(); + int choiceIdx = 0; + boolean choiceIsValid = false; + String userDivertedPath = null; + + if (autoPlay) { + choiceIdx = rand.nextInt(choices.size()); + } else { + if (!jsonOutput) { + System.out.println(); + int i = 1; + for (Choice choice : choices) { + System.out.println(i + ": " + choice.getText()); + i++; + } + } else { + SimpleJson.Writer writer = new SimpleJson.Writer(); + writer.writeObjectStart(); + writer.writePropertyStart("choices"); + writer.writeArrayStart(); + for (Choice choice : choices) { + writer.writeObjectStart(); + writer.writePropertyStart("text"); + writer.write(choice.getText()); + writer.writePropertyEnd(); + if (choice.getTags() != null && !choice.getTags().isEmpty()) { + writer.writePropertyStart("tags"); + writer.writeArrayStart(); + for (String tag : choice.getTags()) { + writer.write(tag); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + writer.writePropertyStart("tag_count"); + writer.write(choice.getTags().size()); + writer.writePropertyEnd(); + } + writer.writeObjectEnd(); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + writer.writeObjectEnd(); + System.out.println(writer.toString()); + } + + do { + if (!jsonOutput) { + System.out.print("?> "); + } else { + System.out.print("{\"needInput\": true}"); + } + + String userInput = reader.readLine(); + if (userInput == null) { + if (jsonOutput) { + System.out.println("{\"close\": true}"); + } else { + System.out.println(""); + } + return; + } + + Compiler.CommandLineInputResult result = readCommandLineInput(userInput); + if (result.output != null) { + if (jsonOutput) { + SimpleJson.Writer writer = new SimpleJson.Writer(); + writer.writeObjectStart(); + writer.writeProperty("cmdOutput", result.output); + writer.writeObjectEnd(); + System.out.println(writer.toString()); + } else { + System.out.println(result.output); + } + } + + if (result.requestsExit) { + return; + } + + if (result.divertedPath != null) { + userDivertedPath = result.divertedPath; + } + + if (result.choiceIdx >= 0) { + if (result.choiceIdx >= choices.size()) { + if (!jsonOutput) { + System.out.println("Choice out of range"); + } + } else { + choiceIdx = result.choiceIdx; + choiceIsValid = true; + } + } + + } while (!choiceIsValid && userDivertedPath == null); + } + + if (choiceIsValid) { + story.chooseChoiceIndex(choiceIdx); + } else if (userDivertedPath != null) { + story.choosePathString(userDivertedPath); + } + + evaluateStory(); + } + } + + private void evaluateStory() throws Exception { + int lineCounter = 0; + + while (story.canContinue()) { + story.Continue(); + + if (compiler != null) { + compiler.retrieveDebugSourceForLatestContent(); + } + + if (jsonOutput) { + SimpleJson.Writer writer = new SimpleJson.Writer(); + writer.writeObjectStart(); + writer.writeProperty("text", story.getCurrentText()); + writer.writeObjectEnd(); + System.out.println(writer.toString()); + } else { + System.out.print(story.getCurrentText()); + } + + List tags = story.getCurrentTags(); + if (!tags.isEmpty()) { + if (jsonOutput) { + SimpleJson.Writer writer = new SimpleJson.Writer(); + writer.writeObjectStart(); + writer.writePropertyStart("tags"); + writer.writeArrayStart(); + for (String tag : tags) { + writer.write(tag); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + writer.writeObjectEnd(); + System.out.println(writer.toString()); + } else { + System.out.println("# tags: " + String.join(", ", tags)); + } + } + + if (jsonOutput && (!errors.isEmpty() || !warnings.isEmpty())) { + SimpleJson.Writer issueWriter = new SimpleJson.Writer(); + issueWriter.writeObjectStart(); + issueWriter.writePropertyStart("issues"); + issueWriter.writeArrayStart(); + for (String errorMsg : errors) { + issueWriter.write(errorMsg); + } + for (String warningMsg : warnings) { + issueWriter.write(warningMsg); + } + issueWriter.writeArrayEnd(); + issueWriter.writePropertyEnd(); + issueWriter.writeObjectEnd(); + System.out.println(issueWriter.toString()); + } + + if (!jsonOutput) { + for (String errorMsg : errors) { + System.out.println(errorMsg); + } + for (String warningMsg : warnings) { + System.out.println(warningMsg); + } + } + + errors.clear(); + warnings.clear(); + + lineCounter++; + if (lineCounter > 1000) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + if (story.getCurrentChoices().isEmpty() && keepOpenAfterStoryFinish) { + if (jsonOutput) { + System.out.println("{\"end\": true}"); + } else { + System.out.println("--- End of story ---"); + } + } + } + + private void onStoryError(String msg, ErrorType type) { + if (type == ErrorType.Error) { + errors.add(msg); + } else { + warnings.add(msg); + } + } + + private Compiler.CommandLineInputResult readCommandLineInput(String userInput) throws Exception { + InkParser inputParser = new InkParser(userInput); + CommandLineInput inputResult = inputParser.CommandLineUserInput(); + Compiler.CommandLineInputResult result = new Compiler.CommandLineInputResult(); + + if (inputResult.choiceInput != null) { + result.choiceIdx = inputResult.choiceInput - 1; + return result; + } + + if (inputResult.isHelp) { + result.output = + "Type a choice number, a divert (e.g. '-> myKnot'), an expression, or a variable assignment (e.g. 'x = 5')"; + return result; + } + + if (inputResult.isExit) { + result.requestsExit = true; + return result; + } + + if (compiler != null) { + Compiler.CommandLineInputResult compilerResult = compiler.handleInput(inputResult); + if (compilerResult != null) { + return compilerResult; + } + } + + result.output = "Unexpected input. Type 'help' or a choice number."; + return result; + } +} diff --git a/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLineTool.java b/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLineTool.java new file mode 100644 index 0000000..945d3e9 --- /dev/null +++ b/inklecate/src/main/java/com/bladecoder/ink/inklecate/CommandLineTool.java @@ -0,0 +1,413 @@ +package com.bladecoder.ink.inklecate; + +import com.bladecoder.ink.compiler.Compiler; +import com.bladecoder.ink.compiler.IFileHandler; +import com.bladecoder.ink.compiler.Stats; +import com.bladecoder.ink.runtime.Error.ErrorHandler; +import com.bladecoder.ink.runtime.Error.ErrorType; +import com.bladecoder.ink.runtime.SimpleJson; +import com.bladecoder.ink.runtime.Story; +import com.bladecoder.ink.runtime.StoryException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class CommandLineTool { + private static class Options { + boolean verbose; + boolean playMode; + boolean stats; + boolean jsonOutput; + String inputFile; + String outputFile; + boolean countAllVisits; + boolean keepOpenAfterStoryFinish; + } + + public static int ExitCodeError = 1; + + public static void main(String[] args) { + new CommandLineTool(args); + } + + private CommandLineTool(String[] args) { + if (!processArguments(args)) { + exitWithUsageInstructions(); + } + + if (opts.inputFile == null) { + exitWithUsageInstructions(); + } + + String inputString; + Path workingDirectory = Paths.get("").toAbsolutePath(); + Path inputBaseDir = null; + Path outputBaseDir = workingDirectory; + + if (opts.outputFile == null) { + opts.outputFile = changeExtension(opts.inputFile, ".ink.json"); + } + boolean outputFileWasRelative = !Paths.get(opts.outputFile).isAbsolute(); + + try { + Path fullFilename = Paths.get(opts.inputFile); + if (!fullFilename.isAbsolute()) { + fullFilename = workingDirectory.resolve(opts.inputFile); + } + + if (!Files.exists(fullFilename)) { + fullFilename = workingDirectory.resolve(opts.inputFile); + } + + Path parent = fullFilename.getParent(); + if (parent != null) { + inputBaseDir = parent; + outputBaseDir = parent; + } + + opts.inputFile = fullFilename.getFileName().toString(); + + if (outputFileWasRelative) { + opts.outputFile = outputBaseDir + .resolve(changeExtension(opts.inputFile, ".ink.json")) + .toString(); + } + + inputString = readFile(fullFilename.toString()); + } catch (IOException e) { + System.out.println("Could not open file '" + opts.inputFile + "'"); + System.exit(ExitCodeError); + return; + } + + boolean inputIsJson = opts.inputFile.toLowerCase().endsWith(".json"); + if (inputIsJson && opts.stats) { + System.out.println("Cannot show stats for .json, only for .ink"); + System.exit(ExitCodeError); + return; + } + + com.bladecoder.ink.compiler.ParsedHierarchy.Story parsedStory = null; + Story story; + Compiler compiler = null; + + if (!inputIsJson) { + compiler = new Compiler( + inputString, + buildCompilerOptions(opts, opts.inputFile, pluginDirectories, this::onError, inputBaseDir)); + + if (opts.stats) { + parsedStory = compiler.parse(); + + printAllMessages(); + + Stats stats = Stats.generate(parsedStory); + + if (opts.jsonOutput) { + SimpleJson.Writer writer = new SimpleJson.Writer(); + try { + writer.writeObjectStart(); + writer.writePropertyStart("stats"); + writer.writeObjectStart(); + writer.writeProperty("words", stats.words); + writer.writeProperty("knots", stats.knots); + writer.writeProperty("stitches", stats.stitches); + writer.writeProperty("functions", stats.functions); + writer.writeProperty("choices", stats.choices); + writer.writeProperty("gathers", stats.gathers); + writer.writeProperty("diverts", stats.diverts); + writer.writeObjectEnd(); + writer.writePropertyEnd(); + writer.writeObjectEnd(); + } catch (Exception e) { + throw new RuntimeException(e); + } + System.out.println(writer.toString()); + } else { + System.out.println("Words: " + stats.words); + System.out.println("Knots: " + stats.knots); + System.out.println("Stitches: " + stats.stitches); + System.out.println("Functions: " + stats.functions); + System.out.println("Choices: " + stats.choices); + System.out.println("Gathers: " + stats.gathers); + System.out.println("Diverts: " + stats.diverts); + } + + return; + } else { + story = compiler.compile(); + } + } else { + try { + story = new Story(inputString); + } catch (Exception e) { + throw new RuntimeException(e); + } + opts.playMode = true; + } + + boolean compileSuccess = story != null && errors.isEmpty(); + if (opts.jsonOutput) { + if (compileSuccess) { + System.out.println("{\"compile-success\": true}"); + } else { + System.out.println("{\"compile-success\": false}"); + } + } + + printAllMessages(); + + if (!compileSuccess) { + System.exit(ExitCodeError); + return; + } + + if (opts.playMode) { + playing = true; + story.setAllowExternalFunctionFallbacks(true); + + CommandLinePlayer player = + new CommandLinePlayer(story, false, compiler, opts.keepOpenAfterStoryFinish, opts.jsonOutput); + + try { + player.begin(); + } catch (StoryException e) { + if (e.getMessage() != null && e.getMessage().contains("Missing function binding")) { + onError(e.getMessage(), ErrorType.Error); + printAllMessages(); + } else { + throw new RuntimeException(e); + } + } catch (Exception e) { + String storyPath = ""; + String path = story.getState().getCurrentPathString(); + if (path != null) { + storyPath = path; + } + throw new RuntimeException(e.getMessage() + " (Internal story path: " + storyPath + ")", e); + } + } else { + String jsonStr; + try { + jsonStr = story.toJson(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + try { + writeFile(opts.outputFile, jsonStr); + if (opts.jsonOutput) { + System.out.println("{\"export-complete\": true}"); + } + } catch (IOException e) { + System.out.println("Could not write to output file '" + opts.outputFile + "'"); + System.exit(ExitCodeError); + } + } + } + + private void exitWithUsageInstructions() { + System.out.println("Usage: inklecate \n" + + " -o : Output file name\n" + + " -c: Count all visits to knots, stitches and weave points, not\n" + + " just those referenced by TURNS_SINCE and read counts.\n" + + " -p: Play mode\n" + + " -j: Output in JSON format (for communication with tools like Inky)\n" + + " -s: Print stats about story including word count in JSON format\n" + + " -v: Verbose mode - print compilation timings\n" + + " -k: Keep inklecate running in play mode even after story is complete\n" + + " -x : Import plugins for the compiler."); + System.exit(ExitCodeError); + } + + private Compiler.Options buildCompilerOptions( + Options opts, String inputFile, List pluginDirectories, ErrorHandler handler, Path inputBaseDir) { + Compiler.Options options = new Compiler.Options(); + options.sourceFilename = inputFile; + options.pluginDirectories = pluginDirectories; + options.countAllVisits = opts.countAllVisits; + options.errorHandler = handler; + if (inputBaseDir != null) { + options.fileHandler = new InklecateFileHandler(inputBaseDir); + } + return options; + } + + private boolean processArguments(String[] args) { + if (args.length < 1) { + opts = null; + return false; + } + + opts = new Options(); + pluginDirectories = new ArrayList<>(); + + boolean nextArgIsOutputFilename = false; + boolean nextArgIsPluginDirectory = false; + + int argIdx = 0; + for (String arg : args) { + if (nextArgIsOutputFilename) { + opts.outputFile = arg; + nextArgIsOutputFilename = false; + } else if (nextArgIsPluginDirectory) { + pluginDirectories.add(arg); + nextArgIsPluginDirectory = false; + } + + if (arg.startsWith("-") && arg.length() > 1) { + for (int i = 1; i < arg.length(); ++i) { + char argChar = arg.charAt(i); + + switch (argChar) { + case 'p': + opts.playMode = true; + break; + case 'j': + opts.jsonOutput = true; + break; + case 'v': + opts.verbose = true; + break; + case 's': + opts.stats = true; + break; + case 'o': + nextArgIsOutputFilename = true; + break; + case 'c': + opts.countAllVisits = true; + break; + case 'x': + nextArgIsPluginDirectory = true; + break; + case 'k': + opts.keepOpenAfterStoryFinish = true; + break; + default: + System.out.println("Unsupported argument type: '" + argChar + "'"); + break; + } + } + } else if (argIdx == args.length - 1) { + opts.inputFile = arg; + } + + argIdx++; + } + + return true; + } + + private void onError(String message, ErrorType errorType) { + switch (errorType) { + case Author: + authorMessages.add(message); + break; + case Warning: + warnings.add(message); + break; + case Error: + errors.add(message); + break; + default: + break; + } + + if (playing) { + printAllMessages(); + } + } + + private void printAllMessages() { + if (opts != null && opts.jsonOutput) { + SimpleJson.Writer writer = new SimpleJson.Writer(); + try { + writer.writeObjectStart(); + writer.writePropertyStart("issues"); + writer.writeArrayStart(); + for (String msg : authorMessages) { + writer.write(msg); + } + for (String msg : warnings) { + writer.write(msg); + } + for (String msg : errors) { + writer.write(msg); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + writer.writeObjectEnd(); + } catch (Exception e) { + throw new RuntimeException(e); + } + System.out.print(writer.toString()); + } else { + printIssues(authorMessages); + printIssues(warnings); + printIssues(errors); + } + + authorMessages.clear(); + warnings.clear(); + errors.clear(); + } + + private void printIssues(List messageList) { + for (String msg : messageList) { + System.out.println(msg); + } + } + + private static String readFile(String filename) throws IOException { + Path path = Paths.get(filename); + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } + + private static void writeFile(String filename, String contents) throws IOException { + Path path = Paths.get(filename); + if (!path.isAbsolute()) { + path = Paths.get(System.getProperty("user.dir"), filename); + } + Files.write(path, contents.getBytes(StandardCharsets.UTF_8)); + } + + private static String changeExtension(String filename, String extension) { + int lastDot = filename.lastIndexOf('.'); + if (lastDot == -1) { + return filename + extension; + } + return filename.substring(0, lastDot) + extension; + } + + private static class InklecateFileHandler implements IFileHandler { + private final Path baseDir; + + private InklecateFileHandler(Path baseDir) { + this.baseDir = baseDir; + } + + @Override + public String resolveInkFilename(String includeName) { + return baseDir.resolve(includeName).toString(); + } + + @Override + public String loadInkFileContents(String fullFilename) throws IOException { + byte[] bytes = Files.readAllBytes(Paths.get(fullFilename)); + return new String(bytes, StandardCharsets.UTF_8); + } + } + + private Options opts; + private List pluginDirectories; + private final List errors = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + private final List authorMessages = new ArrayList<>(); + private boolean playing; +} diff --git a/runtime/build.gradle b/runtime/build.gradle new file mode 100644 index 0000000..0153907 --- /dev/null +++ b/runtime/build.gradle @@ -0,0 +1,44 @@ +javadoc { + title = "Blade Ink Runtime" +} +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'blade-ink' + from components.java + artifact sourcesJar + artifact javadocJar + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + pom { + name = 'blade-ink' + description = "This is a Java port of inkle's ink, a scripting language for writing interactive narrative." + url = 'https://github.com/bladecoder/blade-ink' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'bladecoder' + name = 'Rafael Garcia' + email = 'bladecoder@gmail.com' + } + } + scm { + connection = 'scm:git@github.com:bladecoder/blade-ink-java.git' + developerConnection = 'scm:git@github.com:bladecoder/blade-ink-java.git' + url = 'scm:git@github.com:bladecoder/blade-ink-java.git' + } + } + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java similarity index 98% rename from src/main/java/com/bladecoder/ink/runtime/AbstractValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java index 2212098..8ee509f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java @@ -38,7 +38,7 @@ public static AbstractValue create(Object val) { } @Override - RTObject copy() { + public RTObject copy() { return create(getValueObject()); } diff --git a/src/main/java/com/bladecoder/ink/runtime/BoolValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/BoolValue.java similarity index 94% rename from src/main/java/com/bladecoder/ink/runtime/BoolValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/BoolValue.java index 0542f6c..755eaef 100644 --- a/src/main/java/com/bladecoder/ink/runtime/BoolValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/BoolValue.java @@ -1,6 +1,6 @@ package com.bladecoder.ink.runtime; -class BoolValue extends Value { +public class BoolValue extends Value { public BoolValue() { this(false); } diff --git a/src/main/java/com/bladecoder/ink/runtime/CallStack.java b/runtime/src/main/java/com/bladecoder/ink/runtime/CallStack.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/CallStack.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/CallStack.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Choice.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Choice.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Choice.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Choice.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Container.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Container.java similarity index 93% rename from src/main/java/com/bladecoder/ink/runtime/Container.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Container.java index 431cd5a..1a9337f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Container.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/Container.java @@ -143,7 +143,7 @@ public void addContents(List contentList) throws Exception { } } - private void addContent(RTObject contentObj) throws Exception { + public void addContent(RTObject contentObj) throws Exception { getContent().add(contentObj); if (contentObj.getParent() != null) { @@ -155,6 +155,18 @@ private void addContent(RTObject contentObj) throws Exception { tryAddNamedContent(contentObj); } + public void insertContent(RTObject contentObj, int index) throws Exception { + getContent().add(index, contentObj); + + if (contentObj.getParent() != null) { + throw new Exception("content is already in " + contentObj.getParent()); + } + + contentObj.setParent(this); + + tryAddNamedContent(contentObj); + } + private void tryAddNamedContent(RTObject contentObj) { INamedContent namedContentObj = contentObj instanceof INamedContent ? (INamedContent) contentObj : null; if (namedContentObj != null && namedContentObj.hasValidName()) { @@ -172,6 +184,14 @@ public void addToNamedContentOnly(INamedContent namedContentObj) { getNamedContent().put(namedContentObj.getName(), namedContentObj); } + public void addContentsOfContainer(Container otherContainer) { + getContent().addAll(otherContainer.getContent()); + for (RTObject obj : otherContainer.getContent()) { + obj.setParent(this); + tryAddNamedContent(obj); + } + } + private RTObject contentWithPathComponent(Path.Component component) { if (component.isIndex()) { diff --git a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java similarity index 97% rename from src/main/java/com/bladecoder/ink/runtime/ControlCommand.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java index 79b0a37..a34fa07 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java @@ -51,7 +51,7 @@ public ControlCommand() { } @Override - RTObject copy() { + public RTObject copy() { return new ControlCommand(getCommandType()); } diff --git a/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java b/runtime/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Divert.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Divert.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Divert.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Divert.java diff --git a/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java similarity index 94% rename from src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java index 438321b..e1d8730 100644 --- a/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java @@ -1,6 +1,6 @@ package com.bladecoder.ink.runtime; -class DivertTargetValue extends Value { +public class DivertTargetValue extends Value { public DivertTargetValue() { super(null); } diff --git a/src/main/java/com/bladecoder/ink/runtime/Error.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Error.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Error.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Error.java diff --git a/src/main/java/com/bladecoder/ink/runtime/FloatValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/FloatValue.java similarity index 94% rename from src/main/java/com/bladecoder/ink/runtime/FloatValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/FloatValue.java index 0e61ad2..f1ceca1 100644 --- a/src/main/java/com/bladecoder/ink/runtime/FloatValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/FloatValue.java @@ -1,6 +1,6 @@ package com.bladecoder.ink.runtime; -class FloatValue extends Value { +public class FloatValue extends Value { public FloatValue() { this(0.0f); } diff --git a/src/main/java/com/bladecoder/ink/runtime/Flow.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Flow.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Flow.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Flow.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Glue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Glue.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Glue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Glue.java diff --git a/src/main/java/com/bladecoder/ink/runtime/INamedContent.java b/runtime/src/main/java/com/bladecoder/ink/runtime/INamedContent.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/INamedContent.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/INamedContent.java diff --git a/src/main/java/com/bladecoder/ink/runtime/InkList.java b/runtime/src/main/java/com/bladecoder/ink/runtime/InkList.java similarity index 99% rename from src/main/java/com/bladecoder/ink/runtime/InkList.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/InkList.java index 88c860e..b69ac9f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/InkList.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/InkList.java @@ -110,12 +110,12 @@ public List getOriginNames() { return originNames; } - void setInitialOriginName(String initialOriginName) { + public void setInitialOriginName(String initialOriginName) { originNames = new ArrayList<>(); originNames.add(initialOriginName); } - void setInitialOriginNames(List initialOriginNames) { + public void setInitialOriginNames(List initialOriginNames) { if (initialOriginNames == null) originNames = null; else { originNames = new ArrayList<>(); diff --git a/src/main/java/com/bladecoder/ink/runtime/InkListItem.java b/runtime/src/main/java/com/bladecoder/ink/runtime/InkListItem.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/InkListItem.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/InkListItem.java diff --git a/src/main/java/com/bladecoder/ink/runtime/IntValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/IntValue.java similarity index 94% rename from src/main/java/com/bladecoder/ink/runtime/IntValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/IntValue.java index 478a98f..19eb6c8 100644 --- a/src/main/java/com/bladecoder/ink/runtime/IntValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/IntValue.java @@ -1,6 +1,6 @@ package com.bladecoder.ink.runtime; -class IntValue extends Value { +public class IntValue extends Value { public IntValue() { this(0); } diff --git a/src/main/java/com/bladecoder/ink/runtime/Json.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Json.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Json.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Json.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/ListDefinition.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ListValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ListValue.java similarity index 97% rename from src/main/java/com/bladecoder/ink/runtime/ListValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ListValue.java index 169da1d..f7557b7 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ListValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/ListValue.java @@ -2,7 +2,7 @@ import java.util.Map.Entry; -class ListValue extends Value { +public class ListValue extends Value { public ListValue(InkList list) { super(list); diff --git a/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java b/runtime/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Path.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Path.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Path.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Path.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Pointer.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Pointer.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Pointer.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Pointer.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/ProfileNode.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Profiler.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Profiler.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Profiler.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Profiler.java diff --git a/src/main/java/com/bladecoder/ink/runtime/PushPopType.java b/runtime/src/main/java/com/bladecoder/ink/runtime/PushPopType.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/PushPopType.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/PushPopType.java diff --git a/src/main/java/com/bladecoder/ink/runtime/RTObject.java b/runtime/src/main/java/com/bladecoder/ink/runtime/RTObject.java similarity index 98% rename from src/main/java/com/bladecoder/ink/runtime/RTObject.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/RTObject.java index 3e8bbdd..3e5346c 100644 --- a/src/main/java/com/bladecoder/ink/runtime/RTObject.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/RTObject.java @@ -33,7 +33,7 @@ public void setParent(Container value) { parent = value; } - DebugMetadata getOwnDebugMetadata() { + public DebugMetadata getOwnDebugMetadata() { return debugMetadata; } @@ -177,7 +177,7 @@ public Container getRootContentContainer() { return ancestor instanceof Container ? (Container) ancestor : null; } - RTObject copy() throws Exception { + public RTObject copy() throws Exception { throw new UnsupportedOperationException(this.getClass().getSimpleName() + " doesn't support copying"); } } diff --git a/src/main/java/com/bladecoder/ink/runtime/SearchResult.java b/runtime/src/main/java/com/bladecoder/ink/runtime/SearchResult.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/SearchResult.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/SearchResult.java diff --git a/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java b/runtime/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java similarity index 99% rename from src/main/java/com/bladecoder/ink/runtime/SimpleJson.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java index a09f94c..1b6f5a9 100644 --- a/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java @@ -16,7 +16,7 @@ * System.Collections that are produced by the ink engine and converts to and * from JSON text. */ -class SimpleJson { +public class SimpleJson { public static HashMap textToDictionary(String text) throws Exception { return new Reader(text).toHashMap(); diff --git a/src/main/java/com/bladecoder/ink/runtime/StatePatch.java b/runtime/src/main/java/com/bladecoder/ink/runtime/StatePatch.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/StatePatch.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/StatePatch.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Stopwatch.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Story.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Story.java similarity index 99% rename from src/main/java/com/bladecoder/ink/runtime/Story.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Story.java index a15c607..ead3ddd 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Story.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/Story.java @@ -715,7 +715,7 @@ void ifAsyncWeCant(String activityStr) throws Exception { + "Continue() call beforehand."); } - SearchResult contentAtPath(Path path) throws Exception { + public SearchResult contentAtPath(Path path) throws Exception { return getMainContentContainer().contentAtPath(path); } @@ -1123,7 +1123,7 @@ void error(String message, boolean useEndLineNumber) throws Exception { // Evaluate a "hot compiled" piece of ink content, as used by the REPL-like // CommandLinePlayer. - RTObject evaluateExpression(Container exprContainer) throws StoryException, Exception { + public RTObject evaluateExpression(Container exprContainer) throws StoryException, Exception { int startCallStackHeight = state.getCallStack().getElements().size(); state.getCallStack().push(PushPopType.Tunnel); @@ -1736,9 +1736,9 @@ else if (contentObj instanceof ControlCommand) { "Expected to be in an expression when evaluating a string"); state.setInExpressionEvaluation(false); break; - // Leave it to story.currentText and story.currentTags to sort out the text from the tags - // This is mostly because we can't always rely on the existence of EndTag, and we don't want - // to try and flatten dynamic tags to strings every time \n is pushed to output + // Leave it to story.currentText and story.currentTags to sort out the text from the tags + // This is mostly because we can't always rely on the existence of EndTag, and we don't want + // to try and flatten dynamic tags to strings every time \n is pushed to output case BeginTag: state.pushToOutputStream(evalCommand); break; @@ -1817,7 +1817,7 @@ else if (contentObj instanceof ControlCommand) { } break; } - // Dynamic strings and tags are built in the same way + // Dynamic strings and tags are built in the same way case EndString: { // Since we're iterating backward through the content, @@ -2005,7 +2005,7 @@ else if (contentObj instanceof ControlCommand) { break; - // Force flow to end completely + // Force flow to end completely case End: state.forceEnd(); break; diff --git a/src/main/java/com/bladecoder/ink/runtime/StoryException.java b/runtime/src/main/java/com/bladecoder/ink/runtime/StoryException.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/StoryException.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/StoryException.java diff --git a/src/main/java/com/bladecoder/ink/runtime/StoryState.java b/runtime/src/main/java/com/bladecoder/ink/runtime/StoryState.java similarity index 99% rename from src/main/java/com/bladecoder/ink/runtime/StoryState.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/StoryState.java index 0dddaed..b2e6381 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StoryState.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/StoryState.java @@ -333,6 +333,14 @@ public String previousPathString() { else return pointer.getPath().toString(); } + public String getCurrentPathString() { + Pointer pointer = getCurrentPointer(); + if (pointer == null || pointer.isNull()) { + return null; + } + return pointer.getPath().toString(); + } + Pointer getCurrentPointer() { return getCallStack().getCurrentElement().currentPointer; } @@ -529,7 +537,7 @@ boolean hasWarning() { return currentWarnings != null && currentWarnings.size() > 0; } - List getOutputStream() { + public List getOutputStream() { return currentFlow.outputStream; } diff --git a/src/main/java/com/bladecoder/ink/runtime/StringExt.java b/runtime/src/main/java/com/bladecoder/ink/runtime/StringExt.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/StringExt.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/StringExt.java diff --git a/src/main/java/com/bladecoder/ink/runtime/StringValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/StringValue.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/StringValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/StringValue.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Tag.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Tag.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Tag.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Tag.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Value.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Value.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Value.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Value.java diff --git a/src/main/java/com/bladecoder/ink/runtime/ValueType.java b/runtime/src/main/java/com/bladecoder/ink/runtime/ValueType.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/ValueType.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/ValueType.java diff --git a/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java b/runtime/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java diff --git a/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java b/runtime/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java similarity index 95% rename from src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java index 35bb8bf..f0e1fbc 100644 --- a/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java +++ b/runtime/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java @@ -2,7 +2,7 @@ // TODO: Think: Erm, I get that this contains a string, but should // we really derive from Value? That seems a bit misleading to me. -class VariablePointerValue extends Value { +public class VariablePointerValue extends Value { // Where the variable is located // -1 = default, unknown, yet to be determined // 0 = in global scope @@ -31,7 +31,7 @@ public AbstractValue cast(ValueType newType) throws Exception { } @Override - RTObject copy() { + public RTObject copy() { return new VariablePointerValue(getVariableName(), getContextIndex()); } diff --git a/src/main/java/com/bladecoder/ink/runtime/VariableReference.java b/runtime/src/main/java/com/bladecoder/ink/runtime/VariableReference.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/VariableReference.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/VariableReference.java diff --git a/src/main/java/com/bladecoder/ink/runtime/VariablesState.java b/runtime/src/main/java/com/bladecoder/ink/runtime/VariablesState.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/VariablesState.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/VariablesState.java diff --git a/src/main/java/com/bladecoder/ink/runtime/Void.java b/runtime/src/main/java/com/bladecoder/ink/runtime/Void.java similarity index 100% rename from src/main/java/com/bladecoder/ink/runtime/Void.java rename to runtime/src/main/java/com/bladecoder/ink/runtime/Void.java diff --git a/settings.gradle b/settings.gradle index 66ac0cf..29cf2d8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,11 +9,8 @@ * in the user guide at https://docs.gradle.org/2.13/userguide/multi_project_builds.html */ -/* -// To declare projects as part of a multi-project build use the 'include' method -include 'shared' -include 'api' -include 'services:webservice' -*/ +rootProject.name = 'blade-ink-java' -rootProject.name = 'blade-ink' +include 'runtime' +include 'compiler' +include 'inklecate' diff --git a/src/main/java/com/bladecoder/ink.gwt.xml b/src/main/java/com/bladecoder/ink.gwt.xml deleted file mode 100644 index b82209a..0000000 --- a/src/main/java/com/bladecoder/ink.gwt.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file