mirror of https://github.com/status-im/NimYAML.git
1091 lines
38 KiB
Nim
1091 lines
38 KiB
Nim
# NimYAML - YAML implementation in Nim
|
|
# (c) Copyright 2015-2023 Felix Krause
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
|
|
## =====================
|
|
## Module yaml/presenter
|
|
## =====================
|
|
##
|
|
## This is the presenter API, used for generating YAML character streams.
|
|
|
|
import std / [streams, deques, strutils, options]
|
|
import data, taglib, stream, private/internal, hints, parser
|
|
|
|
type
|
|
|
|
ContainerStyle* = enum
|
|
## How to serialize containers nodes.
|
|
##
|
|
## - ``cBlock`` writes all container nodes in block style,
|
|
## i.e. indentation-based.
|
|
## - ``cFlow`` writes all container nodes in flow style,
|
|
## i.e. JSON-like.
|
|
## - ``cMixed`` writes container nodes that only contain alias nodes
|
|
## and short scalar nodes in flow style, all other container nodes
|
|
## in block style.
|
|
cBlock, cFlow, cMixed
|
|
|
|
NewLineStyle* = enum
|
|
## What kind of newline sequence is used when presenting.
|
|
##
|
|
## - ``nlLF``: Use a single linefeed char as newline.
|
|
## - ``nlCRLF``: Use a sequence of carriage return and linefeed as
|
|
## newline.
|
|
## - ``nlOSDefault``: Use the target operation system's default newline
|
|
## sequence (CRLF on Windows, LF everywhere else).
|
|
## - ``nlNone``: Don't use newlines, write everything in one line.
|
|
## forces ContainerStyle cFlow.
|
|
nlLF, nlCRLF, nlOSDefault, nlNone
|
|
|
|
OutputYamlVersion* = enum
|
|
## Specify which YAML version number the presenter shall emit. The
|
|
## presenter will always emit content that is valid YAML 1.1, but by
|
|
## default will write a directive ``%YAML 1.2``. For compatibility with
|
|
## other YAML implementations, it is possible to change this here.
|
|
##
|
|
## It is also possible to specify that the presenter shall not emit any
|
|
## YAML version. The generated content is then guaranteed to be valid
|
|
## YAML 1.1 and 1.2 (but not 1.0 or any newer YAML version).
|
|
ov1_2, ov1_1, ovNone
|
|
|
|
ScalarQuotingStyle* = enum
|
|
## Specifies whether scalars should forcibly be double-quoted.
|
|
## - ``sqUnset``: Quote where necessary
|
|
## - ``sqDouble``: Force double-quoted style for every scalar
|
|
## - ``sqJson``: Force JSON-compatible double-quoted style for every scalar
|
|
## except for scalars of other JSON types (bool, int, double)
|
|
sqUnset, sqDouble, sqJson
|
|
|
|
DirectivesEndStyle* = enum
|
|
## Whether to write a directives end marker '---'
|
|
## - ``deAlways``: Always write it.
|
|
## - ``deIfNecessary``: Write it if any directive has been written,
|
|
## or if the root node has an explicit tag
|
|
## - ``deNever``: Don't write it. Suppresses output of directives
|
|
deAlways, deIfNecessary, deNever
|
|
|
|
PresentationOptions* = object
|
|
## Options for generating a YAML character stream
|
|
containers* : ContainerStyle = cMixed ## how mappings and sequences are presented
|
|
indentationStep*: int = 2 ## how many spaces a new level should be indented
|
|
newlines* : NewLineStyle = nlOSDefault ## kind of newline sequence to use
|
|
outputVersion* : OutputYamlVersion = ovNone ## whether to write the %YAML tag
|
|
maxLineLength* : Option[int] = some(80) ## max length of a line, including indentation
|
|
directivesEnd* : DirectivesEndStyle = deIfNecessary ## whether to write '---' after tags
|
|
suppressAttrs* : bool = false ## whether to suppress all attributes on nodes
|
|
quoting* : ScalarQuotingStyle = sqUnset ## how scalars are quoted
|
|
condenseFlow* : bool = true ## whether non-nested flow containers use a single line
|
|
explicitKeys* : bool = false ## whether mapping keys should always use '?'
|
|
|
|
YamlPresenterJsonError* = object of ValueError
|
|
## Exception that may be raised by the YAML presenter when it is
|
|
## instructed to output JSON, but is unable to do so. This may occur if:
|
|
##
|
|
## - The given `YamlStream <#YamlStream>`_ contains a map which has any
|
|
## non-scalar type as key.
|
|
## - Any float scalar bears a ``NaN`` or positive/negative infinity value
|
|
|
|
YamlPresenterOutputError* = object of ValueError
|
|
## Exception that may be raised by the YAML presenter. This occurs if
|
|
## writing character data to the output stream raises any exception.
|
|
## The error that has occurred is available from ``parent``.
|
|
|
|
DumperState = enum
|
|
dBlockExplicitMapKey, dBlockImplicitMapKey, dBlockMapValue,
|
|
dBlockInlineMap, dBlockSequenceItem, dFlowImplicitMapKey, dFlowMapValue,
|
|
dFlowExplicitMapKey, dFlowSequenceItem, dFlowMapStart, dFlowSequenceStart
|
|
|
|
DumperLevel = tuple
|
|
state: DumperState
|
|
indentation: int
|
|
singleLine: bool
|
|
wroteAnything: bool
|
|
|
|
Context = object
|
|
target: Stream
|
|
options: PresentationOptions
|
|
handles: seq[tuple[handle, uriPrefix: string]]
|
|
levels: seq[DumperLevel]
|
|
needsWhitespace: int
|
|
wroteDirectivesEnd: bool
|
|
lastImplicitKeyLen: int
|
|
cached: DeQue[Event]
|
|
|
|
ItemKind = enum
|
|
ikCompactScalar, ikMultilineFlowScalar, ikBlockScalar, ikCollection
|
|
|
|
MapParseState = enum
|
|
mpInitial, mpKey, mpValue, mpNeedBlock
|
|
|
|
proc level(ctx: var Context): var DumperLevel = ctx.levels[^1]
|
|
|
|
proc level(ctx: Context): DumperLevel = ctx.levels[^1]
|
|
|
|
proc state(ctx: Context): DumperState = ctx.level.state
|
|
|
|
proc `state=`(ctx: var Context, v: DumperState) =
|
|
ctx.level.state = v
|
|
|
|
proc isFlow(state: DumperState): bool =
|
|
result = state in [
|
|
dFlowImplicitMapKey, dFlowMapValue,
|
|
dFlowExplicitMapKey, dFlowSequenceItem,
|
|
dFlowMapStart, dFlowSequenceStart
|
|
]
|
|
|
|
proc indentation(ctx: Context): int =
|
|
result = if ctx.levels.len == 0: 0 else: ctx.level.indentation
|
|
|
|
proc searchHandle(ctx: Context, tag: string):
|
|
tuple[handle: string, len: int] {.raises: [].} =
|
|
## search in the registered tag handles for one whose prefix matches the start
|
|
## of the given tag. If multiple registered handles match, the one with the
|
|
## longest prefix is returned. If no registered handle matches, ("", 0) is
|
|
## returned.
|
|
result.len = 0
|
|
for item in ctx.handles:
|
|
if item.uriPrefix.len > result.len:
|
|
if tag.startsWith(item.uriPrefix):
|
|
result.len = item.uriPrefix.len
|
|
result.handle = item.handle
|
|
|
|
proc inspect(
|
|
scalar : string,
|
|
indentation : int,
|
|
words, lines: var seq[tuple[start, finish: int]],
|
|
multiLine : var bool,
|
|
lineLength : Option[int],
|
|
inFlow : bool,
|
|
proposed : ScalarStyle,
|
|
): ScalarStyle {.raises: [].} =
|
|
## inspects the given scalar and returns the style in which it should be
|
|
## presented. fills information in words, lines and multiLine that can be
|
|
## used later for presenting it:
|
|
##
|
|
## * words will contain substring boundaries of parts of the string that
|
|
## are separated by exactly one, not more, spaces. occurrences of multiple
|
|
## spaces will become part of a single word. This is used for folded block
|
|
## scalars, which can only break at single spaces.
|
|
## * lines will contain substring boundaries of parts of the string that
|
|
## can be emitted as lines within a literal block scalar.
|
|
## * multiLine is set to true for double quoted scalars iff the scalar will
|
|
## be written on multiple lines. important when using the scalar as block
|
|
## mapping key. Will not be set for other styles since single quoted and
|
|
## plain scalars always occupy only one line, and block scalars always
|
|
## occupy multiple.
|
|
##
|
|
## The proposed style will take precedence over other decisions and will be
|
|
## returned if possible. a proposed folded style will decay into a literal
|
|
## style if folded style is not possible but literal is. Otherwise, if the
|
|
## proposed style is not a valid style, it is ignored.
|
|
##
|
|
## A proposed style of ssAny is not valid and is therefore always ignored.
|
|
## This proc will never emit ssAny.
|
|
##
|
|
## This inspector will not always allow a style that would be possible.
|
|
## For example, the presenter is currently unable to emit multi-line plain
|
|
## scalars, therefore multi-line string will never yield ssPlain. Similarly,
|
|
## ssFolded will never be returned if there are more-indented lines.
|
|
lines.setLen(0)
|
|
words.setLen(0)
|
|
var
|
|
inLine = false
|
|
inWord = false
|
|
multipleSpaces = true
|
|
curWord, curLine: tuple[start, finish: int]
|
|
canUseFolded = not inFlow
|
|
canUseLiteral = not inFlow
|
|
canUsePlain = scalar.len > 0 and
|
|
scalar[0] notin {'@', '`', '|', '>', '&', '*', '!', ' ', '\t'} and
|
|
(not lineLength.isSome or scalar.len <= indentation + lineLength.get())
|
|
canUseSingleQuoted = true
|
|
curDqLen = indentation + 2
|
|
for i, c in scalar:
|
|
case c
|
|
of ' ':
|
|
if inWord:
|
|
if not multipleSpaces:
|
|
curWord.finish = i - 1
|
|
inWord = false
|
|
else:
|
|
multipleSpaces = true
|
|
inWord = true
|
|
if not inLine:
|
|
inLine = true
|
|
curLine.start = i
|
|
# space at beginning of line will preserve previous and next
|
|
# line break. that is currently too complex to handle.
|
|
canUseFolded = false
|
|
inc(curDqLen)
|
|
of '\l':
|
|
canUsePlain = false # we don't use multiline plain scalars
|
|
canUseSingleQuoted = false
|
|
if inWord:
|
|
curWord.finish = i - 1
|
|
if lineLength.isSome and
|
|
curWord.finish - curWord.start + 1 > lineLength.get() - indentation:
|
|
multiLine = lineLength.isSome and curDqLen > lineLength.get()
|
|
return ssDoubleQuoted
|
|
words.add(curWord)
|
|
inWord = false
|
|
if inLine and scalar[i - 1] in ['\t', ' ']:
|
|
# cannot use block scalars if line ends with space
|
|
canUseLiteral = false
|
|
canUseFolded = false
|
|
curWord.start = i + 1
|
|
multipleSpaces = true
|
|
if not inLine: curLine.start = i
|
|
inLine = false
|
|
curLine.finish = i - 1
|
|
if lineLength.isSome and
|
|
curLine.finish - curLine.start + 1 > lineLength.get() - indentation:
|
|
canUseLiteral = false
|
|
lines.add(curLine)
|
|
inc(curDqLen, 2)
|
|
else:
|
|
inc(curDqLen, if c in {'"', '\\', '\t', '\''}: 2 else: 1)
|
|
|
|
if c in {'{', '}', '[', ']', ',', '#', '-', ':', '?', '%', '"', '\''}:
|
|
canUsePlain = false
|
|
elif c.ord < 32:
|
|
canUsePlain = false
|
|
canUseSingleQuoted = false
|
|
if not inLine:
|
|
curLine.start = i
|
|
inLine = true
|
|
if not inWord:
|
|
if not multipleSpaces:
|
|
if lineLength.isSome and
|
|
curWord.finish - curWord.start + 1 > lineLength.get() - indentation:
|
|
multiLine = lineLength.isSome and curDqLen > lineLength.get()
|
|
return ssDoubleQuoted
|
|
words.add(curWord)
|
|
curWord.start = i
|
|
inWord = true
|
|
multipleSpaces = false
|
|
if inWord:
|
|
curWord.finish = scalar.len - 1
|
|
if lineLength.isSome and
|
|
curWord.finish - curWord.start + 1 > lineLength.get() - indentation:
|
|
multiLine = lineLength.isSome and curDqLen > lineLength.get()
|
|
return ssDoubleQuoted
|
|
words.add(curWord)
|
|
if inLine:
|
|
if scalar[^1] in ['\t', ' ']:
|
|
canUseLiteral = false
|
|
canUseFolded = false
|
|
canUsePlain = false
|
|
curLine.finish = scalar.len - 1
|
|
if lineLength.isSome and
|
|
curLine.finish - curLine.start + 1 > lineLength.get() - indentation:
|
|
canUseLiteral = false
|
|
lines.add(curLine)
|
|
if lineLength.isSome and curDqLen > lineLength.get():
|
|
canUseSingleQuoted = false
|
|
|
|
case proposed
|
|
of ssLiteral:
|
|
if canUseLiteral: return ssLiteral
|
|
of ssFolded:
|
|
if canUseFolded: return ssFolded
|
|
elif canUseLiteral: return ssLiteral
|
|
of ssPlain:
|
|
if canUsePlain: return ssPlain
|
|
of ssSingleQuoted:
|
|
if canUseSingleQuoted: return ssSingleQuoted
|
|
of ssDoubleQuoted:
|
|
multiLine = lineLength.isSome and curDqLen > lineLength.get()
|
|
return ssDoubleQuoted
|
|
else: discard
|
|
|
|
if lineLength.isNone or scalar.len <= lineLength.get() - indentation:
|
|
result = if canUsePlain: ssPlain else: ssDoubleQuoted
|
|
elif canUseLiteral: result = ssLiteral
|
|
elif canUseFolded: result = ssFolded
|
|
elif canUsePlain: result = ssPlain
|
|
else: result = ssDoubleQuoted
|
|
if result == ssDoubleQuoted:
|
|
multiLine = lineLength.isSome and curDqLen > lineLength.get()
|
|
|
|
proc append(ctx: var Context, val: string | char) {.inline.} =
|
|
if ctx.needsWhitespace > 0:
|
|
ctx.target.write(repeat(' ', ctx.needsWhitespace))
|
|
ctx.needsWhitespace = 0
|
|
ctx.target.write(val)
|
|
if ctx.levels.len > 0: ctx.level.wroteAnything = true
|
|
|
|
proc whitespace(ctx: var Context, single: bool = false) {.inline.} =
|
|
if single or ctx.options.indentationStep == 1: ctx.needsWhitespace = 1
|
|
else: ctx.needsWhitespace = ctx.options.indentationStep - 1
|
|
|
|
proc newline(ctx: var Context) {.inline.} =
|
|
case ctx.options.newlines
|
|
of nlCRLF: ctx.target.write("\c\l")
|
|
of nlLF: ctx.target.write("\l")
|
|
else: ctx.target.write("\n")
|
|
ctx.needsWhitespace = 0
|
|
if ctx.levels.len > 0: ctx.level.wroteAnything = true
|
|
|
|
proc writeDoubleQuoted(
|
|
ctx : var Context,
|
|
scalar: string,
|
|
): int {.raises: [YamlPresenterOutputError].} =
|
|
let indentation = ctx.indentation + ctx.options.indentationStep
|
|
var curPos = indentation
|
|
let t = ctx.target
|
|
try:
|
|
result = 2
|
|
ctx.append('"')
|
|
curPos.inc()
|
|
for i, c in scalar:
|
|
var nextLength = 1
|
|
case c
|
|
of '"', '\l', '\t', '\\':
|
|
nextLength = 2
|
|
else:
|
|
if ord(c) < 32:
|
|
nextLength = 4
|
|
|
|
if ctx.options.maxLineLength.isSome and
|
|
(curPos + nextLength >= ctx.options.maxLineLength.get() or
|
|
curPos + nextLength == ctx.options.maxLineLength.get() - 1 and i == scalar.len - 2):
|
|
t.write('\\')
|
|
ctx.newline()
|
|
t.write(repeat(' ', indentation))
|
|
result.inc(2 + indentation)
|
|
curPos = indentation
|
|
if c == ' ':
|
|
t.write('\\')
|
|
curPos.inc()
|
|
result.inc()
|
|
else:
|
|
curPos.inc(nextLength)
|
|
result.inc(nextLength)
|
|
case c
|
|
of '"': t.write("\\\"")
|
|
of '\l': t.write("\\n")
|
|
of '\t': t.write("\\t")
|
|
of '\\': t.write("\\\\")
|
|
else:
|
|
if ord(c) < 32: t.write("\\x" & toHex(ord(c), 2))
|
|
else: t.write(c)
|
|
t.write('"')
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
proc writeDoubleQuotedJson(
|
|
ctx : var Context,
|
|
scalar: string,
|
|
): int {.raises: [YamlPresenterOutputError].} =
|
|
let t = ctx.target
|
|
try:
|
|
ctx.append('"')
|
|
result = 2
|
|
for c in scalar:
|
|
case c
|
|
of '"':
|
|
t.write("\\\"")
|
|
result.inc(2)
|
|
of '\\':
|
|
t.write("\\\\")
|
|
result.inc(2)
|
|
of '\l':
|
|
t.write("\\n")
|
|
result.inc(2)
|
|
of '\t':
|
|
t.write("\\t")
|
|
result.inc(2)
|
|
of '\f':
|
|
t.write("\\f")
|
|
result.inc(2)
|
|
of '\b':
|
|
t.write("\\b")
|
|
result.inc(2)
|
|
else:
|
|
if ord(c) < 32:
|
|
t.write("\\u" & toHex(ord(c), 4))
|
|
result.inc(4)
|
|
else:
|
|
t.write(c)
|
|
result.inc()
|
|
t.write('"')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeSingleQuoted(
|
|
ctx : var Context,
|
|
scalar: string,
|
|
): int {.raises: [YamlPresenterOutputError].} =
|
|
let t = ctx.target
|
|
try:
|
|
# writing \39 instead of \' because my syntax highlighter is dumb
|
|
ctx.append('\39')
|
|
result = 2
|
|
for c in scalar:
|
|
if c == '\39':
|
|
t.write("''")
|
|
result.inc(2)
|
|
else:
|
|
t.write(c)
|
|
result.inc()
|
|
ctx.append('\39')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeLiteral(
|
|
ctx : var Context,
|
|
scalar: string,
|
|
lines : seq[tuple[start, finish: int]],
|
|
) {.raises: [YamlPresenterOutputError].} =
|
|
var indentation = ctx.indentation
|
|
if ctx.levels.len > 0: inc(indentation, ctx.options.indentationStep)
|
|
let t = ctx.target
|
|
try:
|
|
ctx.append('|')
|
|
if scalar[^1] != '\l': t.write('-')
|
|
if scalar[0] in [' ', '\t']: t.write($ctx.options.indentationStep)
|
|
for line in lines:
|
|
ctx.newline()
|
|
t.write(repeat(' ', indentation))
|
|
if line.finish >= line.start:
|
|
t.write(scalar[line.start .. line.finish])
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
proc writeFolded(
|
|
ctx : var Context,
|
|
scalar: string,
|
|
words : seq[tuple[start, finish: int]],
|
|
) {.raises: [YamlPresenterOutputError].} =
|
|
let t = ctx.target
|
|
let indentation = ctx.indentation + ctx.options.indentationStep
|
|
let lineLength = (
|
|
if ctx.options.maxLineLength.isSome:
|
|
ctx.options.maxLineLength.get() else: 1024)
|
|
try:
|
|
ctx.append('>')
|
|
if scalar[^1] != '\l': t.write('-')
|
|
if scalar[0] in [' ', '\t']: t.write($ctx.options.indentationStep)
|
|
var curPos = lineLength
|
|
for word in words:
|
|
if word.start > 0 and scalar[word.start - 1] == '\l':
|
|
ctx.newline()
|
|
ctx.newline()
|
|
t.write(repeat(' ', indentation))
|
|
curPos = indentation
|
|
elif curPos + (word.finish - word.start + 1) > lineLength:
|
|
ctx.newline()
|
|
t.write(repeat(' ', indentation))
|
|
curPos = indentation
|
|
else:
|
|
t.write(' ')
|
|
curPos.inc()
|
|
t.write(scalar[word.start .. word.finish])
|
|
curPos += word.finish - word.start + 1
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
template safeWrite(ctx: var Context, s: string or char) =
|
|
try: ctx.append(s)
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
template safeNewline(c: var Context) =
|
|
try: ctx.newline()
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
proc startItem(
|
|
ctx : var Context,
|
|
kind: ItemKind,
|
|
) {.raises: [YamlPresenterOutputError].} =
|
|
if ctx.levels.len == 0:
|
|
if kind == ikBlockScalar:
|
|
if not ctx.wroteDirectivesEnd:
|
|
ctx.wroteDirectivesEnd = true
|
|
ctx.safeWrite("---")
|
|
ctx.whitespace(true)
|
|
return
|
|
let t = ctx.target
|
|
try:
|
|
case ctx.state
|
|
of dBlockMapValue:
|
|
if ctx.level.wroteAnything or ctx.options.indentationStep < 2:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
else:
|
|
ctx.level.wroteAnything = true
|
|
if kind != ikCompactScalar or ctx.options.explicitKeys:
|
|
ctx.append('?')
|
|
ctx.whitespace()
|
|
ctx.state = dBlockExplicitMapKey
|
|
else: ctx.state = dBlockImplicitMapKey
|
|
of dBlockInlineMap: ctx.state = dBlockImplicitMapKey
|
|
of dBlockExplicitMapKey:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
t.write(':')
|
|
ctx.whitespace()
|
|
ctx.state = dBlockMapValue
|
|
of dBlockImplicitMapKey:
|
|
ctx.append(':')
|
|
ctx.whitespace(true)
|
|
ctx.state = dBlockMapValue
|
|
of dFlowExplicitMapKey:
|
|
if ctx.options.newlines != nlNone:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
ctx.append(':')
|
|
ctx.whitespace()
|
|
ctx.state = dFlowMapValue
|
|
of dFlowMapValue:
|
|
ctx.append(',')
|
|
ctx.whitespace(true)
|
|
if not ctx.level.singleLine:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
if kind == ikCompactScalar and not ctx.options.explicitKeys:
|
|
ctx.state = dFlowImplicitMapKey
|
|
else:
|
|
t.write('?')
|
|
ctx.whitespace()
|
|
ctx.state = dFlowExplicitMapKey
|
|
of dFlowMapStart:
|
|
if not ctx.level.singleLine:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
if kind == ikCompactScalar and not ctx.options.explicitKeys:
|
|
ctx.state = dFlowImplicitMapKey
|
|
else:
|
|
ctx.append('?')
|
|
ctx.whitespace()
|
|
ctx.state = dFlowExplicitMapKey
|
|
of dFlowImplicitMapKey:
|
|
ctx.append(':')
|
|
ctx.whitespace(true)
|
|
ctx.state = dFlowMapValue
|
|
of dBlockSequenceItem:
|
|
if ctx.level.wroteAnything or ctx.options.indentationStep < 2:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
else:
|
|
ctx.level.wroteAnything = true
|
|
ctx.append('-')
|
|
ctx.whitespace()
|
|
of dFlowSequenceStart:
|
|
if not ctx.level.singleLine:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
ctx.state = dFlowSequenceItem
|
|
of dFlowSequenceItem:
|
|
ctx.append(',')
|
|
ctx.whitespace(true)
|
|
if not ctx.level.singleLine:
|
|
ctx.newline()
|
|
t.write(repeat(' ', ctx.indentation))
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
proc writeTagAndAnchor(
|
|
ctx : var Context,
|
|
props: Properties,
|
|
): bool {.raises: [YamlPresenterOutputError].} =
|
|
if ctx.options.suppressAttrs: return false
|
|
let t = ctx.target
|
|
result = false
|
|
try:
|
|
if props.tag notin [yTagQuestionMark, yTagExclamationMark]:
|
|
let tagUri = $props.tag
|
|
let (handle, length) = ctx.searchHandle(tagUri)
|
|
if length > 0:
|
|
ctx.append(handle)
|
|
t.write(tagUri[length..tagUri.high])
|
|
ctx.whitespace(true)
|
|
else:
|
|
ctx.append("!<")
|
|
t.write(tagUri)
|
|
t.write('>')
|
|
ctx.whitespace(true)
|
|
result = true
|
|
if props.anchor != yAnchorNone:
|
|
ctx.append("&")
|
|
t.write($props.anchor)
|
|
ctx.whitespace(true)
|
|
result = true
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
|
|
proc nextItem(
|
|
c: var Deque,
|
|
s: YamlStream,
|
|
): Event {.raises: [YamlStreamError].} =
|
|
if c.len > 0:
|
|
try: result = c.popFirst
|
|
except IndexDefect: internalError("Unexpected IndexError")
|
|
else:
|
|
result = s.next()
|
|
|
|
proc checkSingleLine(
|
|
ctx : var Context,
|
|
s : YamlStream,
|
|
item : Event,
|
|
mapping: bool
|
|
): (bool, MapParseState) =
|
|
let style = if mapping: item.mapStyle else: item.seqStyle
|
|
if ctx.options.newlines == nlNone: return (true, mpInitial)
|
|
if not ctx.options.condenseFlow: return (false, mpNeedBlock)
|
|
if (ctx.levels.len > 0 and ctx.state.isFlow) or style == csFlow or
|
|
(style == csAny and ctx.options.containers != cBlock):
|
|
result[1] = mpInitial
|
|
var length = 0
|
|
while not mapping or result[1] != mpNeedBlock:
|
|
let next = s.next()
|
|
ctx.cached.addLast(next)
|
|
case next.kind
|
|
of yamlScalar:
|
|
length += 2 + next.scalarContent.len
|
|
if next.scalarStyle in [ssFolded, ssLiteral]:
|
|
length = high(int)
|
|
result[1] = mpNeedBlock
|
|
break
|
|
case result[1]
|
|
of mpInitial: result[1] = mpKey
|
|
of mpKey: result[1] = mpValue
|
|
else: result[1] = mpNeedBlock
|
|
of yamlAlias:
|
|
length += 6
|
|
case result[1]
|
|
of mpInitial: result[1] = mpKey
|
|
of mpKey: result[1] = mpValue
|
|
else: result[1] = mpNeedBlock
|
|
of yamlEndSeq, yamlEndMap: break
|
|
else:
|
|
length = high(int)
|
|
result[1] = mpNeedBlock
|
|
break
|
|
result[0] = (not mapping or result[1] != mpNeedBlock) and
|
|
(ctx.options.maxLineLength.isNone or
|
|
length < ctx.options.maxLineLength.get() - ctx.indentation - 2)
|
|
else: result = (false, mpNeedBlock)
|
|
|
|
proc doPresent(
|
|
ctx: var Context,
|
|
s : YamlStream,
|
|
) {.raises: [
|
|
YamlPresenterJsonError, YamlPresenterOutputError,
|
|
YamlStreamError
|
|
].} =
|
|
var unclosedDoc = false
|
|
ctx.cached = initDeQue[Event]()
|
|
ctx.wroteDirectivesEnd = false
|
|
while true:
|
|
let item = nextItem(ctx.cached, s)
|
|
case item.kind
|
|
of yamlStartStream: discard
|
|
of yamlEndStream: break
|
|
of yamlStartDoc:
|
|
if unclosedDoc:
|
|
ctx.safeWrite("...")
|
|
ctx.safeNewline()
|
|
ctx.wroteDirectivesEnd =
|
|
item.explicitDirectivesEnd or ctx.options.directivesEnd == deAlways or not s.peek().emptyProperties()
|
|
|
|
if ctx.options.directivesEnd != deNever:
|
|
resetHandles(ctx.handles)
|
|
for v in item.handles:
|
|
discard registerHandle(ctx.handles, v.handle, v.uriPrefix)
|
|
|
|
try:
|
|
case ctx.options.outputVersion
|
|
of ov1_2:
|
|
ctx.target.write("%YAML 1.2")
|
|
ctx.newline()
|
|
ctx.wroteDirectivesEnd = true
|
|
of ov1_1:
|
|
ctx.target.write("%YAML 1.1")
|
|
ctx.newline()
|
|
ctx.wroteDirectivesEnd = true
|
|
of ovNone: discard
|
|
for v in ctx.handles:
|
|
if v.handle == "!":
|
|
if v.uriPrefix != "!":
|
|
ctx.target.write("%TAG ! " & v.uriPrefix)
|
|
ctx.newline()
|
|
ctx.wroteDirectivesEnd = true
|
|
elif v.handle == "!!":
|
|
if v.uriPrefix != yamlTagRepositoryPrefix:
|
|
ctx.target.write("%TAG !! " & v.uriPrefix)
|
|
ctx.newline()
|
|
ctx.wroteDirectivesEnd = true
|
|
else:
|
|
ctx.target.write("%TAG " & v.handle & ' ' & v.uriPrefix)
|
|
ctx.newline()
|
|
ctx.wroteDirectivesEnd = true
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
if ctx.wroteDirectivesEnd:
|
|
ctx.safeWrite("---")
|
|
ctx.whitespace(true)
|
|
of yamlScalar:
|
|
var
|
|
words, lines: seq[tuple[start, finish: int]]
|
|
scalarStyle = ssAny
|
|
multiLine = false
|
|
needsNextLine = false
|
|
if ctx.options.quoting in [sqUnset, sqDouble]:
|
|
words = @[]
|
|
lines = @[]
|
|
if ctx.levels.len > 0 and ctx.state == dBlockImplicitMapKey:
|
|
if ctx.options.maxLineLength.isNone or
|
|
ctx.indentation + ctx.options.indentationStep + ctx.lastImplicitKeyLen + 4 <
|
|
ctx.options.maxLineLength.get():
|
|
scalarStyle = item.scalarContent.inspect(
|
|
ctx.indentation + ctx.options.indentationStep + ctx.lastImplicitKeyLen + 2,
|
|
words, lines, multiLine,
|
|
ctx.options.maxLineLength, ctx.levels.len > 0 and ctx.state.isFlow, item.scalarStyle)
|
|
case scalarStyle
|
|
of ssPlain, ssSingleQuoted: discard
|
|
of ssDoubleQuoted:
|
|
if multiLine:
|
|
multiLine = false
|
|
scalarStyle = ssAny
|
|
needsNextLine = true
|
|
else:
|
|
scalarStyle = ssAny
|
|
needsNextLine = true
|
|
else: needsNextLine = true
|
|
if scalarStyle == ssAny:
|
|
scalarStyle = item.scalarContent.inspect(
|
|
ctx.indentation + ctx.options.indentationStep, words, lines, multiLine,
|
|
ctx.options.maxLineLength, ctx.levels.len > 0 and ctx.state.isFlow, item.scalarStyle)
|
|
ctx.startItem(case scalarStyle
|
|
of ssLiteral, ssFolded: ikBlockScalar
|
|
of ssDoubleQuoted:
|
|
if multiLine: ikMultilineFlowScalar else: ikCompactScalar
|
|
else: ikCompactScalar)
|
|
discard ctx.writeTagAndAnchor(item.scalarProperties)
|
|
if ctx.levels.len == 0:
|
|
if ctx.wroteDirectivesEnd and scalarStyle notin [ssLiteral, ssFolded]: ctx.safeNewline()
|
|
elif needsNextLine and scalarStyle in [ssDoubleQuoted, ssSingleQuoted, ssPlain]:
|
|
ctx.safeNewline()
|
|
ctx.safeWrite(repeat(' ', ctx.indentation + ctx.options.indentationStep))
|
|
case ctx.options.quoting
|
|
of sqJson:
|
|
var hint = yTypeUnknown
|
|
if ctx.state == dFlowMapValue: hint = guessType(item.scalarContent)
|
|
let tag = item.scalarProperties.tag
|
|
if tag in [yTagQuestionMark, yTagBoolean] and
|
|
hint in {yTypeBoolTrue, yTypeBoolFalse}:
|
|
ctx.safeWrite(if hint == yTypeBoolTrue: "true" else: "false")
|
|
elif tag in [yTagQuestionMark, yTagNull] and
|
|
hint == yTypeNull:
|
|
ctx.safeWrite("null")
|
|
elif tag in [yTagQuestionMark, yTagInteger,
|
|
yTagNimInt8, yTagNimInt16, yTagNimInt32, yTagNimInt64,
|
|
yTagNimUInt8, yTagNimUInt16, yTagNimUInt32, yTagNimUInt64] and
|
|
hint == yTypeInteger:
|
|
ctx.safeWrite(item.scalarContent)
|
|
elif tag in [yTagQuestionMark, yTagFloat, yTagNimFloat32,
|
|
yTagNimFloat64] and hint in {yTypeFloatInf, yTypeFloatNaN}:
|
|
raise newException(YamlPresenterJsonError,
|
|
"Infinity and not-a-number values cannot be presented as JSON!")
|
|
elif tag in [yTagQuestionMark, yTagFloat] and
|
|
hint == yTypeFloat:
|
|
ctx.safeWrite(item.scalarContent)
|
|
else:
|
|
ctx.lastImplicitKeyLen = ctx.writeDoubleQuotedJson(item.scalarContent)
|
|
of sqDouble:
|
|
ctx.lastImplicitKeyLen = ctx.writeDoubleQuoted(item.scalarContent)
|
|
else:
|
|
case scalarStyle
|
|
of ssLiteral: ctx.writeLiteral(item.scalarContent, lines)
|
|
of ssFolded: ctx.writeFolded(item.scalarContent, words)
|
|
of ssPlain:
|
|
ctx.safeWrite(item.scalarContent)
|
|
ctx.lastImplicitKeyLen = item.scalarContent.len
|
|
of ssSingleQuoted:
|
|
ctx.lastImplicitKeyLen = ctx.writeSingleQuoted(item.scalarContent)
|
|
of ssDoubleQuoted:
|
|
ctx.lastImplicitKeyLen = ctx.writeDoubleQuoted(item.scalarContent)
|
|
else: discard # ssAny, can never happen
|
|
of yamlAlias:
|
|
if ctx.options.quoting == sqJson:
|
|
raise newException(YamlPresenterJsonError,
|
|
"Alias not allowed in JSON output")
|
|
yAssert ctx.levels.len > 0
|
|
ctx.startItem(ikCompactScalar)
|
|
try:
|
|
ctx.append('*')
|
|
ctx.target.write($item.aliasTarget)
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
of yamlStartSeq:
|
|
let (singleLine, _) = ctx.checkSingleLine(s, item, false)
|
|
var nextState: DumperState
|
|
if (ctx.levels.len > 0 and ctx.state.isFlow) or item.seqStyle == csFlow:
|
|
nextState = dFlowSequenceStart
|
|
elif item.seqStyle == csBlock:
|
|
let next = s.peek()
|
|
nextState = if next.kind == yamlEndSeq: dFlowSequenceStart else: dBlockSequenceItem
|
|
else:
|
|
case ctx.options.containers
|
|
of cMixed:
|
|
nextState = if singleLine: dFlowSequenceStart else: dBlockSequenceItem
|
|
of cFlow: nextState = dFlowSequenceStart
|
|
of cBlock:
|
|
let next = s.peek()
|
|
nextState = if next.kind == yamlEndSeq: dFlowSequenceStart else: dBlockSequenceItem
|
|
|
|
var indentation = 0
|
|
var wroteAnything = false
|
|
if ctx.levels.len > 0: wroteAnything = ctx.state == dBlockImplicitMapKey
|
|
|
|
if ctx.levels.len == 0:
|
|
if nextState == dFlowSequenceStart:
|
|
indentation = ctx.options.indentationStep
|
|
else:
|
|
ctx.startItem(ikCollection)
|
|
indentation = ctx.indentation + ctx.options.indentationStep
|
|
if nextState.isFlow and not ctx.state.isFlow:
|
|
inc(indentation, ctx.options.indentationStep)
|
|
|
|
let wroteAttrs = ctx.writeTagAndAnchor(item.seqProperties)
|
|
if wroteAttrs or (ctx.wroteDirectivesEnd and ctx.levels.len == 0):
|
|
wroteAnything = true
|
|
|
|
if nextState == dFlowSequenceStart:
|
|
if ctx.levels.len == 0:
|
|
if wroteAttrs or ctx.wroteDirectivesEnd: ctx.safeNewline()
|
|
ctx.safeWrite('[')
|
|
|
|
ctx.levels.add (nextState, indentation, singleLine, wroteAnything)
|
|
of yamlStartMap:
|
|
let (singleLine, mps) = ctx.checkSingleLine(s, item, true)
|
|
var nextState: DumperState
|
|
if (ctx.levels.len > 0 and ctx.state.isFlow) or item.mapStyle == csFlow:
|
|
nextState = dFlowMapStart
|
|
elif item.mapStyle == csBlock:
|
|
let next = s.peek()
|
|
nextState = if next.kind == yamlEndMap: dFlowMapStart else: dBlockMapValue
|
|
else:
|
|
case ctx.options.containers
|
|
of cMixed:
|
|
if mps == mpNeedBlock:
|
|
nextState = dBlockMapValue
|
|
elif ctx.levels.len == 0 or ctx.state == dBlockSequenceItem and item.emptyProperties:
|
|
nextState = dBlockInlineMap
|
|
else:
|
|
nextState = dFlowMapStart
|
|
of cFlow: nextState = dFlowMapStart
|
|
of cBlock:
|
|
let next = s.peek()
|
|
if next.kind == yamlEndMap: nextState = dFlowMapStart
|
|
else: nextState = dBlockMapValue
|
|
|
|
var indentation = 0
|
|
var wroteAnything = false
|
|
if ctx.levels.len > 0: wroteAnything = ctx.state == dBlockImplicitMapKey
|
|
|
|
if ctx.levels.len == 0:
|
|
if nextState == dFlowMapStart:
|
|
indentation = ctx.options.indentationStep
|
|
else:
|
|
ctx.startItem(ikCollection)
|
|
indentation = ctx.indentation + ctx.options.indentationStep
|
|
if nextState.isFlow and not ctx.state.isFlow:
|
|
inc(indentation, ctx.options.indentationStep)
|
|
|
|
let wroteAttrs = ctx.writeTagAndAnchor(item.properties)
|
|
if wroteAttrs or (ctx.wroteDirectivesEnd and ctx.levels.len == 0):
|
|
wroteAnything = true
|
|
|
|
if nextState == dFlowMapStart:
|
|
if ctx.levels.len == 0:
|
|
if wroteAttrs or ctx.wroteDirectivesEnd: ctx.safeNewline()
|
|
ctx.safeWrite('{')
|
|
|
|
ctx.levels.add (nextState, indentation, singleLine, wroteAnything)
|
|
of yamlEndSeq:
|
|
yAssert ctx.levels.len > 0
|
|
let level = ctx.levels.pop()
|
|
case level.state
|
|
of dFlowSequenceItem:
|
|
try:
|
|
if not level.singleLine:
|
|
ctx.newline()
|
|
ctx.target.write(repeat(' ', level.indentation - ctx.options.indentationStep))
|
|
ctx.target.write(']')
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
of dFlowSequenceStart: ctx.safeWrite(']')
|
|
of dBlockSequenceItem: discard
|
|
else: internalError("Invalid popped level")
|
|
of yamlEndMap:
|
|
yAssert ctx.levels.len > 0
|
|
let level = ctx.levels.pop()
|
|
case level.state
|
|
of dFlowMapValue:
|
|
try:
|
|
if not level.singleLine:
|
|
ctx.safeNewline()
|
|
ctx.target.write(repeat(' ', level.indentation - ctx.options.indentationStep))
|
|
ctx.append('}')
|
|
except CatchableError as ce:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = ce
|
|
raise e
|
|
of dFlowMapStart: ctx.safeWrite('}')
|
|
of dBlockMapValue, dBlockInlineMap: discard
|
|
else: internalError("Invalid level: " & $level)
|
|
of yamlEndDoc:
|
|
ctx.safeNewline()
|
|
if item.explicitDocumentEnd:
|
|
ctx.safeWrite("...")
|
|
ctx.safeNewline()
|
|
else:
|
|
unclosedDoc = true
|
|
|
|
proc present*(
|
|
s : YamlStream,
|
|
target : Stream,
|
|
options: PresentationOptions = PresentationOptions(),
|
|
) {.raises: [
|
|
YamlPresenterJsonError, YamlPresenterOutputError,
|
|
YamlStreamError
|
|
].} =
|
|
## Convert ``s`` to a YAML character stream and write it to ``target``.
|
|
var c = Context(target: target, options: options)
|
|
doPresent(c, s)
|
|
|
|
proc present*(
|
|
s : YamlStream,
|
|
options: PresentationOptions = PresentationOptions(),
|
|
): string {.raises: [
|
|
YamlPresenterJsonError, YamlPresenterOutputError,
|
|
YamlStreamError
|
|
].} =
|
|
## Convert ``s`` to a YAML character stream and return it as string.
|
|
|
|
var
|
|
ss = newStringStream()
|
|
c = Context(target: ss, options: options)
|
|
doPresent(c, s)
|
|
return ss.data
|
|
|
|
proc doTransform(
|
|
ctx : var Context,
|
|
input: Stream,
|
|
resolveToCoreYamlTags: bool,
|
|
) =
|
|
var parser: YamlParser
|
|
parser.init()
|
|
var events = parser.parse(input)
|
|
try:
|
|
var bys: YamlStream = newBufferYamlStream()
|
|
for e in events:
|
|
var event = e
|
|
case event.kind
|
|
of yamlStartStream, yamlEndStream, yamlStartDoc, yamlEndDoc, yamlEndMap, yamlAlias, yamlEndSeq:
|
|
discard
|
|
of yamlStartMap:
|
|
event.mapStyle = csAny
|
|
if resolveToCoreYamlTags:
|
|
if event.mapProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
|
|
event.mapProperties.tag = yTagMapping
|
|
of yamlStartSeq:
|
|
event.seqStyle = csAny
|
|
if resolveToCoreYamlTags:
|
|
if event.seqProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
|
|
event.seqProperties.tag = yTagSequence
|
|
of yamlScalar:
|
|
event.scalarStyle = ssAny
|
|
if resolveToCoreYamlTags:
|
|
if event.scalarProperties.tag == yTagQuestionMark:
|
|
case guessType(event.scalarContent)
|
|
of yTypeInteger: event.scalarProperties.tag = yTagInteger
|
|
of yTypeFloat, yTypeFloatInf, yTypeFloatNaN:
|
|
event.scalarProperties.tag = yTagFloat
|
|
of yTypeBoolTrue, yTypeBoolFalse: event.scalarProperties.tag = yTagBoolean
|
|
of yTypeNull: event.scalarProperties.tag = yTagNull
|
|
of yTypeTimestamp: event.scalarProperties.tag = yTagTimestamp
|
|
of yTypeUnknown: event.scalarProperties.tag = yTagString
|
|
elif event.scalarProperties.tag == yTagExclamationMark:
|
|
event.scalarProperties.tag = yTagString
|
|
BufferYamlStream(bys).put(event)
|
|
doPresent(ctx, bys)
|
|
except YamlStreamError as e:
|
|
var curE: ref Exception = e
|
|
while curE.parent of YamlStreamError: curE = curE.parent
|
|
if curE.parent of IOError: raise (ref IOError)(curE.parent)
|
|
elif curE.parent of OSError: raise (ref OSError)(curE.parent)
|
|
elif curE.parent of YamlParserError: raise (ref YamlParserError)(curE.parent)
|
|
else: internalError("Unexpected exception: " & curE.parent.repr)
|
|
|
|
proc genInput(input: Stream): Stream = input
|
|
proc genInput(input: string): Stream = newStringStream(input)
|
|
|
|
proc transform*(
|
|
input : Stream | string,
|
|
output : Stream,
|
|
options: PresentationOptions = PresentationOptions(),
|
|
resolveToCoreYamlTags: bool = false,
|
|
) {.raises: [
|
|
IOError, OSError, YamlParserError, YamlPresenterJsonError,
|
|
YamlPresenterOutputError
|
|
].} =
|
|
## Parse ``input`` as YAML character stream and then dump it to ``output``
|
|
## using the given presentation options.
|
|
## If ``resolveToCoreYamlTags`` is ``true``, non-specific tags will
|
|
## be replaced by specific tags according to the YAML core schema.
|
|
var c = Context(target: output, options: options)
|
|
doTransform(c, genInput(input), resolveToCoreYamlTags)
|
|
|
|
proc transform*(
|
|
input : Stream | string,
|
|
options: PresentationOptions = PresentationOptions(),
|
|
resolveToCoreYamlTags: bool = false,
|
|
): string {.raises: [
|
|
IOError, OSError, YamlParserError, YamlPresenterJsonError,
|
|
YamlPresenterOutputError
|
|
].} =
|
|
## Parse ``input`` as YAML character stream and then dump it
|
|
## using the given presentation options. Returns the resulting string.
|
|
## If ``resolveToCoreYamlTags`` is ``true``, non-specific tags will
|
|
## be replaced by specific tags according to the YAML core schema.
|
|
var
|
|
ss = newStringStream()
|
|
c = Context(target: ss, options: options)
|
|
doTransform(c, genInput(input), resolveToCoreYamlTags)
|
|
return ss.data
|