Repository navigation
Syntax Completion
GuideNH's page editor completes MDX tags, their attributes, attribute values, markdown syntax, code fence names and frontmatter keys. None of that is hardcoded: everything the editor offers comes from a registry that your mod can add to.
There are two ways in, and you normally want both.
The editor lists the tag names of every registered TagCompiler and SceneElementTagCompiler. If your
mod already registers a tag compiler, its tags are offered in the editor with no extra work:
A TagCompiler can be registered for every guide through
GuideNhIntegrationRegistry.registerTagCompilerProvider(...), and a SceneElementTagCompiler through
registerSceneElementTagCompilerProvider(...); both can also be added to one guide with
GuideBuilder.extension(...). A provider is read when a guide is built, so register it while your mod is
loading rather than while someone is reading.
public class MyModMachineTagCompiler implements TagCompiler {
@Override
public Set<String> getTagNames() {
return Set.of("MyMachine");
}
@Override
public void compileBlockContext(PageCompiler compiler, LytBlockContainer parent, MdxJsxFlowElement el) {
// ...
}
}Register it the same way you already do:
GuideNhIntegrationRegistry.global().registerTagCompilerProvider(compilers -> compilers.add(new MyModMachineTagCompiler()));or for a single guide:
Guide.builder(id).extension(TagCompiler.EXTENSION_POINT, new MyModMachineTagCompiler()).build();Tags a compiler accepts but page authors never write - parser output such as p, h1 or em - are not
offered. The library hides its own with SyntaxSink.hiddenTags(...).
A tag compiler does not say which attributes it reads or what those attributes take. That is what a
SyntaxContributor is for. It also declares markdown snippets, code fence names and frontmatter keys.
public class MyModSyntaxContributor implements SyntaxContributor {
@Override
public String namespace() {
return "mymod";
}
@Override
public void contribute(SyntaxSink sink) {
// Attributes. The value kind decides which value source answers completion.
sink.attributes(
"MyMachine",
AttributeSyntax.of("id", SyntaxValueKind.STRING),
AttributeSyntax.of("machine", SyntaxValueKind.of("MYMOD_MACHINE")), // your own kind
AttributeSyntax.of("tier", SyntaxValueKind.INT),
AttributeSyntax.of("formed", SyntaxValueKind.BOOLEAN),
AttributeSyntax.of("mode", SyntaxValueKind.ENUM, "input", "output", "both"));
// Tags that wrap content are completed as <Name></Name> with the caret inside.
sink.containerTags("MyPanel");
sink.children("MyPanel", "MyMachine", "Tooltip");
// Values resolved from your own registry at completion time.
sink.valueSource(new MachineIdValueSource());
// Extra markdown and fence names.
sink.markdown(MarkdownSnippet.block("::", "Mod note", ":: note\n", 3));
sink.fenceLanguages("mymod-diagram");
// Frontmatter keys, with fixed values or a value kind.
sink.frontmatterKeys("mymod_machine");
sink.frontmatterKind("mymod_machine", SyntaxValueKind.of("MYMOD_MACHINE"));
}
}Register it globally, or per guide:
GuideNhIntegrationRegistry.global().registerSyntaxContributor(new MyModSyntaxContributor());
Guide.builder(id).extension(SyntaxContributor.EXTENSION_POINT, new MyModSyntaxContributor()).build();Globally registered contributors are applied first, so a guide's own contributor can override them.
The built-in syntax that ships with the mod is an ordinary contributor (BuiltinSyntaxContributor)
registered as a default extension. GuideBuilder.disableDefaultExtensions() therefore turns the whole
built-in syntax off along with the default tag compilers.
An attribute's SyntaxValueKind decides how the editor completes and quotes its value:
| Kind | Completion | Written as |
|---|---|---|
STRING |
free text (or the values declared next to the attribute) | "..." |
INT, FLOAT
|
numeric presets for well known attribute names | bare |
BOOLEAN |
true, false
|
bare |
ENUM |
the constants of the enum passed to AttributeSyntax.of, or the declared values |
"..." |
COLOR |
#rrggbb plus the symbolic colour names |
"..." |
ITEM_ID, BLOCK_ID
|
item and block registry names, with icons | "..." |
ORE_DICT, ENTITY_ID, KEY_BIND, COMMAND
|
the matching game registry | "..." |
PAGE_PATH, FILE_PATH
|
guide page ids, and files in the guide assets | "..." |
MOD_ID |
mod ids taken from the item registry namespaces | "..." |
EXPRESSION, DOMAIN, FORMAT_PATTERN
|
common expressions, intervals and patterns | "..." |
VECTOR3 |
free text | bare |
SNBT |
free text | {...} |
QUEST_UUID |
free text; quest ids are not discoverable yet | "..." |
Reusing a built-in kind means you do not register anything: marking an attribute as ITEM_ID is enough
to get item completion for it.
To complete from your own registry, declare a kind and a source. A kind is just a name, so you can declare it where you like:
public final class MyModValueKinds {
public static final SyntaxValueKind MACHINE = SyntaxValueKind.of("MYMOD_MACHINE");
}public class MachineIdValueSource implements SyntaxValueSource {
@Override
public Set<SyntaxValueKind> kinds() {
return Set.of(MyModValueKinds.MACHINE);
}
@Override
public List<SyntaxSuggestion> suggest(SyntaxValueRequest request, int limit) {
List<SyntaxSuggestion> results = new ArrayList<>();
for (MyMachine machine : MyMachineRegistry.all()) {
if (results.size() >= limit) {
break;
}
if (machine.id()
.startsWith(request.partialText())) {
results.add(SyntaxSuggestion.withValue(machine.id(), machine.id(), machine.displayName()));
}
}
return results;
}
}request carries the typed text, the enclosing tag and attribute, and the frontmatter key, so one
source can serve several attributes.
A source that needs data from the open guide or from the document implements
SyntaxEnvironmentAware. prepare is called once per completion tick, before any request is answered:
public class MyMachineValueSource implements SyntaxValueSource, SyntaxEnvironmentAware {
@Override
public void prepare(SyntaxEnvironment environment) {
// environment.guide(), environment.pagePaths(), environment.documentText()
}
}Keep prepare cheap: it runs on every editor tick. Cache derived lists and only rebuild them when the
input actually changed - that is what the built-in PagePathValueSource and AnchorValueSource do.
Completing a tag name writes <Name />, or a paired tag with the caret inside the container. A tag that
needs attributes or content to be useful declares the text it should complete as instead:
sink.insertTemplates(
InsertTemplate.caretAfter("MyMachine", "<MyMachine id=\"\" tier=\"1\" />", "id=\""),
InsertTemplate.caretAfter("MyBlock", "<MyBlock id=\"\">\n \n</MyBlock>", "\n "));caretAfter puts the caret right after the first occurrence of the marker it is given, so a template can
say where typing continues without counting characters. InsertTemplate.of(tag, text) writes the text
with the caret at its end. The template replaces the typed tag name, so the author accepts it and carries
on inside the form that was written.
The mod itself declares templates for the tags whose useful form is more than the tag name, for example
<BlockStat item="" count="1" />, <InputAnnotation pos="0.5 1.5 0.5" inputType="lmb" /> and a chart
with its first series.
Everything above describes syntax this mod already parses. For syntax of your own - a directive inside a
block, a small language in a code fence, a field you compile yourself - register a SyntaxSlot:
public class MyModSlot implements SyntaxSlot {
@Override
public String namespace() {
return "mymod";
}
@Override
public SyntaxSlotMatch match(String text, int cursorIndex, GuideSyntaxModel model) {
// Own the value of one attribute of your own tag, while its quote is still open.
String attribute = "ports=\"";
int attributeStart = text.lastIndexOf(attribute, cursorIndex);
if (attributeStart < 0) {
return null;
}
int from = attributeStart + attribute.length();
int closingQuote = text.indexOf('"', from);
if (closingQuote >= 0 && cursorIndex > closingQuote) {
return null;
}
return new SyntaxSlotMatch(
from,
cursorIndex,
text.substring(from, cursorIndex),
MyPortRegistry.suggestions(),
suggestion -> SyntaxReplacement.cursorAtEnd(suggestion.value()));
}
}Register it globally or per guide, exactly like a contributor:
GuideNhIntegrationRegistry.global().registerSyntaxSlot(new MyModSlot());
Guide.builder(id).extension(SyntaxSlot.EXTENSION_POINT, new MyModSlot()).build();What a slot controls:
-
matchdecides the range an accepted value replaces, the text already typed inside it and the values the editor offers. Returnnullwhen the caret is not inside your syntax. The text you report as typed has to be what the document really holds in that range: the editor re-checks it before writing, and a normalized form of it would drop the commit. - The
SyntaxValueWriterof the match writes an accepted value: the text that replaces the range, where the caret lands and what is selected. Leaving itnullreplaces the range with the value itself. A writer that throws or answers nothing is reported in the log and the editor writes nothing. -
selectionoptionally declares the range a double click inside your syntax selects.
Slots are asked before the editor's own resolvers, and the first slot that matches owns the caret even when it offers no values, so this mod never guesses inside text another mod claims.
- MDX tags, scoped to the enclosing container, with container tags completing as
<Name></Name>. - Attribute names, which expand to
name="",name={}orname={true}according to the value kind. - Attribute values, routed by value kind, including recipes, item and block ids, and mod ids.
- Markdown syntax: headings, lists, task lists, quotes, alerts, tables, code blocks, thematic breaks, math blocks, and inline emphasis, code, links and images. Each snippet places the caret where you continue typing.
- Code fence names, derived from the code block language registry plus the GuideNH specific fences.
- YAML frontmatter keys, and values for the keys that declare a value kind.
- Insert templates, so a tag whose useful form is more than its name is written complete.
Candidates are ranked so the popup pre-selects the closest match: an exact prefix first, then a match on the identifier after the namespace, then a match anywhere in the name.
Everything on this page is a registry, and a registered plugin is only read when a guide is built or a page compiled, so register during mod loading.
| You want to | Use |
|---|---|
| Add a tag, a scene element, an attribute, a value kind and its values |
TagCompiler, SceneElementTagCompiler, SyntaxContributor, SyntaxValueSource
|
| Complete syntax of your own (your own slot, your own writer, double-click range) | SyntaxSlot |
| Make a tag complete as a whole form |
sink.insertTemplates(...) (also offered in the editor's insert menu) |
| Add markdown snippets, fence names, frontmatter keys and values | SyntaxContributor |
| Decide what the body of your own fence means |
CodeFenceRenderer (declare the name with sink.fenceLanguages(...); override renderSiteFence so the site shows it too) |
| Add a symbolic colour name |
SymbolicColorResolver (per guide) or registerSymbolicColorResolver
|
| Add a page index |
GuideBuilder.index(...) / GuideBuilderIntegrationHook
|
| Export your tags to the site |
GuideSiteTagRenderer (see Mod Compatibility) |
| Add a scene editor toolbar button or menu item |
SceneEditorToolbarRegistry / SceneEditorMenuRegistry
|
| Add a resource to the exported site |
ExportableResourceProvider on your node |
| Report a problem with your own syntax to the reader |
LytErrorSink.appendError(compiler, text, element), which is the parent your tag compiler is given |
| Add a button or menu entry to the guide editor |
GuideNhIntegrationRegistry.registerEditorAction(GuideEditorActionContribution.insert(...)) or .wrap(...), or GuideEditorActionContribution.Provider for one guide |
Deliberately closed, with what it costs you:
- The guide editor's built-in toolbar is a fixed set. A contributed action (see
GuideEditorActionContribution) gets its own toolbar button and menu entry after the built-ins, so what cannot be added is a change to a built-in action, not a new one. - Diagnostics are reported at compile time through
LytErrorSink: the error block is appended to the document, so the reader sees it in the book and on the exported site. There is no separate "warn while typing" hook in the editor. - The site export renders its own tags with a built-in chain. A registered renderer is asked first, for every MDX element and for your fence names, so your tags and fences export; the built-in chain itself is not a set of registered renderers yet.
- Scene annotations of your own render in the book but are not serialized into the exported site's viewer.
- A scene element of your own, registered as a
SceneElementTagCompiler, renders in the book and reaches the site only if its element is aSceneAnnotation: that is the one shape the exporter collects. An element of any other shape, such as one that draws geometry of its own, is left out of the export while the rest of the scene is exported around it. Exporting such an element needs an annotation that carries it, or a site renderer for the tag that produces the markup.
Conventions that matter for a plugin:
- Give your
SyntaxValueKinda mod-namespaced id ("MYMOD_MACHINE"). Kinds route to value sources by that id alone; the quoting hint belongs to the attribute that declares the value. - Return what the document really holds from
SyntaxSlot.match: the editor re-checks the text before it writes, and a normalized form drops the commit. - A plugin that throws is reported with its namespace and skipped, so a failure costs your own contributions rather than the editor.