diff --git a/Syntax.fs b/Syntax.fs index 1a2d4d8..e9bb1d4 100644 --- a/Syntax.fs +++ b/Syntax.fs @@ -1,46 +1,145 @@ +/// AST definition for VijiN declarative markup. +/// +/// VijiN scene files are inspired by stage play manuscripts. +/// Most of a scene is "spoken" text, written as a speaker name followed by a colon. +/// Directives in square brackets indicate how the content should be presented, +/// rather than defining the content itself. +/// +/// The syntax includes some formatting indicators. +/// Text between forward slashes is emphasized, with the expectation that it +/// will be rendered in italics. +/// Double asterisks indicate yelled text, to be rendered in bold or large font. +/// Underscores mark whispered text. +/// +/// Almost all syntactic constraints can be escaped by quoting text in backticks. +/// The exception is that backtick-quoted text cannot contain backticks or newlines. module VijiN.Syntax +/// Name of a variable to expand. +/// +/// A variable name may contain any text that doesn't have other syntactic meaning, +/// including whitespace. It may be quoted in backticks. type Variable = Variable of string +/// A single term of text, as an element of a spoken line, directive argument, +/// character name, &c. type Term = TextTerm of string | VarTerm of Variable -type Character = Character of Term -// TODO: Character should probably have more, like selecting portrait outfit/style - type DirectivePrefix = string type DirectiveInfix = string -type DirectiveOperator = DirectivePrefix * DirectiveInfix +type DirectiveSuffix = string +/// Fixed, syntactic components of a directive. May be interspersed with arguments. +/// Directives in a directive line may be separated with either dots or commas; +/// in the former case, the variant is DirectiveOperator, and in the latter +/// case, the variant is ContinueDirective with the implication that the prefix +/// is the same as that of the previous operator in the block. +type DirectiveOperator = + | DirectiveOperator of DirectivePrefix * DirectiveInfix option * DirectiveSuffix option + | ContinueDirective of DirectiveInfix option * DirectiveSuffix option +/// A single argument to a directive. type DirectiveArg = Arg of Term list -type Directive = DirectiveOperator * DirectiveArg list list // TODO: non-empty list -type Directives = Directive list // TODO: non-empty list - -type ChoiceText = ChoiceText of Term list -type ChoiceValue = ChoiceValue of Term list -type ChoiceOption = ChoiceText * ChoiceValue option -type Choice = Variable * ChoiceOption list // TODO: non-empty list +type DirectivePrefixArg = DirectiveArg +type DirectiveInfixArg = DirectiveArg +type DirectiveSuffixArg = DirectiveArg +type DirectiveListArg = DirectiveArg +/// A directive instance, the composition of an operator with any arguments. +/// An argument may be absent even if a later argument is provided. +type Directive = DirectiveOperator * DirectivePrefixArg * DirectiveInfixArg * DirectiveSuffixArg +/// A directive block. Either a single directive with a possibly empty list of +/// arguments, or a list of directives +type DirectiveBlock = + | SingleDirective of Directive * DirectiveListArg list + | DirectiveList of Directive list +/// The speaker of a spoken line. +/// The narrator is syntactically indicated by an empty name, i.e. a line +/// beginning with a colon. Narrated lines may also be given as lines spoken +/// by "Narrator" when there is no defined character named the same, but that +/// is resolved semantically rather than syntactically. +type Speaker = + | Narrator + | Speaker of Term list +/// An inline directive appearing between a speaker name and their line. type InlineDirective = Directive +/// The text of a spoken line. type SpokenText = + /// Plain spoken or narrated text. | Plain of Term list + /// Emphasized text written between slashes. Intended to be rendered in italics. | Emphasis of Term list + /// Yelled text written between double asterisks. Intended to be rendered + /// in bold or large text. | Yell of Term list + /// Whispered text written between underscores. Intended to be rendered + /// in small text. | Whisper of Term list -type Spoken = Character option * InlineDirective list * SpokenText list // TODO: non-empty list +/// A spoken line. The character is absent for narrated lines. +type Spoken = Speaker * InlineDirective list * SpokenText list // TODO: non-empty list +/// A comment. Text between both square and round brackets, without nesting. type Comment = Comment of string +/// A line of script. type Line = | SpokenLine of Spoken - | DirectiveLine of Directives + | DirectiveLine of DirectiveBlock | CommentLine of Comment - | ChoiceLine of Choice -type LineNumber = LineNo of int // TODO: >= 1 -type PageNumber = PageNo of int // TODO: >= 1 +type LineNumber = LineNo of int +type PageNumber = PageNo of int +/// A complete line of script along with metadata. type ScriptLine = LineNumber * PageNumber * Line // TODO: other diffing metadata -type SceneName = SceneName of string +/// An entire parsed scene. +type Scene = ScriptLine list -type Scene = SceneName * Character list * ScriptLine list + +module private Parse = + open FParsec + + type UserState = { + /// The list of all available parsers. + /// Never changes within a parsing unit. + directiveParsers: seq * Parser * Parser> + } + type Parser<'t> = Parser<'t, UserState> + + let ws = unicodeSpaces + + /// Unquoted terms may contain most characters, including whitespace, but + /// not those characters used elsewhere in the syntax: square brackets, + /// curly brackets, stops, commas, or colons. + let unquotedTerm: Parser<_> = noneOf "[]{}.,:`" |> manyChars + + /// Quoted terms can be used to escape from syntactic requirements almost + /// anywhere, including inside variable names, character names, directive + /// arguments, &c. + /// + /// A quoted term is expressed as arbitrary text between backticks (`). + /// They cannot themselves contain backticks or newlines. + let quotedTerm: Parser<_> = noneOf "`\n" |> manyChars |> between (pchar '`') (pchar '`') + + /// Text is the concatenation of quoted and unquoted terms. + let text: Parser<_> = quotedTerm <|> unquotedTerm |> manyStrings + + /// A variable use is text between curly brackets. + let variable: Parser<_> = text |> between (pchar '{') (pchar '}') |>> Variable + + /// A term is either a variable or text, possibly quoted. + let term: Parser<_> = (variable |>> VarTerm) <|> (text |>> TextTerm) + + /// A dot-ended directive contains a fixed-prefix directive instance and + /// zero or more continued directives separated by commas. + let dotDirective prefix infix suffix: Parser = + // TODO: continued directives + pipe4 + (prefix .>> ws) + (((preturn [] .>>. infix) <|> (many term .>>. infix)) .>> ws) + (((preturn [] .>>. suffix) <|> (many term .>>. suffix)) .>> ws) + (many term .>> ws .>> skipChar '.' .>> ws) + (fun pfx (parg, ifx) (iarg, sfx) sarg -> (DirectiveOperator (pfx, ifx, sfx), Arg parg, Arg iarg, Arg sarg)) + + /// Parse the speaker of a spoken line, through the colon. + let speaker = ws >>. ((skipChar ':' >>% Narrator) <|> (many1 term |>> Speaker .>> ws .>> skipChar ':')) "speaker" diff --git a/vijin.fsproj b/vijin.fsproj index 4fc971b..b639256 100644 --- a/vijin.fsproj +++ b/vijin.fsproj @@ -7,4 +7,7 @@ + + + \ No newline at end of file