mirror of https://github.com/status-im/NimYAML.git
825 lines
31 KiB
Nim
825 lines
31 KiB
Nim
# NimYAML - YAML implementation in Nim
|
|
# (c) Copyright 2016 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 streams, deques, strutils
|
|
import data, taglib, stream, private/internal, hints, parser, stream
|
|
|
|
type
|
|
PresentationStyle* = enum
|
|
## Different styles for YAML character stream output.
|
|
##
|
|
## - ``ypsMinimal``: Single-line flow-only output which tries to
|
|
## use as few characters as possible.
|
|
## - ``ypsCanonical``: Canonical YAML output. Writes all tags except
|
|
## for the non-specific tags ``?`` and ``!``, uses flow style, quotes
|
|
## all string scalars.
|
|
## - ``ypsDefault``: Tries to be as human-readable as possible. Uses
|
|
## block style by default, but tries to condense mappings and
|
|
## sequences which only contain scalar nodes into a single line using
|
|
## flow style.
|
|
## - ``ypsJson``: Omits the ``%YAML`` directive and the ``---``
|
|
## marker. Uses flow style. Flattens anchors and aliases, omits tags.
|
|
## Output will be parseable as JSON. ``YamlStream`` to dump may only
|
|
## contain one document.
|
|
## - ``ypsBlockOnly``: Formats all output in block style, does not use
|
|
## flow style at all.
|
|
psMinimal, psCanonical, psDefault, psJson, psBlockOnly
|
|
|
|
TagStyle* = enum
|
|
## Whether object should be serialized with explicit tags.
|
|
##
|
|
## - ``tsNone``: No tags will be outputted unless necessary.
|
|
## - ``tsRootOnly``: A tag will only be outputted for the root tag and
|
|
## where necessary.
|
|
## - ``tsAll``: Tags will be outputted for every object.
|
|
tsNone, tsRootOnly, tsAll
|
|
|
|
AnchorStyle* = enum
|
|
## How ref object should be serialized.
|
|
##
|
|
## - ``asNone``: No anchors will be outputted. Values present at
|
|
## multiple places in the content that should be serialized will be
|
|
## fully serialized at every occurence. If the content is cyclic, this
|
|
## will lead to an endless loop!
|
|
## - ``asTidy``: Anchors will only be generated for objects that
|
|
## actually occur more than once in the content to be serialized.
|
|
## This is a bit slower and needs more memory than ``asAlways``.
|
|
## - ``asAlways``: Achors will be generated for every ref object in the
|
|
## content to be serialized, regardless of whether the object is
|
|
## referenced again afterwards
|
|
asNone, asTidy, asAlways
|
|
|
|
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).
|
|
nlLF, nlCRLF, nlOSDefault
|
|
|
|
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
|
|
|
|
PresentationOptions* = object
|
|
## Options for generating a YAML character stream
|
|
style*: PresentationStyle
|
|
indentationStep*: int
|
|
newlines*: NewLineStyle
|
|
outputVersion*: OutputYamlVersion
|
|
|
|
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
|
|
|
|
ScalarStyle = enum
|
|
sLiteral, sFolded, sPlain, sDoubleQuoted
|
|
|
|
Context = object
|
|
target: Stream
|
|
options: PresentationOptions
|
|
handles: seq[tuple[handle, uriPrefix: string]]
|
|
levels: seq[DumperState]
|
|
|
|
const
|
|
defaultPresentationOptions* =
|
|
PresentationOptions(style: psDefault, indentationStep: 2,
|
|
newlines: nlOSDefault)
|
|
|
|
proc defineOptions*(style: PresentationStyle = psDefault,
|
|
indentationStep: int = 2,
|
|
newlines: NewLineStyle = nlOSDefault,
|
|
outputVersion: OutputYamlVersion = ov1_2):
|
|
PresentationOptions {.raises: [].} =
|
|
## Define a set of options for presentation. Convenience proc that requires
|
|
## you to only set those values that should not equal the default.
|
|
PresentationOptions(style: style, indentationStep: indentationStep,
|
|
newlines: newlines, outputVersion: outputVersion)
|
|
|
|
proc state(c: Context): DumperState = c.levels[^1]
|
|
|
|
proc `state=`(c: var Context, v: DumperState) =
|
|
c.levels[^1] = v
|
|
|
|
proc searchHandle(c: 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 c.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]]):
|
|
ScalarStyle {.raises: [].} =
|
|
var
|
|
inLine = false
|
|
inWord = false
|
|
multipleSpaces = true
|
|
curWord, curLine: tuple[start, finish: int]
|
|
canUseFolded = true
|
|
canUseLiteral = true
|
|
canUsePlain = scalar.len > 0 and
|
|
scalar[0] notin {'@', '`', '|', '>', '&', '*', '!', ' ', '\t'}
|
|
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
|
|
# linebreak. that is currently too complex to handle.
|
|
canUseFolded = false
|
|
of '\l':
|
|
canUsePlain = false # we don't use multiline plain scalars
|
|
curWord.finish = i - 1
|
|
if curWord.finish - curWord.start + 1 > 80 - indentation:
|
|
return if canUsePlain: sPlain else: sDoubleQuoted
|
|
words.add(curWord)
|
|
inWord = false
|
|
curWord.start = i + 1
|
|
multipleSpaces = true
|
|
if not inLine: curLine.start = i
|
|
inLine = false
|
|
curLine.finish = i - 1
|
|
if curLine.finish - curLine.start + 1 > 80 - indentation:
|
|
canUseLiteral = false
|
|
lines.add(curLine)
|
|
else:
|
|
if c in {'{', '}', '[', ']', ',', '#', '-', ':', '?', '%', '"', '\''} or
|
|
c.ord < 32: canUsePlain = false
|
|
if not inLine:
|
|
curLine.start = i
|
|
inLine = true
|
|
if not inWord:
|
|
if not multipleSpaces:
|
|
if curWord.finish - curWord.start + 1 > 80 - indentation:
|
|
return if canUsePlain: sPlain else: sDoubleQuoted
|
|
words.add(curWord)
|
|
curWord.start = i
|
|
inWord = true
|
|
multipleSpaces = false
|
|
if inWord:
|
|
curWord.finish = scalar.len - 1
|
|
if curWord.finish - curWord.start + 1 > 80 - indentation:
|
|
return if canUsePlain: sPlain else: sDoubleQuoted
|
|
words.add(curWord)
|
|
if inLine:
|
|
curLine.finish = scalar.len - 1
|
|
if curLine.finish - curLine.start + 1 > 80 - indentation:
|
|
canUseLiteral = false
|
|
lines.add(curLine)
|
|
if scalar.len <= 80 - indentation:
|
|
result = if canUsePlain: sPlain else: sDoubleQuoted
|
|
elif canUseLiteral: result = sLiteral
|
|
elif canUseFolded: result = sFolded
|
|
elif canUsePlain: result = sPlain
|
|
else: result = sDoubleQuoted
|
|
|
|
template append(target: Stream, val: string | char) =
|
|
target.write(val)
|
|
|
|
template append(target: ptr[string], val: string | char) =
|
|
target[].add(val)
|
|
|
|
proc writeDoubleQuoted(c: Context, scalar: string, indentation: int,
|
|
newline: string)
|
|
{.raises: [YamlPresenterOutputError].} =
|
|
var curPos = indentation
|
|
let t = c.target
|
|
try:
|
|
t.append('"')
|
|
curPos.inc()
|
|
for c in scalar:
|
|
if curPos == 79:
|
|
t.append('\\')
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
curPos = indentation
|
|
if c == ' ':
|
|
t.append('\\')
|
|
curPos.inc()
|
|
case c
|
|
of '"':
|
|
t.append("\\\"")
|
|
curPos.inc(2)
|
|
of '\l':
|
|
t.append("\\n")
|
|
curPos.inc(2)
|
|
of '\t':
|
|
t.append("\\t")
|
|
curPos.inc(2)
|
|
of '\\':
|
|
t.append("\\\\")
|
|
curPos.inc(2)
|
|
else:
|
|
if ord(c) < 32:
|
|
t.append("\\x" & toHex(ord(c), 2))
|
|
curPos.inc(4)
|
|
else:
|
|
t.append(c)
|
|
curPos.inc()
|
|
t.append('"')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeDoubleQuotedJson(c: Context, scalar: string)
|
|
{.raises: [YamlPresenterOutputError].} =
|
|
let t = c.target
|
|
try:
|
|
t.append('"')
|
|
for c in scalar:
|
|
case c
|
|
of '"': t.append("\\\"")
|
|
of '\\': t.append("\\\\")
|
|
of '\l': t.append("\\n")
|
|
of '\t': t.append("\\t")
|
|
of '\f': t.append("\\f")
|
|
of '\b': t.append("\\b")
|
|
else:
|
|
if ord(c) < 32: t.append("\\u" & toHex(ord(c), 4)) else: t.append(c)
|
|
t.append('"')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeLiteral(c: Context, scalar: string, indentation, indentStep: int,
|
|
lines: seq[tuple[start, finish: int]], newline: string)
|
|
{.raises: [YamlPresenterOutputError].} =
|
|
let t = c.target
|
|
try:
|
|
t.append('|')
|
|
if scalar[^1] != '\l': t.append('-')
|
|
if scalar[0] in [' ', '\t']: t.append($indentStep)
|
|
for line in lines:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation + indentStep))
|
|
if line.finish >= line.start:
|
|
t.append(scalar[line.start .. line.finish])
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeFolded(c: Context, scalar: string, indentation, indentStep: int,
|
|
words: seq[tuple[start, finish: int]],
|
|
newline: string)
|
|
{.raises: [YamlPresenterOutputError].} =
|
|
let t = c.target
|
|
try:
|
|
t.append(">")
|
|
if scalar[^1] != '\l': t.append('-')
|
|
if scalar[0] in [' ', '\t']: t.append($indentStep)
|
|
var curPos = 80
|
|
for word in words:
|
|
if word.start > 0 and scalar[word.start - 1] == '\l':
|
|
t.append(newline & newline)
|
|
t.append(repeat(' ', indentation + indentStep))
|
|
curPos = indentation + indentStep
|
|
elif curPos + (word.finish - word.start) > 80:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation + indentStep))
|
|
curPos = indentation + indentStep
|
|
else:
|
|
t.append(' ')
|
|
curPos.inc()
|
|
t.append(scalar[word.start .. word.finish])
|
|
curPos += word.finish - word.start + 1
|
|
except:
|
|
var e = newException(YamlPresenterOutputError,
|
|
"Error while writing to output stream")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
template safeWrite(c: Context, s: string or char) =
|
|
try: c.target.append(s)
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc startItem(c: var Context, indentation: int, isObject: bool,
|
|
newline: string) {.raises: [YamlPresenterOutputError].} =
|
|
let t = c.target
|
|
try:
|
|
case c.state
|
|
of dBlockMapValue:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
if isObject or c.options.style == psCanonical:
|
|
t.append("? ")
|
|
c.state = dBlockExplicitMapKey
|
|
else: c.state = dBlockImplicitMapKey
|
|
of dBlockInlineMap: c.state = dBlockImplicitMapKey
|
|
of dBlockExplicitMapKey:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
t.append(": ")
|
|
c.state = dBlockMapValue
|
|
of dBlockImplicitMapKey:
|
|
t.append(": ")
|
|
c.state = dBlockMapValue
|
|
of dFlowExplicitMapKey:
|
|
if c.options.style != psMinimal:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
t.append(": ")
|
|
c.state = dFlowMapValue
|
|
of dFlowMapValue:
|
|
if (isObject and c.options.style != psMinimal) or c.options.style in [psJson, psCanonical]:
|
|
t.append(',' & newline & repeat(' ', indentation))
|
|
if c.options.style == psJson: c.state = dFlowImplicitMapKey
|
|
else:
|
|
t.append("? ")
|
|
c.state = dFlowExplicitMapKey
|
|
elif isObject and c.options.style == psMinimal:
|
|
t.append(", ? ")
|
|
c.state = dFlowExplicitMapKey
|
|
else:
|
|
t.append(", ")
|
|
c.state = dFlowImplicitMapKey
|
|
of dFlowMapStart:
|
|
if (isObject and c.options.style != psMinimal) or c.options.style in [psJson, psCanonical]:
|
|
t.append(newline & repeat(' ', indentation))
|
|
if c.options.style == psJson: c.state = dFlowImplicitMapKey
|
|
else:
|
|
t.append("? ")
|
|
c.state = dFlowExplicitMapKey
|
|
else: c.state = dFlowImplicitMapKey
|
|
of dFlowImplicitMapKey:
|
|
t.append(": ")
|
|
c.state = dFlowMapValue
|
|
of dBlockSequenceItem:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
t.append("- ")
|
|
of dFlowSequenceStart:
|
|
case c.options.style
|
|
of psMinimal, psDefault: discard
|
|
of psCanonical, psJson:
|
|
t.append(newline)
|
|
t.append(repeat(' ', indentation))
|
|
of psBlockOnly: discard # can never happen
|
|
c.state = dFlowSequenceItem
|
|
of dFlowSequenceItem:
|
|
case c.options.style
|
|
of psMinimal, psDefault: t.append(", ")
|
|
of psCanonical, psJson:
|
|
t.append(',' & newline)
|
|
t.append(repeat(' ', indentation))
|
|
of psBlockOnly: discard # can never happen
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc writeTagAndAnchor(c: Context, props: Properties) {.raises: [YamlPresenterOutputError].} =
|
|
let t = c.target
|
|
try:
|
|
if props.tag notin [yTagQuestionMark, yTagExclamationMark]:
|
|
let tagUri = $props.tag
|
|
let (handle, length) = c.searchHandle(tagUri)
|
|
if length > 0:
|
|
t.append(handle)
|
|
t.append(tagUri[length..tagUri.high])
|
|
t.append(' ')
|
|
else:
|
|
t.append("!<")
|
|
t.append(tagUri)
|
|
t.append("> ")
|
|
if props.anchor != yAnchorNone:
|
|
t.append("&")
|
|
t.append($props.anchor)
|
|
t.append(' ')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
|
|
proc nextItem(c: var Deque, s: var YamlStream):
|
|
Event {.raises: [YamlStreamError].} =
|
|
if c.len > 0:
|
|
try: result = c.popFirst
|
|
except IndexDefect: internalError("Unexpected IndexError")
|
|
else:
|
|
result = s.next()
|
|
|
|
proc doPresent(c: var Context, s: var YamlStream) =
|
|
var
|
|
indentation = 0
|
|
cached = initDeQue[Event]()
|
|
let newline = if c.options.newlines == nlLF: "\l"
|
|
elif c.options.newlines == nlCRLF: "\c\l" else: "\n"
|
|
var firstDoc = true
|
|
while true:
|
|
let item = nextItem(cached, s)
|
|
case item.kind
|
|
of yamlStartStream: discard
|
|
of yamlEndStream: break
|
|
of yamlStartDoc:
|
|
resetHandles(c.handles)
|
|
for v in item.handles:
|
|
discard registerHandle(c.handles, v.handle, v.uriPrefix)
|
|
if not firstDoc:
|
|
if c.options.style == psJson:
|
|
raise newException(YamlPresenterJsonError,
|
|
"Cannot output more than one document in JSON style")
|
|
c.safeWrite("..." & newline)
|
|
|
|
if c.options.style != psJson:
|
|
try:
|
|
case c.options.outputVersion
|
|
of ov1_2: c.target.append("%YAML 1.2" & newline)
|
|
of ov1_1: c.target.append("%YAML 1.1" & newLine)
|
|
of ovNone: discard
|
|
for v in c.handles:
|
|
if v.handle == "!":
|
|
if v.uriPrefix != "!":
|
|
c.target.append("%TAG ! " & v.uriPrefix & newline)
|
|
elif v.handle == "!!":
|
|
if v.uriPrefix != yamlTagRepositoryPrefix:
|
|
c.target.append("%TAG !! " & v.uriPrefix & newline)
|
|
else:
|
|
c.target.append("%TAG " & v.handle & ' ' & v.uriPrefix & newline)
|
|
c.target.append("--- ")
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
of yamlScalar:
|
|
if c.levels.len == 0:
|
|
if c.options.style != psJson: c.safeWrite(newline)
|
|
else:
|
|
c.startItem(indentation, false, newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.scalarProperties)
|
|
|
|
if c.options.style == psJson:
|
|
let hint = guessType(item.scalarContent)
|
|
let tag = item.scalarProperties.tag
|
|
if tag in [yTagQuestionMark, yTagBoolean] and
|
|
hint in {yTypeBoolTrue, yTypeBoolFalse}:
|
|
c.safeWrite(if hint == yTypeBoolTrue: "true" else: "false")
|
|
elif tag in [yTagQuestionMark, yTagNull] and
|
|
hint == yTypeNull:
|
|
c.safeWrite("null")
|
|
elif tag in [yTagQuestionMark, yTagInteger,
|
|
yTagNimInt8, yTagNimInt16, yTagNimInt32, yTagNimInt64,
|
|
yTagNimUInt8, yTagNimUInt16, yTagNimUInt32, yTagNimUInt64] and
|
|
hint == yTypeInteger:
|
|
c.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:
|
|
c.safeWrite(item.scalarContent)
|
|
else: c.writeDoubleQuotedJson(item.scalarContent)
|
|
elif c.options.style == psCanonical:
|
|
c.writeDoubleQuoted(item.scalarContent,
|
|
indentation + c.options.indentationStep, newline)
|
|
else:
|
|
var words, lines = newSeq[tuple[start, finish: int]]()
|
|
case item.scalarContent.inspect(
|
|
indentation + c.options.indentationStep, words, lines)
|
|
of sLiteral: c.writeLiteral(item.scalarContent, indentation,
|
|
c.options.indentationStep, lines, newline)
|
|
of sFolded: c.writeFolded(item.scalarContent, indentation,
|
|
c.options.indentationStep, words, newline)
|
|
of sPlain: c.safeWrite(item.scalarContent)
|
|
of sDoubleQuoted: c.writeDoubleQuoted(item.scalarContent,
|
|
indentation + c.options.indentationStep, newline)
|
|
of yamlAlias:
|
|
if c.options.style == psJson:
|
|
raise newException(YamlPresenterJsonError,
|
|
"Alias not allowed in JSON output")
|
|
yAssert c.levels.len > 0
|
|
c.startItem(indentation, false, newline)
|
|
try:
|
|
c.target.append('*')
|
|
c.target.append($item.aliasTarget)
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
of yamlStartSeq:
|
|
var nextState: DumperState
|
|
case c.options.style
|
|
of psDefault:
|
|
var length = 0
|
|
while true:
|
|
let next = s.next()
|
|
cached.addLast(next)
|
|
case next.kind
|
|
of yamlScalar: length += 2 + next.scalarContent.len
|
|
of yamlAlias: length += 6
|
|
of yamlEndSeq: break
|
|
else:
|
|
length = high(int)
|
|
break
|
|
nextState = if length <= 60: dFlowSequenceStart else: dBlockSequenceItem
|
|
of psJson:
|
|
if c.levels.len > 0 and c.state in [dFlowMapStart, dFlowMapValue]:
|
|
raise newException(YamlPresenterJsonError, "Cannot have sequence as map key in JSON output!")
|
|
nextState = dFlowSequenceStart
|
|
of psMinimal, psCanonical: nextState = dFlowSequenceStart
|
|
of psBlockOnly:
|
|
let next = s.peek()
|
|
if next.kind == yamlEndSeq: nextState = dFlowSequenceStart
|
|
else: nextState = dBlockSequenceItem
|
|
|
|
if c.levels.len == 0:
|
|
case nextState
|
|
of dBlockSequenceItem:
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.seqProperties)
|
|
of dFlowSequenceStart:
|
|
c.safeWrite(newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.seqProperties)
|
|
indentation += c.options.indentationStep
|
|
else: internalError("Invalid nextState: " & $nextState)
|
|
else:
|
|
c.startItem(indentation, true, newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.seqProperties)
|
|
indentation += c.options.indentationStep
|
|
|
|
if nextState == dFlowSequenceStart: c.safeWrite('[')
|
|
if c.levels.len > 0 and c.options.style in [psJson, psCanonical] and
|
|
c.state in [dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockSequenceItem]:
|
|
indentation += c.options.indentationStep
|
|
c.levels.add(nextState)
|
|
of yamlStartMap:
|
|
var nextState: DumperState
|
|
case c.options.style
|
|
of psDefault:
|
|
type MapParseState = enum
|
|
mpInitial, mpKey, mpValue, mpNeedBlock
|
|
var mps: MapParseState = mpInitial
|
|
while mps != mpNeedBlock:
|
|
case s.peek().kind
|
|
of yamlScalar, yamlAlias:
|
|
case mps
|
|
of mpInitial: mps = mpKey
|
|
of mpKey: mps = mpValue
|
|
else: mps = mpNeedBlock
|
|
of yamlEndMap: break
|
|
else: mps = mpNeedBlock
|
|
nextState = if mps == mpNeedBlock: dBlockMapValue else: dBlockInlineMap
|
|
of psMinimal: nextState = dFlowMapStart
|
|
of psCanonical: nextState = dFlowMapStart
|
|
of psJson:
|
|
if c.levels.len > 0 and c.state in [dFlowMapStart, dFlowMapValue]:
|
|
raise newException(YamlPresenterJsonError,
|
|
"Cannot have map as map key in JSON output!")
|
|
nextState = dFlowMapStart
|
|
of psBlockOnly:
|
|
let next = s.peek()
|
|
if next.kind == yamlEndMap: nextState = dFlowMapStart
|
|
else: nextState = dBlockMapValue
|
|
if c.levels.len == 0:
|
|
case nextState
|
|
of dBlockMapValue:
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.mapProperties)
|
|
else:
|
|
if c.options.style != psJson:
|
|
c.safeWrite(newline)
|
|
c.writeTagAndAnchor(item.mapProperties)
|
|
indentation += c.options.indentationStep
|
|
of dFlowMapStart:
|
|
c.safeWrite(newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.mapProperties)
|
|
indentation += c.options.indentationStep
|
|
of dBlockInlineMap: discard
|
|
else: internalError("Invalid nextState: " & $nextState)
|
|
else:
|
|
if nextState in [dBlockMapValue, dBlockImplicitMapKey]:
|
|
c.startItem(indentation, true, newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.mapProperties)
|
|
else:
|
|
c.startItem(indentation, true, newline)
|
|
if c.options.style != psJson:
|
|
c.writeTagAndAnchor(item.mapProperties)
|
|
indentation += c.options.indentationStep
|
|
|
|
if nextState == dFlowMapStart: c.safeWrite('{')
|
|
if c.levels.len > 0 and c.options.style in [psJson, psCanonical] and
|
|
c.state in [dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockImplicitMapKey,
|
|
dBlockSequenceItem]:
|
|
indentation += c.options.indentationStep
|
|
c.levels.add(nextState)
|
|
|
|
of yamlEndSeq:
|
|
yAssert c.levels.len > 0
|
|
case c.levels.pop()
|
|
of dFlowSequenceItem:
|
|
case c.options.style
|
|
of psDefault, psMinimal, psBlockOnly: c.safeWrite(']')
|
|
of psJson, psCanonical:
|
|
indentation -= c.options.indentationStep
|
|
try:
|
|
c.target.append(newline)
|
|
c.target.append(repeat(' ', indentation))
|
|
c.target.append(']')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
if c.levels.len == 0 or c.state notin
|
|
[dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockSequenceItem]:
|
|
continue
|
|
of dFlowSequenceStart:
|
|
if c.levels.len > 0 and c.options.style in [psJson, psCanonical] and
|
|
c.state in [dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockSequenceItem]:
|
|
indentation -= c.options.indentationStep
|
|
c.safeWrite(']')
|
|
of dBlockSequenceItem: discard
|
|
else: internalError("Invalid popped level")
|
|
indentation -= c.options.indentationStep
|
|
of yamlEndMap:
|
|
yAssert c.levels.len > 0
|
|
let level = c.levels.pop()
|
|
case level
|
|
of dFlowMapValue:
|
|
case c.options.style
|
|
of psDefault, psMinimal, psBlockOnly: c.safeWrite('}')
|
|
of psJson, psCanonical:
|
|
indentation -= c.options.indentationStep
|
|
try:
|
|
c.target.append(newline)
|
|
c.target.append(repeat(' ', indentation))
|
|
c.target.append('}')
|
|
except:
|
|
var e = newException(YamlPresenterOutputError, "")
|
|
e.parent = getCurrentException()
|
|
raise e
|
|
if c.levels.len == 0 or c.state notin
|
|
[dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockSequenceItem]:
|
|
continue
|
|
of dFlowMapStart:
|
|
if c.levels.len > 0 and c.options.style in [psJson, psCanonical] and
|
|
c.state in [dBlockExplicitMapKey, dBlockMapValue,
|
|
dBlockImplicitMapKey, dBlockSequenceItem]:
|
|
indentation -= c.options.indentationStep
|
|
c.safeWrite('}')
|
|
of dBlockMapValue, dBlockInlineMap: discard
|
|
else: internalError("Invalid level: " & $level)
|
|
indentation -= c.options.indentationStep
|
|
of yamlEndDoc:
|
|
firstDoc = false
|
|
|
|
proc present*(s: var YamlStream, target: Stream,
|
|
options: PresentationOptions = defaultPresentationOptions)
|
|
{.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: var YamlStream,
|
|
options: PresentationOptions = defaultPresentationOptions):
|
|
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(c: var Context, input: Stream,
|
|
resolveToCoreYamlTags: bool) =
|
|
var parser: YamlParser
|
|
parser.init()
|
|
var events = parser.parse(input)
|
|
try:
|
|
if c.options.style == psCanonical:
|
|
var bys: YamlStream = newBufferYamlStream()
|
|
for e in events:
|
|
if resolveToCoreYamlTags:
|
|
var event = e
|
|
case event.kind
|
|
of yamlStartStream, yamlEndStream, yamlStartDoc, yamlEndDoc, yamlEndMap, yamlAlias, yamlEndSeq:
|
|
discard
|
|
of yamlStartMap:
|
|
if event.mapProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
|
|
event.mapProperties.tag = yTagMapping
|
|
of yamlStartSeq:
|
|
if event.seqProperties.tag in [yTagQuestionMark, yTagExclamationMark]:
|
|
event.seqProperties.tag = yTagSequence
|
|
of yamlScalar:
|
|
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)
|
|
else: BufferYamlStream(bys).put(e)
|
|
doPresent(c, bys)
|
|
else:
|
|
doPresent(c, events)
|
|
except YamlStreamError:
|
|
var e = getCurrentException()
|
|
while e.parent of YamlStreamError: e = e.parent
|
|
if e.parent of IOError: raise (ref IOError)(e.parent)
|
|
elif e.parent of OSError: raise (ref OSError)(e.parent)
|
|
elif e.parent of YamlParserError: raise (ref YamlParserError)(e.parent)
|
|
else: internalError("Unexpected exception: " & e.parent.repr)
|
|
|
|
proc genInput(input: Stream): Stream = input
|
|
proc genInput(input: string): Stream = newStringStream(input)
|
|
|
|
proc transform*(input: Stream | string, output: Stream,
|
|
options: PresentationOptions = defaultPresentationOptions,
|
|
resolveToCoreYamlTags: bool = false)
|
|
{.raises: [IOError, OSError, YamlParserError, YamlPresenterJsonError,
|
|
YamlPresenterOutputError].} =
|
|
## Parser ``input`` as YAML character stream and then dump it to ``output``
|
|
## while resolving non-specific tags to the ones in the YAML core tag
|
|
## library. 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 = defaultPresentationOptions,
|
|
resolveToCoreYamlTags: bool = false):
|
|
string {.raises: [IOError, OSError, YamlParserError, YamlPresenterJsonError,
|
|
YamlPresenterOutputError].} =
|
|
## Parser ``input`` as YAML character stream, resolves non-specific tags to
|
|
## the ones in the YAML core tag library, and then returns a serialized
|
|
## YAML string that represents the stream. 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 |