From a3a70eb93b451c14422784b5249c89ddc4377692 Mon Sep 17 00:00:00 2001 From: MaximAL Date: Thu, 20 Aug 2026 15:35:00 +0300 Subject: [PATCH 1/4] Change comments in line protocol syntax to be self-descriptive `#` Similar to #5821. --- content/shared/v3-line-protocol.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/shared/v3-line-protocol.md b/content/shared/v3-line-protocol.md index 4153ee1954..57319ae6f4 100644 --- a/content/shared/v3-line-protocol.md +++ b/content/shared/v3-line-protocol.md @@ -11,11 +11,11 @@ timestamp of a data point. - [Naming restrictions](#naming-restrictions) - [Duplicate points](#duplicate-points) -```js -// Syntax +```python +# Syntax [,=[,=]] =[,=] [] -// Example +# Example myTable,tag1=value1,tag2=value2 fieldKey="fieldValue" 1556813561098000000 ``` From d46865f6f251ae06ea5086a979b342965adc374a Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Tue, 25 Aug 2026 13:13:41 -0500 Subject: [PATCH 2/4] fix(content): use text+lp fences for line protocol examples #7678 and #5821 fixed the misleading `js`/`//` comment syntax in the line protocol "Syntax" block, but relabeled it `python`, which is still not what the block is (a placeholder grammar, not runnable Python or JS). Split each occurrence into a `text` fence for the placeholder syntax and a standalone `lp` fence for the real example, matching this repo's line-protocol fence convention. Also fixed the several other `js`/`sh`-tagged blocks in the same two shared files that are real, standalone line protocol (not templates) to use `lp`, since they were failing codeblock lint as invalid JavaScript. PR #5821 targeted content/influxdb/v2/reference/syntax/line-protocol.md, which is now a stub whose body is sourced from content/shared/influxdb-v2/reference/syntax/line-protocol.md; applied its intent to the shared file instead. Co-authored-by: MaximAL --- .../v1/guides/write_data.md | 7 +++--- .../reference/syntax/line-protocol.md | 25 ++++++++++--------- content/shared/v3-line-protocol.md | 23 +++++++++-------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/content/enterprise_influxdb/v1/guides/write_data.md b/content/enterprise_influxdb/v1/guides/write_data.md index 889e0936f8..f2e01dc783 100644 --- a/content/enterprise_influxdb/v1/guides/write_data.md +++ b/content/enterprise_influxdb/v1/guides/write_data.md @@ -60,11 +60,12 @@ The InfluxDB API is the primary means of writing data into InfluxDB. The preceding examples use a curl `--data-` option to include a POST request body that contains [InfluxDB line protocol](/enterprise_influxdb/v1/concepts/glossary/#influxdb-line-protocol) for the time series data that you want to store. -```js -// Syntax +```text +# Syntax [,=[,=]] =[,=] [] +``` -// Example +```lp cpu_load_short,host=server01,region=us-west value=0.64 1434055562000000000 ``` diff --git a/content/shared/influxdb-v2/reference/syntax/line-protocol.md b/content/shared/influxdb-v2/reference/syntax/line-protocol.md index 711886f265..ec9fab48d1 100644 --- a/content/shared/influxdb-v2/reference/syntax/line-protocol.md +++ b/content/shared/influxdb-v2/reference/syntax/line-protocol.md @@ -10,11 +10,12 @@ It is a text-based format that provides the measurement, tag set, field set, and - [Naming restrictions](#naming-restrictions) - [Duplicate points](#duplicate-points) -```js -// Syntax +```text +# Syntax [,=[,=]] =[,=] [] +``` -// Example +```lp myMeasurement,tag1=value1,tag2=value2 fieldKey="fieldValue" 1556813561098000000 ``` @@ -63,7 +64,7 @@ _**Value data type:** [Float](#float) | [Integer](#integer) | [UInteger](#uinteg {{% note %}} _Always double quote string field values. More on quotes [below](#quotes)._ -```sh +```lp measurementName fieldKey="field string value" 1556813561098000000 ``` {{% /note %}} @@ -99,7 +100,7 @@ Default numerical type. _InfluxDB supports scientific notation in float field values._ ##### Float field value examples -```js +```lp myMeasurement fieldKey=1.0 myMeasurement fieldKey=1 myMeasurement fieldKey=-1.234456e+78 @@ -114,7 +115,7 @@ Trailing `i` on the number specifies an integer. | `-9223372036854775808i` | `9223372036854775807i` | ##### Integer field value examples -```js +```lp myMeasurement fieldKey=1i myMeasurement fieldKey=12485903i myMeasurement fieldKey=-12485903i @@ -129,7 +130,7 @@ Trailing `u` on the number specifies an unsigned integer. | `0u` | `18446744073709551615u` | ##### UInteger field value examples -```js +```lp myMeasurement fieldKey=1u myMeasurement fieldKey=12485903u ``` @@ -139,7 +140,7 @@ Plain text string. Length limit 64KB. ##### String example -```sh +```lp # String measurement name, field key, and field value myMeasurement fieldKey="this is a string" ``` @@ -153,7 +154,7 @@ Stores `true` or `false` values. | False | `f`, `F`, `false`, `False`, `FALSE` | ##### Boolean field value examples -```js +```lp myMeasurement fieldKey=true myMeasurement fieldKey=false myMeasurement fieldKey=t @@ -176,7 +177,7 @@ Default precision is nanoseconds (`ns`). | `-9223372036854775806` | `9223372036854775806` | ##### Unix timestamp example -```js +```lp myMeasurementName fieldKey="fieldValue" 1556813561098000000 ``` @@ -211,7 +212,7 @@ In the following contexts, it requires escaping certain characters with a backsl You do not need to escape other special characters. ##### Examples of special characters in line protocol -```sh +```lp # Measurement name with spaces my\ Measurement fieldKey="string value" @@ -243,7 +244,7 @@ For example: Line protocol interprets `#` at the beginning of a line as a comment character and ignores all subsequent characters until the next newline `\n`. -```sh +```lp # This is a comment myMeasurement fieldKey="string value" 1556813561098000000 ``` diff --git a/content/shared/v3-line-protocol.md b/content/shared/v3-line-protocol.md index 57319ae6f4..d7a5254d8d 100644 --- a/content/shared/v3-line-protocol.md +++ b/content/shared/v3-line-protocol.md @@ -11,11 +11,12 @@ timestamp of a data point. - [Naming restrictions](#naming-restrictions) - [Duplicate points](#duplicate-points) -```python +```text # Syntax
[,=[,=]] =[,=] [] +``` -# Example +```lp myTable,tag1=value1,tag2=value2 fieldKey="fieldValue" 1556813561098000000 ``` @@ -69,7 +70,7 @@ _**Value data type:** [Float](#float) | [Integer](#integer) | [UInteger](#uinteg > [!Note] > _Always double quote string field values. More on quotes [below](#quotes)._ > -> ```sh +> ```lp > tableName fieldKey="field string value" 1556813561098000000 > ``` @@ -108,7 +109,7 @@ _InfluxDB supports scientific notation in float field values._ ##### Float field value examples -```js +```lp myTable fieldKey=1.0 myTable fieldKey=1 myTable fieldKey=-1.234456e+78 @@ -125,7 +126,7 @@ Trailing `i` on the number specifies an integer. ##### Integer field value examples -```js +```lp myTable fieldKey=1i myTable fieldKey=12485903i myTable fieldKey=-12485903i @@ -142,7 +143,7 @@ Trailing `u` on the number specifies an unsigned integer. ##### UInteger field value examples -```js +```lp myTable fieldKey=1u myTable fieldKey=12485903u ``` @@ -158,7 +159,7 @@ Plain text string. ##### String example -```sh +```lp # String table name, field key, and field value myTable fieldKey="this is a string" ``` @@ -174,7 +175,7 @@ Stores `true` or `false` values. ##### Boolean field value examples -```js +```lp myTable fieldKey=true myTable fieldKey=false myTable fieldKey=t @@ -198,7 +199,7 @@ Default precision is nanoseconds (`ns`). ##### Unix timestamp example -```js +```lp myTableName fieldKey="fieldValue" 1556813561098000000 ``` @@ -236,7 +237,7 @@ You do not need to escape other special characters. ##### Examples of special characters in line protocol -```sh +```lp # Table name with spaces my\ Table fieldKey="string value" @@ -270,7 +271,7 @@ For example: Line protocol interprets `#` at the beginning of a line as a comment character and ignores all subsequent characters until the next newline `\n`. -```sh +```lp # This is a comment myTable fieldKey="string value" 1556813561098000000 ``` From fd05e594b206235bd3d4a2f54a5c82da19ec5ee4 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Tue, 25 Aug 2026 14:47:36 -0500 Subject: [PATCH 3/4] feat(line-protocol): add fences highlighting and validation What changed - Add a Hugo lp render hook with field-family highlighting and safe malformed-input fallback. - Add a blocking lp code-block validator and unit, extractor, CLI, and Cypress coverage. - Document lp fence validation and regenerate instruction adapters. Why Line protocol examples need syntax-aware rendering and CI validation while preserving escaped source text. Impact The linter now fails invalid lp fences unless they explicitly use lint="false". Verification - yarn test:lint-codeblocks - yarn test:render-regression - yarn build:agent:instructions - yarn validate:agent-instructions --- .agents/instructions/content-review.md | 2 + .agents/instructions/content.md | 8 + .agents/instructions/layouts.md | 7 + .agents/skills/content-editing/SKILL.md | 2 +- .agents/skills/docs-testing/SKILL.md | 35 +-- .claude/rules/content-review.md | 2 + .claude/rules/content.md | 8 + .claude/rules/layouts.md | 7 + .../content-review.instructions.md | 2 + .github/instructions/content.instructions.md | 8 + .github/instructions/layouts.instructions.md | 7 + DOCS-TESTING.md | 8 +- content/AGENTS.md | 10 + content/example.md | 16 ++ cypress/e2e/content/render-regression.cy.js | 29 +++ layouts/AGENTS.md | 7 + .../_default/_markup/render-codeblock-lp.html | 1 + layouts/partials/lp/highlight.html | 16 ++ scripts/ci/__tests__/extractor.test.mjs | 5 + scripts/ci/__tests__/lint-codeblocks.test.mjs | 10 + scripts/ci/__tests__/validators/lp.test.mjs | 42 ++++ scripts/ci/lint-codeblocks.mjs | 2 +- scripts/lib/codeblock-extractor.mjs | 1 + scripts/lib/codeblock-normalizer.mjs | 2 + scripts/lib/codeblock-validators/lp.mjs | 204 ++++++++++++++++++ 25 files changed, 423 insertions(+), 18 deletions(-) create mode 100644 layouts/_default/_markup/render-codeblock-lp.html create mode 100644 layouts/partials/lp/highlight.html create mode 100644 scripts/ci/__tests__/validators/lp.test.mjs create mode 100644 scripts/lib/codeblock-validators/lp.mjs diff --git a/.agents/instructions/content-review.md b/.agents/instructions/content-review.md index 6e4006bf82..d742627de2 100644 --- a/.agents/instructions/content-review.md +++ b/.agents/instructions/content-review.md @@ -60,6 +60,8 @@ Products defined in [data/products.yml](../../data/products.yml): ## Code Blocks - Use `python` not `py` for language identifiers (pytest requirement) +- Use `lp` for line protocol examples. `lp` fences are blocking syntax checks; + use `{lint="false"}` only for examples that intentionally show invalid input. - Long options in CLI examples (`--output` not `-o`) - Keep lines within 80 characters - Include language identifier on fenced code blocks diff --git a/.agents/instructions/content.md b/.agents/instructions/content.md index 2e120b9e83..e533bf4928 100644 --- a/.agents/instructions/content.md +++ b/.agents/instructions/content.md @@ -122,6 +122,14 @@ yarn test:codeblocks:all For comprehensive testing workflows, see [content-editing skill](../skills/content-editing/SKILL.md). +### Line protocol fences + +Use `lp` for InfluxDB line protocol examples. +The code-block linter validates `lp` fences and blocks malformed syntax in CI. +Qualified field keys use `family::field`; only the first `::` identifies the +family delimiter, so later `::` sequences remain part of the field name. +For an intentionally invalid example, add `{lint="false"}` to the fence. + ## Style Guidelines - Use semantic line feeds (one sentence per line) diff --git a/.agents/instructions/layouts.md b/.agents/instructions/layouts.md index 6a18b77794..63c276bf5c 100644 --- a/.agents/instructions/layouts.md +++ b/.agents/instructions/layouts.md @@ -117,6 +117,13 @@ Add shortcode usage examples to `content/example.md` to verify: See [DOCS-SHORTCODES.md](../../DOCS-SHORTCODES.md) for complete shortcode documentation. +### Line protocol render hook + +`layouts/_default/_markup/render-codeblock-lp.html` renders `lp` code fences. +Keep its output Chroma-compatible (`.highlight > pre.chroma > code.language-lp`) +and HTML-escape source text before marking generated markup safe. +For malformed source, render escaped plain text rather than partial highlighting. + ## Related Resources - **Complete Hugo template workflow**: diff --git a/.agents/skills/content-editing/SKILL.md b/.agents/skills/content-editing/SKILL.md index c593c42045..a309647642 100644 --- a/.agents/skills/content-editing/SKILL.md +++ b/.agents/skills/content-editing/SKILL.md @@ -205,7 +205,7 @@ yarn test:lint-codeblocks | Language | On failure | | ------------------------ | ---------------------------------- | -| JSON, YAML, TOML | `::error::` — fails the PR check | +| JSON, YAML, TOML, LP | `::error::` — fails the PR check | | bash, python, javascript | `::warning::` — informational only | **Normalization:** declared `placeholders="TOKEN|DURATION"` fence attributes and Hugo shortcodes (`{{< >}}`, `{{% %}}`) are substituted before parsing. See `DOCS-TESTING.md § "Parse/compile code-block lint"` for details. diff --git a/.agents/skills/docs-testing/SKILL.md b/.agents/skills/docs-testing/SKILL.md index 0cf4bda0f8..193153d9db 100644 --- a/.agents/skills/docs-testing/SKILL.md +++ b/.agents/skills/docs-testing/SKILL.md @@ -58,20 +58,20 @@ Code block execution tests are **disabled** in pre-push hooks. Run them manually ### CI checks on every PR -| Workflow | What it checks | Blocks merge? | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -| `pr-vale-check.yml` | Vale on changed markdown + shared content | Errors only | -| `pr-link-check.yml` | Links in changed pages (also download/install pages when `data/products.yml` changes) | Warnings only | -| `pr-release-check.yml` | Reminds to bump `data/products.yml` when release notes advance; on a version bump, reminds to confirm download artifacts are published | No (reminders only) | -| `test.yml` (lint-codeblocks job) | Parse/compile check on changed content | JSON/YAML/TOML errors only | -| `pr-render-check.yml` | Whitespace-escaped code blocks, Cypress render | Yes (render artifacts) | -| `pr-remark-check.yml` | Remark lint on repo docs | No | -| `pr-ai-artifacts-check.yml` | Markdown twins, llms-full corpora, JSON-LD `@id` references (full site build) | Yes | -| `block-ephemeral-docs.yml` | Blocks PLAN.md and HANDOVER.md on master | Yes | -| `pr-feedback-links.yml` | Rendered feedback link validation | Warnings only | -| `pr-lockfile-lint.yml` | yarn.lock integrity | Yes | -| `auto-label.yml` | Applies product labels | No | -| `pr-preview.yml` | Deploys a full-site preview to staging S3 | No | +| Workflow | What it checks | Blocks merge? | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| `pr-vale-check.yml` | Vale on changed markdown + shared content | Errors only | +| `pr-link-check.yml` | Links in changed pages (also download/install pages when `data/products.yml` changes) | Warnings only | +| `pr-release-check.yml` | Reminds to bump `data/products.yml` when release notes advance; on a version bump, reminds to confirm download artifacts are published | No (reminders only) | +| `test.yml` (lint-codeblocks job) | Parse/compile check on changed content | JSON/YAML/TOML/LP errors fail | +| `pr-render-check.yml` | Whitespace-escaped code blocks, Cypress render | Yes (render artifacts) | +| `pr-remark-check.yml` | Remark lint on repo docs | No | +| `pr-ai-artifacts-check.yml` | Markdown twins, llms-full corpora, JSON-LD `@id` references (full site build) | Yes | +| `block-ephemeral-docs.yml` | Blocks PLAN.md and HANDOVER.md on master | Yes | +| `pr-feedback-links.yml` | Rendered feedback link validation | Warnings only | +| `pr-lockfile-lint.yml` | yarn.lock integrity | Yes | +| `auto-label.yml` | Applies product labels | No | +| `pr-preview.yml` | Deploys a full-site preview to staging S3 | No | Code block **execution** is NOT a PR check. It runs on demand via `workflow_dispatch`. @@ -107,7 +107,12 @@ yarn lint-codeblocks:pretty content/**/*.md yarn test:lint-codeblocks ``` -Exit code 1 if any JSON/YAML/TOML block fails to parse. bash/python/JS failures are warnings only. +Exit code 1 if any JSON/YAML/TOML/LP block fails to parse. +bash/python/JS failures are warnings only. + +`lp` validates InfluxDB line protocol, including qualified field keys such as +`family::field`. +Use `{lint="false"}` only for intentionally invalid examples. Linter normalizes `{ placeholders="..." }` fence attributes and strips Hugo shortcodes inside fences before parsing. diff --git a/.claude/rules/content-review.md b/.claude/rules/content-review.md index 06cf471a34..63492d4a13 100644 --- a/.claude/rules/content-review.md +++ b/.claude/rules/content-review.md @@ -62,6 +62,8 @@ Products defined in [data/products.yml](../../data/products.yml): ## Code Blocks - Use `python` not `py` for language identifiers (pytest requirement) +- Use `lp` for line protocol examples. `lp` fences are blocking syntax checks; + use `{lint="false"}` only for examples that intentionally show invalid input. - Long options in CLI examples (`--output` not `-o`) - Keep lines within 80 characters - Include language identifier on fenced code blocks diff --git a/.claude/rules/content.md b/.claude/rules/content.md index e1856ebc0d..ef13e4226d 100644 --- a/.claude/rules/content.md +++ b/.claude/rules/content.md @@ -124,6 +124,14 @@ yarn test:codeblocks:all For comprehensive testing workflows, see [content-editing skill](../../.agents/skills/content-editing/SKILL.md). +### Line protocol fences + +Use `lp` for InfluxDB line protocol examples. +The code-block linter validates `lp` fences and blocks malformed syntax in CI. +Qualified field keys use `family::field`; only the first `::` identifies the +family delimiter, so later `::` sequences remain part of the field name. +For an intentionally invalid example, add `{lint="false"}` to the fence. + ## Style Guidelines - Use semantic line feeds (one sentence per line) diff --git a/.claude/rules/layouts.md b/.claude/rules/layouts.md index ce0b0ebea7..56efe3e68c 100644 --- a/.claude/rules/layouts.md +++ b/.claude/rules/layouts.md @@ -119,6 +119,13 @@ Add shortcode usage examples to `content/example.md` to verify: See [DOCS-SHORTCODES.md](../../DOCS-SHORTCODES.md) for complete shortcode documentation. +### Line protocol render hook + +`layouts/_default/_markup/render-codeblock-lp.html` renders `lp` code fences. +Keep its output Chroma-compatible (`.highlight > pre.chroma > code.language-lp`) +and HTML-escape source text before marking generated markup safe. +For malformed source, render escaped plain text rather than partial highlighting. + ## Related Resources - **Complete Hugo template workflow**: diff --git a/.github/instructions/content-review.instructions.md b/.github/instructions/content-review.instructions.md index ee7fff42f6..8c6ee66ab6 100644 --- a/.github/instructions/content-review.instructions.md +++ b/.github/instructions/content-review.instructions.md @@ -61,6 +61,8 @@ Products defined in [data/products.yml](../../data/products.yml): ## Code Blocks - Use `python` not `py` for language identifiers (pytest requirement) +- Use `lp` for line protocol examples. `lp` fences are blocking syntax checks; + use `{lint="false"}` only for examples that intentionally show invalid input. - Long options in CLI examples (`--output` not `-o`) - Keep lines within 80 characters - Include language identifier on fenced code blocks diff --git a/.github/instructions/content.instructions.md b/.github/instructions/content.instructions.md index d121279192..395154e96b 100644 --- a/.github/instructions/content.instructions.md +++ b/.github/instructions/content.instructions.md @@ -123,6 +123,14 @@ yarn test:codeblocks:all For comprehensive testing workflows, see [content-editing skill](../../.agents/skills/content-editing/SKILL.md). +### Line protocol fences + +Use `lp` for InfluxDB line protocol examples. +The code-block linter validates `lp` fences and blocks malformed syntax in CI. +Qualified field keys use `family::field`; only the first `::` identifies the +family delimiter, so later `::` sequences remain part of the field name. +For an intentionally invalid example, add `{lint="false"}` to the fence. + ## Style Guidelines - Use semantic line feeds (one sentence per line) diff --git a/.github/instructions/layouts.instructions.md b/.github/instructions/layouts.instructions.md index 7921707bfc..1bf29e2032 100644 --- a/.github/instructions/layouts.instructions.md +++ b/.github/instructions/layouts.instructions.md @@ -118,6 +118,13 @@ Add shortcode usage examples to `content/example.md` to verify: See [DOCS-SHORTCODES.md](../../DOCS-SHORTCODES.md) for complete shortcode documentation. +### Line protocol render hook + +`layouts/_default/_markup/render-codeblock-lp.html` renders `lp` code fences. +Keep its output Chroma-compatible (`.highlight > pre.chroma > code.language-lp`) +and HTML-escape source text before marking generated markup safe. +For malformed source, render escaped plain text rather than partial highlighting. + ## Related Resources - **Complete Hugo template workflow**: diff --git a/DOCS-TESTING.md b/DOCS-TESTING.md index d176d47013..be3b49a56d 100644 --- a/DOCS-TESTING.md +++ b/DOCS-TESTING.md @@ -138,11 +138,17 @@ yarn test:lint-codeblocks | Language | Policy on parse failure | | ------------------------ | ------------------------------------------ | -| JSON, YAML, TOML | `::error::` — fails the PR check | +| JSON, YAML, TOML, LP | `::error::` — fails the PR check | | bash, python, javascript | `::warning::` — does not fail the PR check | SQL, InfluxQL, Go, and other languages are not yet checked. +`lp` fences validate InfluxDB line protocol, including qualified field keys such +as `family::field`. +The validator accepts a single family delimiter and treats later `::` sequences +as part of the field name. +Use `{lint="false"}` for intentionally invalid line protocol examples. + **Normalization**: The linter handles common docs patterns: - `{ placeholders="TOKEN_NAME|DURATION" }` fence attributes — tokens get language-safe substitutions before parsing diff --git a/content/AGENTS.md b/content/AGENTS.md index 7f82087296..eb510356e1 100644 --- a/content/AGENTS.md +++ b/content/AGENTS.md @@ -123,6 +123,14 @@ yarn test:codeblocks:all For comprehensive testing workflows, see [content-editing skill](../.agents/skills/content-editing/SKILL.md). +#### Line protocol fences + +Use `lp` for InfluxDB line protocol examples. +The code-block linter validates `lp` fences and blocks malformed syntax in CI. +Qualified field keys use `family::field`; only the first `::` identifies the +family delimiter, so later `::` sequences remain part of the field name. +For an intentionally invalid example, add `{lint="false"}` to the fence. + ### Style Guidelines - Use semantic line feeds (one sentence per line) @@ -262,6 +270,8 @@ Products defined in [data/products.yml](../data/products.yml): ### Code Blocks - Use `python` not `py` for language identifiers (pytest requirement) +- Use `lp` for line protocol examples. `lp` fences are blocking syntax checks; + use `{lint="false"}` only for examples that intentionally show invalid input. - Long options in CLI examples (`--output` not `-o`) - Keep lines within 80 characters - Include language identifier on fenced code blocks diff --git a/content/example.md b/content/example.md index 84d07aca3b..738db4664c 100644 --- a/content/example.md +++ b/content/example.md @@ -17,6 +17,22 @@ This is a paragraph. Lorem ipsum dolor ({{< icon "trash" "v2" >}}) sit amet, con This is **bold** text. This is *italic* text. This is ***bold and italic***. +### Line protocol fence + +```lp +# A comment remains source text. +cpu,host=west usage=42.5,active=true 1700000000000000000 +cpu cpu::user=12.5,cpu::system=2.5,mem::used=1.2,plain=3.4 +table a::b::c="quoted-value",status="ready" +東京,tag=値 field="Unicode-value" +``` + +The malformed fence falls back to escaped plain text instead of partial markup: + +```lp {lint="false"} +cpu field="unterminated +``` + ### Clockface v2 icons {{< nav-icon "account" "v2" >}} diff --git a/cypress/e2e/content/render-regression.cy.js b/cypress/e2e/content/render-regression.cy.js index 9140b8768e..950ed3a360 100644 --- a/cypress/e2e/content/render-regression.cy.js +++ b/cypress/e2e/content/render-regression.cy.js @@ -84,6 +84,35 @@ describe('Shortcode examples page', () => { assertHasHighlightedCodeBlock(); assertNoEscapedHighlightMarkup(); }); + + it('highlights line protocol field families without changing source text', () => { + cy.visit('/example/'); + cy.contains('h3', 'Line protocol fence') + .nextUntil('h3') + .find('code.language-lp') + .first() + .as('lineProtocol'); + + cy.get('@lineProtocol').should('contain.text', 'a::b::c="quoted-value"'); + cy.get('@lineProtocol').find('.nx').contains('cpu').should('exist'); + cy.get('@lineProtocol') + .find('.lp-family-delimiter') + .should('have.length', 4) + .each(($delimiter) => expect($delimiter).to.have.text('::')); + cy.get('@lineProtocol') + .find('.na') + .contains('b::c') + .should('exist'); + cy.get('@lineProtocol').find('.s').contains('Unicode-value').should('exist'); + }); + + it('uses escaped plain text for malformed line protocol', () => { + cy.visit('/example/'); + cy.contains('The malformed fence falls back') + .next('div.highlight') + .find('code.language-lp') + .should('contain.text', 'cpu field="unterminated'); + }); }); describe('Representative product pages', () => { diff --git a/layouts/AGENTS.md b/layouts/AGENTS.md index 10d686e719..5d65f7a471 100644 --- a/layouts/AGENTS.md +++ b/layouts/AGENTS.md @@ -118,6 +118,13 @@ Add shortcode usage examples to `content/example.md` to verify: See [DOCS-SHORTCODES.md](../DOCS-SHORTCODES.md) for complete shortcode documentation. +#### Line protocol render hook + +`layouts/_default/_markup/render-codeblock-lp.html` renders `lp` code fences. +Keep its output Chroma-compatible (`.highlight > pre.chroma > code.language-lp`) +and HTML-escape source text before marking generated markup safe. +For malformed source, render escaped plain text rather than partial highlighting. + ### Related Resources - **Complete Hugo template workflow**: diff --git a/layouts/_default/_markup/render-codeblock-lp.html b/layouts/_default/_markup/render-codeblock-lp.html new file mode 100644 index 0000000000..7ab22d0038 --- /dev/null +++ b/layouts/_default/_markup/render-codeblock-lp.html @@ -0,0 +1 @@ +{{- partial "lp/highlight.html" . -}} diff --git a/layouts/partials/lp/highlight.html b/layouts/partials/lp/highlight.html new file mode 100644 index 0000000000..74f1f84cca --- /dev/null +++ b/layouts/partials/lp/highlight.html @@ -0,0 +1,16 @@ +{{- $valid := true -}} +{{- range split .Inner "\n" -}} + {{- $quotes := len (findRE `"` (. | htmlEscape)) -}} + {{- if ne (mod $quotes 2) 0 -}} + {{- $valid = false -}} + {{- end -}} +{{- end -}} +{{- $code := .Inner | htmlEscape -}} +{{- if $valid -}} + {{- /* Apply replacements to escaped source, then mark only the generated + markup safe. An unterminated string falls back to plain text. */ -}} + {{- $code = replaceRE `([[:alnum:]_.-]+)::([^=,[:space:]]+)(=)` `$1::$2$3` $code -}} + {{- $code = replaceRE `"[^"\n]*"` `$0` $code -}} + {{- $code = replaceRE `(?m)^#.*$` `$0` $code -}} +{{- end -}} +{{- printf "
%s
" $code | safeHTML -}} diff --git a/scripts/ci/__tests__/extractor.test.mjs b/scripts/ci/__tests__/extractor.test.mjs index 88566e2c99..ce5608ad8e 100644 --- a/scripts/ci/__tests__/extractor.test.mjs +++ b/scripts/ci/__tests__/extractor.test.mjs @@ -26,6 +26,11 @@ test('normalizes language aliases to canonical keys', () => { assert.deepEqual(langs, ['bash', 'python', 'yaml', null]); }); +test('recognizes lp as a canonical language', () => { + const [block] = extractCodeBlocks('```lp\ncpu usage=1.0\n```\n'); + assert.equal(block.lang, 'lp'); +}); + test('flags unsupported langs as null (out of scope)', () => { const blocks = extractCodeBlocks(fx('aliases.md')); assert.equal(blocks[3].lang, null); diff --git a/scripts/ci/__tests__/lint-codeblocks.test.mjs b/scripts/ci/__tests__/lint-codeblocks.test.mjs index 43af9142f4..063fc7583e 100644 --- a/scripts/ci/__tests__/lint-codeblocks.test.mjs +++ b/scripts/ci/__tests__/lint-codeblocks.test.mjs @@ -28,6 +28,16 @@ test('exits 1 when a JSON block fails to parse', () => { assert.match(r.stdout + r.stderr, /bad-json\.md/); }); +test('fails blocking lp fences and maps diagnostics to Markdown lines', () => { + const dir = mkdtempSync(join(tmpdir(), 'lint-lp-')); + const file = join(dir, 'invalid-lp.md'); + writeFileSync(file, '# Example\n\n```lp\ncpu usage=1.0\ncpu field=NaN\n```\n'); + const r = run([file]); + assert.equal(r.status, 1); + assert.match(r.stdout, /line=5/); + assert.match(r.stdout, /lp: invalid field value/); +}); + test('dedupes inputs that resolve to the same canonical source', () => { // Pass the same consumer file twice — canonical should be grouped once. const consumer = fx('consumer.md'); diff --git a/scripts/ci/__tests__/validators/lp.test.mjs b/scripts/ci/__tests__/validators/lp.test.mjs new file mode 100644 index 0000000000..9f91209aeb --- /dev/null +++ b/scripts/ci/__tests__/validators/lp.test.mjs @@ -0,0 +1,42 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { validate } from '../../../lib/codeblock-validators/lp.mjs'; + +test('accepts standard and qualified line protocol fields', () => { + const code = [ + '# a comment', + 'cpu,host=west usage=42.5,active=true 1700000000000000000', + 'weather temp=72i,humidity=45u,status="sunny",whole=1', + 'cpu cpu::user=12.5,cpu::system=2.5,mem::used=1.2,plain=3.4', + 'table a::b::c="value",escaped\\ key=1.0', + '東京,tag=値 field="quoted, value"', + ].join('\n'); + const result = validate(code); + assert.equal(result.ok, true, JSON.stringify(result.errors)); +}); + +test('accepts signed integer and timestamp boundaries', () => { + const result = validate([ + 'm signed=-9223372036854775808i,unsigned=18446744073709551615u -9223372036854775808', + 'm signed=9223372036854775807i 9223372036854775807', + ].join('\n')); + assert.equal(result.ok, true, JSON.stringify(result.errors)); +}); + +test('returns one diagnostic for each invalid source line', () => { + const result = validate([ + 'm ::field=1.0', + 'm family::=1.0', + 'm field=', + 'm field=NaN', + 'm field=9223372036854775808i', + 'm field=18446744073709551616u', + 'm field=1.0 9223372036854775808', + 'm,tag= field=1.0', + 'm field="unterminated', + 'm field=1.0 extra timestamp', + ].join('\n')); + assert.equal(result.ok, false); + assert.equal(result.errors.length, 10); + assert.deepEqual(result.errors.map((error) => error.line), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +}); diff --git a/scripts/ci/lint-codeblocks.mjs b/scripts/ci/lint-codeblocks.mjs index 9a93542953..913376cfad 100644 --- a/scripts/ci/lint-codeblocks.mjs +++ b/scripts/ci/lint-codeblocks.mjs @@ -9,7 +9,7 @@ import { findPagesReferencingSharedContent, } from '../lib/content-utils.js'; -const BLOCKING_LANGS = new Set(['json', 'jsonl', 'yaml', 'toml']); +const BLOCKING_LANGS = new Set(['json', 'jsonl', 'yaml', 'toml', 'lp']); function escapeGitHubCommandMessage(message) { return String(message) diff --git a/scripts/lib/codeblock-extractor.mjs b/scripts/lib/codeblock-extractor.mjs index a79338cfac..dd9fd4229e 100644 --- a/scripts/lib/codeblock-extractor.mjs +++ b/scripts/lib/codeblock-extractor.mjs @@ -11,6 +11,7 @@ const LANG_ALIASES = { yml: 'yaml', yaml: 'yaml', json: 'json', jsonl: 'jsonl', toml: 'toml', + lp: 'lp', }; const CONT_RE = /^$/; diff --git a/scripts/lib/codeblock-normalizer.mjs b/scripts/lib/codeblock-normalizer.mjs index 16ad3539e7..1587bc8e42 100644 --- a/scripts/lib/codeblock-normalizer.mjs +++ b/scripts/lib/codeblock-normalizer.mjs @@ -4,6 +4,7 @@ import * as toml from './codeblock-validators/toml.mjs'; import * as bash from './codeblock-validators/bash.mjs'; import * as python from './codeblock-validators/python.mjs'; import * as javascript from './codeblock-validators/javascript.mjs'; +import * as lp from './codeblock-validators/lp.mjs'; const VALIDATORS = { json: (c) => json.validate(c), @@ -14,6 +15,7 @@ const VALIDATORS = { python: (c) => python.validate(c), js: (c) => javascript.validate(c), javascript: (c) => javascript.validate(c), + lp: (c) => lp.validate(c), }; // Flux: |> pipe is unambiguous; from(bucket:) is a strong secondary signal. diff --git a/scripts/lib/codeblock-validators/lp.mjs b/scripts/lib/codeblock-validators/lp.mjs new file mode 100644 index 0000000000..d6756b9517 --- /dev/null +++ b/scripts/lib/codeblock-validators/lp.mjs @@ -0,0 +1,204 @@ +const INT64_MIN = -(1n << 63n); +const INT64_MAX = (1n << 63n) - 1n; +const UINT64_MAX = (1n << 64n) - 1n; + +function fail(message) { + throw new Error(message); +} + +function splitUnescaped(value, delimiter, { quotes = false } = {}) { + const parts = []; + let start = 0; + let escaped = false; + let quoted = false; + for (let i = 0; i < value.length; i++) { + const char = value[i]; + if (escaped) { + escaped = false; + continue; + } + if (char === '\\') { + escaped = true; + continue; + } + if (quotes && char === '"') { + quoted = !quoted; + continue; + } + if (!quoted && char === delimiter) { + parts.push(value.slice(start, i)); + start = i + 1; + } + } + if (escaped) fail('dangling escape'); + if (quoted) fail('unterminated string'); + parts.push(value.slice(start)); + return parts; +} + +function pointTokens(line) { + const tokens = []; + let start = -1; + let escaped = false; + let quoted = false; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + if (escaped) { + escaped = false; + continue; + } + if (char === '\\') { + if (start < 0) fail('unexpected escape before measurement'); + escaped = true; + continue; + } + if (char === '"') { + quoted = !quoted; + continue; + } + if (!quoted && /[ \t]/.test(char)) { + if (start >= 0) { + tokens.push(line.slice(start, i)); + start = -1; + } + continue; + } + if (start < 0) start = i; + } + if (escaped) fail('dangling escape'); + if (quoted) fail('unterminated string'); + if (start >= 0) tokens.push(line.slice(start)); + return tokens; +} + +function firstUnescaped(value, needle) { + let escaped = false; + for (let i = 0; i <= value.length - needle.length; i++) { + if (escaped) { + escaped = false; + continue; + } + if (value[i] === '\\') { + escaped = true; + continue; + } + if (value.startsWith(needle, i)) return i; + } + return -1; +} + +function requireNonempty(value, name) { + if (!value) fail(`missing ${name}`); +} + +function validateEscapes(value) { + // The scanner intentionally permits every escaped character: line protocol + // accepts escaped separators and retaining unknown escapes avoids rewriting + // documented identifiers. It still rejects a trailing escape. + const trailing = value.match(/\\+$/)?.[0].length ?? 0; + if (trailing % 2 === 1) fail('dangling escape'); +} + +function validateKey(key, type) { + requireNonempty(key, `${type} key`); + validateEscapes(key); +} + +function validateAssignment(part, type) { + const equals = firstUnescaped(part, '='); + if (equals < 0) fail(`missing ${type} value`); + if (firstUnescaped(part.slice(equals + 1), '=') >= 0 && type === 'tag') { + // Equals signs in values must be escaped. This catches separator mistakes + // without changing the field value grammar, where strings can contain =. + fail('invalid tag separator'); + } + const key = part.slice(0, equals); + const value = part.slice(equals + 1); + validateKey(key, type); + requireNonempty(value, `${type} value`); + validateEscapes(value); + return { key, value }; +} + +function validateFieldKey(key) { + validateKey(key, 'field'); + const delimiter = firstUnescaped(key, '::'); + if (delimiter < 0) return; + requireNonempty(key.slice(0, delimiter), 'field family'); + requireNonempty(key.slice(delimiter + 2), 'field name'); +} + +function validateString(value) { + if (!value.startsWith('"')) return false; + if (value.length < 2 || !value.endsWith('"')) fail('unterminated string'); + let escaped = false; + for (let i = 1; i < value.length - 1; i++) { + if (escaped) { + escaped = false; + } else if (value[i] === '\\') { + escaped = true; + } else if (value[i] === '"') { + fail('unescaped quote in string'); + } + } + if (escaped) fail('dangling escape'); + return true; +} + +function boundedInteger(value, suffix, min, max, label) { + const raw = suffix ? value.slice(0, -suffix.length) : value; + if (!/^[+-]?\d+$/.test(raw)) return false; + const number = BigInt(raw); + if (number < min || number > max) fail(`${label} is out of range`); + return true; +} + +function validateFieldValue(value) { + if (validateString(value)) return; + if (/^(?:true|false|t|f|TRUE|FALSE|T|F)$/.test(value)) return; + if (value.endsWith('i') && boundedInteger(value, 'i', INT64_MIN, INT64_MAX, 'integer field')) return; + if (value.endsWith('u') && boundedInteger(value, 'u', 0n, UINT64_MAX, 'unsigned integer field')) return; + // An unsuffixed numeric value is a float, including a whole number. + if (/^[+-]?(?:(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?|\d+(?:[eE][+-]?\d+)?)$/.test(value)) return; + fail('invalid field value'); +} + +function validateLine(line) { + if (!line || line.startsWith('#')) return; + const tokens = pointTokens(line); + if (tokens.length < 2) fail('point requires a measurement and fields'); + if (tokens.length > 3) fail('extra timestamp tokens'); + + const [measurementAndTags, fields, timestamp] = tokens; + const measurementParts = splitUnescaped(measurementAndTags, ','); + requireNonempty(measurementParts.shift(), 'measurement'); + for (const tag of measurementParts) validateAssignment(tag, 'tag'); + + const fieldParts = splitUnescaped(fields, ',', { quotes: true }); + if (!fieldParts.length) fail('missing fields'); + for (const field of fieldParts) { + const assignment = validateAssignment(field, 'field'); + validateFieldKey(assignment.key); + validateFieldValue(assignment.value); + } + + if (timestamp != null) { + if (!boundedInteger(timestamp, '', INT64_MIN, INT64_MAX, 'timestamp')) { + fail('timestamp must be a signed integer'); + } + } +} + +/** Validate InfluxDB line protocol, including qualified field keys. */ +export function validate(code) { + const errors = []; + for (const [index, line] of code.split('\n').entries()) { + if (!line) continue; + try { + validateLine(line); + } catch (error) { + errors.push({ line: index + 1, message: error.message ?? String(error) }); + } + } + return { ok: errors.length === 0, errors }; +} From de35d653a4cebad7ddd5ea9347fbff50dcb6e137 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Tue, 25 Aug 2026 14:48:30 -0500 Subject: [PATCH 4/4] style(test): format line protocol regression check --- cypress/e2e/content/render-regression.cy.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/content/render-regression.cy.js b/cypress/e2e/content/render-regression.cy.js index 950ed3a360..94ad4c6bf6 100644 --- a/cypress/e2e/content/render-regression.cy.js +++ b/cypress/e2e/content/render-regression.cy.js @@ -99,11 +99,11 @@ describe('Shortcode examples page', () => { .find('.lp-family-delimiter') .should('have.length', 4) .each(($delimiter) => expect($delimiter).to.have.text('::')); + cy.get('@lineProtocol').find('.na').contains('b::c').should('exist'); cy.get('@lineProtocol') - .find('.na') - .contains('b::c') + .find('.s') + .contains('Unicode-value') .should('exist'); - cy.get('@lineProtocol').find('.s').contains('Unicode-value').should('exist'); }); it('uses escaped plain text for malformed line protocol', () => {