NimYAML/yaml/presenter.nim
Felix Krause 189844a72b Rewrote large parts of presenter
* PresentationOptions has gained more fine-tuning options:
   * directivesEnd: specifies when `---` is written. ref #135
   * containers: specifies whether containers use block or flow style
   * suppressAddrs: if set, suppresses output of attributes
   * quoting: specifies how strings should be quoted
   * condenseFlow: specifies whether flow sequences should be on a
     single line
   * explicitKeys: specifies whether mapping keys should always have '?'
 * PresentationStyle is now a list of presets that set
   multiple options in PresentationOptions.
 * Does not output trailing spaces anymore. ref #135
 * Writes compact notation, i.e. a mapping in a sequence starts on the
   line with the sequence's `-`, unless attributes are written
 * Added tests for the presenter
 * Existing code might change behavior because of whitespace, `---` and
   compact notation. The API has been extended so that existing code is
   affected as little as possible.
2023-07-31 19:16:24 +02:00

994 lines
37 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 std / [streams, deques, strutils, options]
import data, taglib, stream, private/internal, hints, parser
type
PresentationStyle* = enum
## Different styles for YAML character stream output.
## These are presets that set multiple options in PresentationOptions.
##
## - ``psMinimal``: Single-line flow-only output which tries to
## use as few characters as possible.
## - ``psCanonical``: Canonical YAML output. Writes all tags except
## for the non-specific tags ``?`` and ``!``, uses flow style, quotes
## all string scalars.
## Hint: To ensure full canonical style, you also need to give
## ``tagStyle = tsAll`` to dump() to generate specific tags for all
## nodes.
## - ``psDefault``: 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.
## - ``psJson``: 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.
## - ``psBlockOnly``: Formats all output in block style, does not use
## flow style at all.
psMinimal, psCanonical, psDefault, psJson, psBlockOnly
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
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 written. Values present at
## multiple places in the content that is serialized will be
## duplicated at every occurrence. If the content is cyclic, this
## will raise a YamlSerializationError.
## - ``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 that is 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).
## - ``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 directives3
deAlways, deIfNecessary, deNever
PresentationOptions* = object
## Options for generating a YAML character stream
containers*: ContainerStyle ## how mappings and sequences are presented
indentationStep*: int ## how many spaces a new level should be indented
newlines*: NewLineStyle ## kind of newline sequence to use
outputVersion*: OutputYamlVersion ## whether to write the %YAML tag
maxLineLength*: Option[int] ## max length of a line, including indentation
directivesEnd*: DirectivesEndStyle ## whether to write '---' after tags
suppressAttrs*: bool ## whether to suppress all attributes on nodes
quoting*: ScalarQuotingStyle ## how scalars are quoted
condenseFlow*: bool ## whether non-nested flow containers use a single line
explicitKeys*: bool ## 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
ScalarStyle = enum
sLiteral, sFolded, sPlain, sDoubleQuoted
Context = object
target: Stream
options: PresentationOptions
handles: seq[tuple[handle, uriPrefix: string]]
levels: seq[DumperLevel]
needsWhitespace: int
const
defaultPresentationOptions* =
PresentationOptions(containers: cMixed, indentationStep: 2,
newlines: nlOSDefault, maxLineLength: some(80),
directivesEnd: deIfNecessary, suppressAttrs: false,
quoting: sqUnset, condenseFlow: true,
explicitKeys: false)
proc `style=`*(po: var PresentationOptions, value: PresentationStyle) {.inline.} =
case value
of psMinimal:
po.newlines = nlNone
po.containers = cFlow
po.directivesEnd = deIfNecessary
po.suppressAttrs = false
po.quoting = sqJson
po.condenseFlow = true
po.explicitKeys = false
of psCanonical:
po.containers = cFlow
po.directivesEnd = deAlways
po.suppressAttrs = false
po.quoting = sqDouble
po.condenseFlow = false
po.explicitKeys = true
of psDefault:
po.containers = cMixed
po.directivesEnd = deIfNecessary
po.suppressAttrs = false
po.quoting = sqUnset
po.condenseFlow = true
po.explicitKeys = false
of psJson:
po.containers = cFlow
po.directivesEnd = deNever
po.suppressAttrs = true
po.quoting = sqJson
po.condenseFlow = false
po.explicitKeys = false
po.outputVersion = ovNone
of psBlockOnly:
po.containers = cBlock
po.directivesEnd = deIfNecessary
po.suppressAttrs = false
po.quoting = sqUnset
po.condenseFlow = true
po.explicitKeys = false
proc style*(po: PresentationOptions): PresentationStyle
{.deprecated: """
The style options are presets that set multiple fields now.
This getter recalculates the style that best matches the actual options.
You should rather query the option you care about directly.
""" .} =
if po.newlines == nlNone: return psMinimal
case po.containers
of cBlock: return psBlockOnly
of cMixed: discard
of cFlow:
case po.quoting
of sqJson:
if po.directivesEnd == deNever: return psJson
of sqDouble:
if not po.suppressAttrs: return psCanonical
else: discard
return psDefault
proc level(ctx: var Context): var DumperLevel = ctx.levels[^1]
proc level(ctx: Context): DumperLevel = ctx.levels[^1]
proc defineOptions*(style: PresentationStyle,
indentationStep: int = 2,
newlines: NewLineStyle = nlOSDefault,
outputVersion: OutputYamlVersion = ov1_2,
maxLineLength: Option[int] = some(80)):
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.
## This version of the proc uses a style argument, use the other version
## to fine-tune your settings.
result = PresentationOptions(
indentationStep: indentationStep,
newlines: newlines, outputVersion: outputVersion,
maxLineLength: maxLineLength)
result.style = style
proc defineOptions*(containers: ContainerStyle = cMixed,
indentationStep: int = 2,
newlines: NewLineStyle = nlOSDefault,
outputVersion: OutputYamlVersion = ov1_2,
maxLineLength: Option[int] = some(80),
directivesEnd: DirectivesEndStyle = deIfNecessary,
suppressAttrs: bool = false,
quoting: ScalarQuotingStyle = sqUnset,
condenseFlow: bool = true,
explicitKeys: bool = false):
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.
## This version of the proc allows you to fine-tune everything, use the
## other version to give a general style template.
result = PresentationOptions(
containers: containers,
indentationStep: indentationStep,
newlines: newlines, outputVersion: outputVersion,
maxLineLength: maxLineLength, directivesEnd: directivesEnd,
suppressAttrs: suppressAttrs, quoting: quoting,
condenseFlow: condenseFlow, explicitKeys: explicitKeys)
proc state(ctx: Context): DumperState = ctx.level.state
proc `state=`(ctx: var Context, v: DumperState) =
ctx.level.state = v
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]],
lineLength: Option[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
# line break. 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 lineLength.isSome and
curWord.finish - curWord.start + 1 > lineLength.get() - 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 lineLength.isSome and
curLine.finish - curLine.start + 1 > lineLength.get() - 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 lineLength.isSome and
curWord.finish - curWord.start + 1 > lineLength.get() - 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 lineLength.isSome and
curWord.finish - curWord.start + 1 > lineLength.get() - indentation:
return if canUsePlain: sPlain else: sDoubleQuoted
words.add(curWord)
if inLine:
curLine.finish = scalar.len - 1
if lineLength.isSome and
curLine.finish - curLine.start + 1 > lineLength.get() - indentation:
canUseLiteral = false
lines.add(curLine)
if lineLength.isNone or scalar.len <= lineLength.get() - indentation:
result = if canUsePlain: sPlain else: sDoubleQuoted
elif canUseLiteral: result = sLiteral
elif canUseFolded: result = sFolded
elif canUsePlain: result = sPlain
else: result = sDoubleQuoted
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)
{.raises: [YamlPresenterOutputError].} =
let indentation = ctx.indentation + ctx.options.indentationStep
var curPos = indentation
let t = ctx.target
try:
ctx.append('"')
curPos.inc()
for c in scalar:
if ctx.options.maxLineLength.isSome and
curPos == ctx.options.maxLineLength.get() - 1:
t.write('\\')
ctx.newline()
t.write(repeat(' ', indentation))
curPos = indentation
if c == ' ':
t.write('\\')
curPos.inc()
case c
of '"':
t.write("\\\"")
curPos.inc(2)
of '\l':
t.write("\\n")
curPos.inc(2)
of '\t':
t.write("\\t")
curPos.inc(2)
of '\\':
t.write("\\\\")
curPos.inc(2)
else:
if ord(c) < 32:
t.write("\\x" & toHex(ord(c), 2))
curPos.inc(4)
else:
t.write(c)
curPos.inc()
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)
{.raises: [YamlPresenterOutputError].} =
let t = ctx.target
try:
ctx.append('"')
for c in scalar:
case c
of '"': t.write("\\\"")
of '\\': t.write("\\\\")
of '\l': t.write("\\n")
of '\t': t.write("\\t")
of '\f': t.write("\\f")
of '\b': t.write("\\b")
else:
if ord(c) < 32: t.write("\\u" & toHex(ord(c), 4)) else: t.write(c)
t.write('"')
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].} =
let indentation = ctx.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, isObject: bool) {.raises: [YamlPresenterOutputError].} =
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 isObject 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 not isObject 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 not isObject 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.options.condenseFlow:
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 doPresent(ctx: var Context, s: YamlStream)
{.raises: [YamlPresenterJsonError, YamlPresenterOutputError,
YamlStreamError].} =
var
cached = initDeQue[Event]()
firstDoc = true
wroteDirectivesEnd = false
while true:
let item = nextItem(cached, s)
case item.kind
of yamlStartStream: discard
of yamlEndStream: break
of yamlStartDoc:
if not firstDoc:
ctx.safeWrite("...")
ctx.safeNewline()
wroteDirectivesEnd =
ctx.options.directivesEnd == deAlways or s.peek().hasSpecificTag()
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()
wroteDirectivesEnd = true
of ov1_1:
ctx.target.write("%YAML 1.1")
ctx.newline()
wroteDirectivesEnd = true
echo "write --- because %YAML"
of ovNone: discard
for v in ctx.handles:
if v.handle == "!":
if v.uriPrefix != "!":
ctx.target.write("%TAG ! " & v.uriPrefix)
ctx.newline()
wroteDirectivesEnd = true
elif v.handle == "!!":
if v.uriPrefix != yamlTagRepositoryPrefix:
ctx.target.write("%TAG !! " & v.uriPrefix)
ctx.newline()
wroteDirectivesEnd = true
else:
ctx.target.write("%TAG " & v.handle & ' ' & v.uriPrefix)
ctx.newline()
wroteDirectivesEnd = true
except CatchableError as ce:
var e = newException(YamlPresenterOutputError, "")
e.parent = ce
raise e
if wroteDirectivesEnd:
ctx.safeWrite("---")
ctx.whitespace(true)
of yamlScalar:
if ctx.levels.len > 0:
ctx.startItem(false)
discard ctx.writeTagAndAnchor(item.scalarProperties)
if ctx.levels.len == 0:
if wroteDirectivesEnd: ctx.safeNewline()
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.writeDoubleQuotedJson(item.scalarContent)
of sqDouble:
ctx.writeDoubleQuoted(item.scalarContent)
else:
var words, lines = newSeq[tuple[start, finish: int]]()
case item.scalarContent.inspect(
ctx.indentation + ctx.options.indentationStep, words, lines,
ctx.options.maxLineLength)
of sLiteral: ctx.writeLiteral(item.scalarContent, lines)
of sFolded: ctx.writeFolded(item.scalarContent, words)
of sPlain: ctx.safeWrite(item.scalarContent)
of sDoubleQuoted: ctx.writeDoubleQuoted(item.scalarContent)
of yamlAlias:
if ctx.options.quoting == sqJson:
raise newException(YamlPresenterJsonError,
"Alias not allowed in JSON output")
yAssert ctx.levels.len > 0
ctx.startItem(false)
try:
ctx.append('*')
ctx.target.write($item.aliasTarget)
except CatchableError as ce:
var e = newException(YamlPresenterOutputError, "")
e.parent = ce
raise e
of yamlStartSeq:
var nextState: DumperState
case ctx.options.containers
of cMixed:
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 cFlow: nextState = dFlowSequenceStart
of cBlock:
let next = s.peek()
if next.kind == yamlEndSeq: nextState = dFlowSequenceStart
else: nextState = dBlockSequenceItem
var indentation = 0
var singleLine = ctx.options.condenseFlow or ctx.options.newlines == nlNone
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(true)
indentation = ctx.indentation + ctx.options.indentationStep
let wroteAttrs = ctx.writeTagAndAnchor(item.seqProperties)
if wroteAttrs or (wroteDirectivesEnd and ctx.levels.len == 0):
wroteAnything = true
if nextState == dFlowSequenceStart:
if ctx.levels.len == 0:
if wroteAttrs or wroteDirectivesEnd: ctx.safeNewline()
ctx.safeWrite('[')
if ctx.levels.len > 0 and not ctx.options.condenseFlow and
ctx.state in [dBlockExplicitMapKey, dBlockMapValue,
dBlockImplicitMapKey, dBlockSequenceItem]:
indentation += ctx.options.indentationStep
if ctx.options.newlines != nlNone: singleLine = false
ctx.levels.add (nextState, indentation, singleLine, wroteAnything)
of yamlStartMap:
var nextState: DumperState
case ctx.options.containers
of cMixed:
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 cFlow: nextState = dFlowMapStart
of cBlock:
let next = s.peek()
if next.kind == yamlEndMap: nextState = dFlowMapStart
else: nextState = dBlockMapValue
var indentation = 0
var singleLine = ctx.options.condenseFlow or ctx.options.newlines == nlNone
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(true)
indentation = ctx.indentation + ctx.options.indentationStep
let wroteAttrs = ctx.writeTagAndAnchor(item.properties)
if wroteAttrs or (wroteDirectivesEnd and ctx.levels.len == 0):
wroteAnything = true
if nextState == dFlowMapStart:
if ctx.levels.len == 0:
if wroteAttrs or wroteDirectivesEnd: ctx.safeNewline()
ctx.safeWrite('{')
if ctx.levels.len > 0 and not ctx.options.condenseFlow and
ctx.state in [dBlockExplicitMapKey, dBlockMapValue,
dBlockImplicitMapKey, dBlockSequenceItem]:
indentation += ctx.options.indentationStep
if ctx.options.newlines != nlNone: singleLine = false
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(' ', ctx.indentation))
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(' ', ctx.indentation))
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:
firstDoc = false
ctx.safeNewline()
proc present*(s: 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: 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(ctx: var Context, input: Stream,
resolveToCoreYamlTags: bool) =
var parser: YamlParser
parser.init()
var events = parser.parse(input)
try:
if ctx.options.explicitKeys:
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(ctx, bys)
else:
doPresent(ctx, events)
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 = 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