Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# These files carry an intentional trailing space after a YAML block-scalar
# indicator ("mapping: | ") as a regression fixture for DOC-2433 - trimming
# it silently weakens the test (see fixture-yaml-trailing-space)
[{tests/bloblang-interactive/mini-playground-test.html,preview-src/bloblang-syntax-test.adoc}]
trim_trailing_whitespace = false
94 changes: 93 additions & 1 deletion preview-src/bloblang-syntax-test.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,96 @@ spec:
env:
- name: DATABASE_URL
value: "postgres://localhost:5432/mydb"
----
----

== Regression: Block Scalar Indicator Must Stay on Its Own Line

Reported against docs.redpanda.com as
https://redpandadata.atlassian.net/browse/DOC-2433[DOC-2433]: this exact
example (the Connect quickstart's producer pipeline) rendered as
`mapping: |let jokes = [` on one line instead of `|` followed by a line
break. Root cause was `extractBloblangFromBlock()` in
`src/js/17-bloblang-yaml.js` trimming away the newline + indentation
between the block-scalar indicator and the first line of Bloblang before
`processMultilineMapping()` discarded the token's original content
entirely.

**Check visually**: `mapping: |` must end its own line, with `let jokes`
starting the next line, indented — not glued onto the same line as `|`.
If this collapses again, the leading-whitespace preservation in
`processMultilineMapping()` has regressed.

[source,yaml]
----
input:
generate:
interval: 5s
count: 0
mapping: |
let jokes = [
"Why don't scientists trust atoms? Because they make up everything!",
"I'm reading a book about anti-gravity. It's impossible to put down!"
]
root = jokes.index(random_int(seed: timestamp_unix_nano(), max: jokes.length() - 1))
output:
redpanda:
topic: dad-jokes
----

== Regression: Literal Block Scalars Are Not Rewritten (DOC-2441)

https://redpandadata.atlassian.net/browse/DOC-2441[DOC-2441]: YAML performs
no quote or escape processing on literal block scalars, so the highlighter
must not either.

**Check visually and with the copy button**: `join("\n")` and `join("\t")`
must render (and copy) as two characters each - a backslash followed by a
letter - never as a real line break or tab inside the string literal.

[source,yaml]
----
pipeline:
processors:
- mapping: |
root.lines = this.values.join("\n")
root.tabbed = this.cols.join("\t")
----

**Check visually**: the first line's opening quote on `"prefix-"` and the
last line's closing quote on `"-suffix"` must both render. They belong to
two different string literals, and must not be stripped as if they enclosed
the whole block.

[source,yaml]
----
pipeline:
processors:
- mapping: |
"prefix-" + this.id + "-suffix"
----

**Check visually**: a trailing space after the `|` indicator must not glue
the first Bloblang line onto the `mapping:` line (the space after `|` below
is intentional).

[source,yaml]
----
pipeline:
processors:
- mapping: |
let doubled = this.value * 2
root.result = $doubled
----

**Check visually and with the copy button**: quoted flow-scalar mappings must
keep their quotes in the rendered output. The double-quoted example's escapes
are processed once: `\"` renders as `"` and `\\n` renders as the two
characters `\n` inside the string literal.

[source,yaml]
----
pipeline:
processors:
- mapping: "root.joined = this.parts.join(\"\\n\")"
- mapping: 'root.id = this.user_id.string()'
----
90 changes: 70 additions & 20 deletions src/js/17-bloblang-yaml.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,57 @@
}

/**
* Extract Bloblang code from a YAML literal block string
* Handles | and > block scalars
* Extract Bloblang code from a YAML scalar token's text.
*
* Returns { leading, code }:
* - leading: the whitespace trim() strips from the front of the token.
* For block scalars this is the newline + indentation that separates
* the | or > indicator from the first content line (the token can also
* begin with spaces or tabs, because the [ \t]* after the indicator is
* part of the scalar token in Prism's YAML grammar). It must be
* restored when the token is re-rendered, or "mapping: |" collapses
* onto the same line as the first Bloblang statement (DOC-2433).
* - code: the Bloblang source to tokenize.
*
* Block scalars (mapping: | ...) are literal source text: YAML performs
* no quote or escape processing on them, so neither does this function -
* rewriting \n inside them, or stripping a coincidental leading/trailing
* quote pair, corrupts the rendered and copied code (DOC-2441).
*
* Quoted flow scalars (mapping: "root = ...") are unwrapped and
* unescaped according to their quote style: double quotes process
* backslash escapes, single quotes only the '' escape. The stripped
* quote character is returned so the caller can re-render it - the
* quotes are part of the YAML source the reader sees and copies.
*/
function extractBloblangFromBlock(tokenText) {
// Remove leading/trailing quotes if present
function extractBloblangFromBlock(tokenText, isBlockScalar) {
var leading = tokenText.slice(0, tokenText.length - tokenText.trimStart().length)
var text = tokenText.trim()
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
text = text.slice(1, -1)
var quote = null

if (!isBlockScalar) {
var q = text.charAt(0)
if ((q === '"' || q === "'") && text.length >= 2 && text.endsWith(q)) {
quote = q
text = text.slice(1, -1)
if (q === '"') {
// Single pass so an escaped backslash can't feed a later rule
// (the old sequential replace chain turned \\n into \<newline>).
// Escapes this display code doesn't translate stay verbatim
// rather than losing their backslash (\r must not become r).
text = text.replace(/\\(.)/g, function (match, ch) {
if (ch === 'n') return '\n'
if (ch === 't') return '\t'
if (ch === '"' || ch === "'" || ch === '\\' || ch === '/') return ch
return match
})
} else {
text = text.replace(/''/g, "'")
}
}
}

// Handle escape sequences
text = text
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\')

return text
return { leading: leading, code: text, quote: quote }
}

/**
Expand Down Expand Up @@ -303,10 +335,20 @@
* Also handles continuation content after blank lines
*/
function processMultilineMapping(token) {
var bloblangCode = extractBloblangFromBlock(token.textContent)

// Check for continuation content after this token
var continuationNodes = collectLiteralBlockContinuation(token)
var rawText = token.textContent
// Prism's YAML grammar gives block scalars the "scalar" class ("string"
// is only an alias on them); a token with "string" alone is a quoted
// flow scalar, whose quotes/escapes YAML actually processes.
var isBlockScalar = token.classList.contains('scalar')
var extracted = extractBloblangFromBlock(rawText, isBlockScalar)
var bloblangCode = extracted.code
var leadingWhitespace = extracted.leading

// Check for continuation content after this token. Only block scalars
// can have it (Prism splits them at blank lines); a quoted flow scalar
// is single-line by grammar, and collecting "continuation" for one
// swallows sibling keys that sit at or above the fallback base indent.
var continuationNodes = isBlockScalar ? collectLiteralBlockContinuation(token) : []
var continuationText = extractTextFromNodes(continuationNodes)

// Combine the token content with continuation
Expand All @@ -323,7 +365,15 @@

var wrapper = document.createElement('span')
wrapper.className = 'bloblang-embedded'
wrapper.innerHTML = highlighted
// Re-render the quotes stripped from a quoted flow scalar (same as
// processSingleLineCheck) so the reader still sees them and the copy
// button keeps them.
var quoteHtml = extracted.quote ? '<span class="token punctuation">' + extracted.quote + '</span>' : ''
// leadingWhitespace is whitespace-only (what trim() stripped), so it is
// HTML-inert and safe to concatenate unescaped. Asymmetry note: trailing
// whitespace on the scalar's last content line is still dropped by the
// trim - invisible in the render, observable only via the copy button.
wrapper.innerHTML = leadingWhitespace + quoteHtml + highlighted + quoteHtml

token.innerHTML = ''
token.appendChild(wrapper)
Expand Down
Loading
Loading