From 06359cfb61f83d2fd1f787110ac03d553b61a0ec Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 15:16:37 +0100 Subject: [PATCH 01/11] Add specification spec --- .github/workflows/ci.yml | 10 +++++- Gemfile | 2 ++ spec/spec_helper.rb | 45 ++++++++++++++++++++++++ spec/specification_spec.rb | 72 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 spec/specification_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8b37f6..b5bee0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,11 +37,19 @@ jobs: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true + - name: Download v0 spec + run: | + curl -fsSL \ + "https://github.com/RaspberryPiFoundation/documentation/raw/cae831932308c884fc81cd5b106cc722b106e582/docs/technology/codebases-and-products/raspberry-flavoured-markdown/kramdown-rpf-v0-spec.md" \ + -o kramdown-rpf-v0-spec.md + - name: Run tests run: bundle exec rake spec + env: + SPEC_MD: kramdown-rpf-v0-spec.md publish: - needs: [lint, test] + needs: [lint, test, spec] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') permissions: diff --git a/Gemfile b/Gemfile index 5417310..3973148 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,9 @@ source 'https://rubygems.org' gemspec group :development do + gem 'compare-xml' gem 'rake' + gem "rexml", "~> 3.4" gem 'rspec', require: false gem 'rubocop', require: false gem 'rubocop-performance', require: false diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6efd082..985b49c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,51 @@ require 'bundler/setup' require 'kramdown_rpf' +require 'compare-xml' +require 'nokogiri' +require 'diff/lcs' +require 'rspec/matchers' +require 'i18n' + +I18n.locale = 'en' + +KRAMDOWN_OPTIONS = { + input: 'KramdownRPF', + parse_block_html: true, + syntax_highlighter: nil +}.freeze + +def html_diff(actual_html, expected_html) + actual_lines = Nokogiri::HTML5.fragment(actual_html).to_xhtml.lines + expected_lines = Nokogiri::HTML5.fragment(expected_html).to_xhtml.lines + + diffs = Diff::LCS.diff(actual_lines, expected_lines) + return '' if diffs.empty? + + output = [] + diffs.each do |hunk| + hunk.each do |change| + prefix = change.action == '+' ? "\e[32m+" : "\e[31m-" + output << "#{prefix} #{change.element.chomp}\e[0m" + end + end + output.join("\n") +end + +RSpec::Matchers.define :match_html do |expected_html, **options| + match do |actual_html| + @actual_html = actual_html + @expected_html = expected_html + expected_doc = Nokogiri::HTML5.fragment(expected_html) + actual_doc = Nokogiri::HTML5.fragment(actual_html) + + CompareXML.equivalent?(expected_doc, actual_doc, verbose: true, **options).empty? + end + + failure_message do + "HTML does not match.\n#{html_diff(@actual_html, @expected_html)}" + end +end RSpec.configure do |config| # Enable flags like --only-failures and --next-failure diff --git a/spec/specification_spec.rb b/spec/specification_spec.rb new file mode 100644 index 0000000..e18b28a --- /dev/null +++ b/spec/specification_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' + +SPEC_MD = ENV.fetch('SPEC_MD', nil) + +# Parses spec.md and returns an array of: +# { section: String, subsection: String|nil, number: Integer, +# input: String, expected: String } +def parse_spec(path) + content = File.read(path) + examples = [] + section = 'Unknown' + subsection = nil + number = 0 + + content.scan(/^((?:\#{1,6}) [^\n]+$)|^```+example\n(.*?)\n```+/m) do |heading, block| + if heading + level = heading.match(/^(#+)/)[1].length + title = heading.sub(/^#+\s*/, '').strip + if level <= 2 + section = title + subsection = nil + else + subsection = title + end + elsif block + parts = block.split(/\n·\n/, 2) + next unless parts.length == 2 + + number += 1 + examples << { + section: section, + subsection: subsection, + number: number, + input: parts[0].strip, + expected: parts[1].strip + } + end + end + + examples +end + +return if SPEC_MD.nil? + +raise "Spec file not found: #{SPEC_MD}" unless File.exist?(SPEC_MD) + + +RSpec.describe "RPF Markdown Spec: #{File.basename(SPEC_MD)}" do + examples = parse_spec(SPEC_MD) + examples.group_by { |e| e[:section] }.each do |section, section_examples| + context section do + section_examples.group_by { |e| e[:subsection] }.each do |subsection, sub_examples| + define_examples = lambda do + sub_examples.each do |example| + it "example #{example[:number]}" do + actual = Kramdown::Document.new(example[:input], KRAMDOWN_OPTIONS).to_html + expect(actual).to match_html(example[:expected]) + end + end + end + + if subsection + context subsection, &define_examples + else + define_examples.call + end + end + end + end +end From f5f702c20a69297e41d26b00fc64849d74c178e7 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 15:17:34 +0100 Subject: [PATCH 02/11] Remove old dependency --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5bee0d..04ea32b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: SPEC_MD: kramdown-rpf-v0-spec.md publish: - needs: [lint, test, spec] + needs: [lint, test] runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') permissions: From 869f480560b24c42bf4fac40aae8e1c30aba3f3a Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 15:23:47 +0100 Subject: [PATCH 03/11] Turns out github CI can't access stuff easily. --- .github/workflows/ci.yml | 8 +- spec/kramdown-rpf-v0-spec.md | 594 +++++++++++++++++++++++++++++++++++ 2 files changed, 595 insertions(+), 7 deletions(-) create mode 100644 spec/kramdown-rpf-v0-spec.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04ea32b..8a006c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,16 +37,10 @@ jobs: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - - name: Download v0 spec - run: | - curl -fsSL \ - "https://github.com/RaspberryPiFoundation/documentation/raw/cae831932308c884fc81cd5b106cc722b106e582/docs/technology/codebases-and-products/raspberry-flavoured-markdown/kramdown-rpf-v0-spec.md" \ - -o kramdown-rpf-v0-spec.md - - name: Run tests run: bundle exec rake spec env: - SPEC_MD: kramdown-rpf-v0-spec.md + SPEC_MD: spec/kramdown-rpf-v0-spec.md publish: needs: [lint, test] diff --git a/spec/kramdown-rpf-v0-spec.md b/spec/kramdown-rpf-v0-spec.md new file mode 100644 index 0000000..10197d7 --- /dev/null +++ b/spec/kramdown-rpf-v0-spec.md @@ -0,0 +1,594 @@ +--- +title: Kramdown RPF v0 Spec +--- + +# Spec — kramdown-rpf version 0.12.0 + +This document is the formal specification for the custom block syntax used in Raspberry Pi Foundation +project content. It is parsed by `kramdown-rpf` (Ruby) and the TypeScript renderer. + +Each **example** below shows the markdown input above the `·` separator and the expected HTML output +below it. These examples are the canonical test suite — the parsers in both renderers are expected to +produce output that matches exactly (modulo leading/trailing whitespace). + +--- + +## How to read this spec + +```text +This is the markdown input. +· +

This is the expected HTML output.

+``` + +The separator is a middle dot (`·`) on its own line. Test runners split on `\n·\n`. + +--- + +## Hint + +A single `hint` block renders as a swiper slide. It is always used inside a `hints` block in +practice but can be used standalone. + +```example +--- hint --- + +Some hint content + +--- /hint --- +· +
+

Some hint content

+ +
+``` + +--- + +## Hints + +A `hints` block wraps one or more `hint` blocks in a swiper panel with pagination controls. + +```example +--- hints --- +--- hint --- + +Hint 1 + +--- /hint --- +--- hint --- + +Hint 2 + +--- /hint --- + +--- /hints --- +· +
+

+ I need a hint +

+ +
+
+
+
+

Hint 1

+ +
+ +
+

Hint 2

+ +
+
+ +
+ + + +
+ +
+
+
+
+
+``` + +--- + +## Task + +A `task` block renders as a checkable task item. Content inside is parsed as markdown. + +```example +--- task --- + +Complete this step. + +--- /task --- +· +
+ +
+

Complete this step.

+ +
+
+``` + +A `task` block can contain code fences, hints, and collapse blocks. + +````example +--- task --- + +Add this code: + +```python +print('hello') +``` + +--- /task --- +· +
+ +
+

Add this code:

+ +
print('hello')
+
+ +
+
+```` + +--- + +## Challenge + +A `challenge` block contains optional markdown content. It has no wrapping element — its content is +rendered directly into the document flow. + +```example +--- challenge --- + +## Challenge: Try something new + +Can you improve your project? + +--- /challenge --- +· +

Challenge: Try something new

+ +

Can you improve your project?

+``` + +--- + +## Code block + +The `code` block provides a styled code display with optional filename, line numbers, and line +highlights. It uses a YAML front matter section to configure the display. + +### Language only + +```example +--- code --- +--- +language: python +--- +print("Hello, World!") +--- /code --- +· +

+print("Hello, World!")
+
+``` + +### With filename + +```example +--- code --- +--- +language: python +filename: hello.py +--- +print("Hello, World!") +--- /code --- +· +
+ hello.py +
+

+print("Hello, World!")
+
+``` + +### With line numbers + +```example +--- code --- +--- +language: python +line_numbers: true +--- +print("Hello, World!") +--- /code --- +· +

+print("Hello, World!")
+
+``` + +### With line highlights + +```example +--- code --- +--- +language: python +line_highlights: 1 +--- +print("Hello, World!") +--- /code --- +· +

+print("Hello, World!")
+
+``` + +### With all features + +```example +--- code --- +--- +filename: button_press.py +language: python +line_numbers: true +line_number_start: 3 +line_highlights: 3, 5-6 +--- +while True: + button.wait_for_press() + parp = random.choice(trumps) + os.system("aplay {0}".format(parp)) + sleep(2) +--- /code --- +· +
+ button_press.py +
+

+while True:
+    button.wait_for_press()
+    parp = random.choice(trumps)
+    os.system("aplay {0}".format(parp))
+    sleep(2)
+
+``` + +--- + +## Collapse + +A `collapse` block renders as a collapsible ingredient panel. It requires a YAML front matter section +with a `title` field. The body is parsed as markdown. + +```example +--- collapse --- +--- +title: How to do something +--- + +Here is some useful information. + +--- /collapse --- +· +
+

+ How to do something +

+ +
+

Here is some useful information.

+ +
+
+``` + +--- + +## Save + +The `save` tag renders a "Save your project" panel. It takes no content. + +```example +--- save --- +· +
+

+ Save your project +

+
+``` + +--- + +## New page + +The `new-page` tag inserts a print page break. + +```example +First Page + +--- new-page --- + +Second Page +· +

First Page

+ +
+ +

Second Page

+``` + +--- + +## No print + +Content inside a `no-print` block is hidden when printing. + +```example +--- no-print --- +This won't print. +--- /no-print --- +· +
+ +

This won’t print.

+
+``` + +--- + +## Print only + +Content inside a `print-only` block is only visible when printing. + +```example +--- print-only --- +This only appears in print. +--- /print-only --- +· +
+ +

This only appears in print.

+
+``` + +--- + +## Quiz + +A `quiz` block renders a simple radio button poll. The question is defined in YAML front matter and +the choices are a markdown list of radio items using `( )` notation. + +```example +--- quiz --- +--- +question: How are you feeling? +--- + +- ( ) Good +- ( ) Bad +- ( ) Okay + +--- /quiz --- +· +
+
+

+ How are you feeling? +

+ +
+ + + + + + +
+ +
+
+
+``` + +--- + +## Knowledge quiz question + +A `question` block renders a self-marking quiz question. It contains a `choices` block with radio +items using `( )` for incorrect and `(x)` for the correct answer. Optional `feedback` blocks provide +per-choice feedback. + +### Simple question + +```example +--- question --- + +What is 2 + 2? + +--- choices --- + +- ( ) 3 +- (x) 4 +- ( ) 5 + +--- /choices --- + +--- /question --- +· +
+
+ Question +
+

What is 2 + 2?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+``` + +### With legend front matter + +The legend can be overridden via a YAML front matter section inside the `question` block. + +```example +--- question --- + +--- +legend: Question 1 of 3 +--- + +What is 2 + 2? + +--- choices --- + +- ( ) 3 +- (x) 4 +- ( ) 5 + +--- /choices --- + +--- /question --- +· +
+
+ Question 1 of 3 +
+

What is 2 + 2?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+``` + +### With per-choice feedback + +```example +--- question --- + +Is the sky blue? + +--- choices --- + +- (x) Yes + +- ( ) No + + --- feedback --- + Look outside on a clear day. + --- /feedback --- + +--- /choices --- + +--- /question --- +· +
+
+ Question +
+

Is the sky blue?

+
+
+
+ + +
+
+ + +
+
+
+ + +
+``` + +--- + +## Microbit code block + +A fenced code block with language `microbit` renders with the `language-microbit` class for the +Microbit simulator widget. + +````example +```microbit +let x = 5 +``` +· +
let x = 5
+
+```` + +--- + +## Scratch code blocks + +Fenced code blocks with language `blocks3` (Scratch 3) or `blocks` (Scratch 2) render with the +corresponding class for the scratchblocks rendering library. + +````example +```blocks3 +when flag clicked +``` +· +
when flag clicked
+
+```` + +````example +```blocks +when flag clicked +``` +· +
when flag clicked
+
+```` From 038a21ceb47fe0157c12a985a93c39c96a87731e Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 15:27:07 +0100 Subject: [PATCH 04/11] Rubocop --- Gemfile | 2 +- spec/specification_spec.rb | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 3973148..cfc51e3 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gemspec group :development do gem 'compare-xml' gem 'rake' - gem "rexml", "~> 3.4" + gem 'rexml', '~> 3.4' gem 'rspec', require: false gem 'rubocop', require: false gem 'rubocop-performance', require: false diff --git a/spec/specification_spec.rb b/spec/specification_spec.rb index e18b28a..45c425e 100644 --- a/spec/specification_spec.rb +++ b/spec/specification_spec.rb @@ -46,11 +46,10 @@ def parse_spec(path) raise "Spec file not found: #{SPEC_MD}" unless File.exist?(SPEC_MD) - -RSpec.describe "RPF Markdown Spec: #{File.basename(SPEC_MD)}" do +RSpec.describe "RPF Markdown Spec: #{File.basename(SPEC_MD)}" do # rubocop:disable RSpec/DescribeClass examples = parse_spec(SPEC_MD) examples.group_by { |e| e[:section] }.each do |section, section_examples| - context section do + context section do # rubocop:disable RSpec/EmptyExampleGroup section_examples.group_by { |e| e[:subsection] }.each do |subsection, sub_examples| define_examples = lambda do sub_examples.each do |example| From d8f16cf10b79f941eada518c55b6fe2b692bfb24 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 16:44:43 +0100 Subject: [PATCH 05/11] Add rename the specification doc --- spec/kramdown-rpf-v0-spec.md | 925 +++++++++++++++++++++++++++++++---- spec/kramdown_rpf_spec.rb | 2 +- spec/specification_spec.rb | 44 +- 3 files changed, 850 insertions(+), 121 deletions(-) diff --git a/spec/kramdown-rpf-v0-spec.md b/spec/kramdown-rpf-v0-spec.md index 10197d7..7c6eb8e 100644 --- a/spec/kramdown-rpf-v0-spec.md +++ b/spec/kramdown-rpf-v0-spec.md @@ -1,15 +1,24 @@ --- -title: Kramdown RPF v0 Spec +title: Kramdown RPF -- Legacy spec --- # Spec — kramdown-rpf version 0.12.0 -This document is the formal specification for the custom block syntax used in Raspberry Pi Foundation -project content. It is parsed by `kramdown-rpf` (Ruby) and the TypeScript renderer. +This document is the formal specification for the **legacy** custom block syntax used in +Raspberry Pi Foundation project content. It is parsed by `kramdown-rpf` (Ruby) +and the `rfm-renderder-core` TypeScript renderer. -Each **example** below shows the markdown input above the `·` separator and the expected HTML output -below it. These examples are the canonical test suite — the parsers in both renderers are expected to -produce output that matches exactly (modulo leading/trailing whitespace). +Each **example** below shows the markdown input above the `·` separator and the +expected HTML output below it. These examples are the canonical test suite — +the parsers in both renderers are expected to produce output that matches +exactly (modulo leading/trailing whitespace). + +This spec replaces the original `example/` directory in the `kramdown-rpf` +repository. It is intended to be a single source of truth for the expected +behaviour of the custom block syntax, and to be used as the basis for test +suites in both the Ruby and TypeScript renderers. Any changes to the syntax or +expected output should be made here first, and then the tests in both +repositories should be updated to match. --- @@ -21,7 +30,8 @@ This is the markdown input.

This is the expected HTML output.

``` -The separator is a middle dot (`·`) on its own line. Test runners split on `\n·\n`. +The separator is a middle dot (`·`) on its own line. Test runners split on +`\n·\n`. --- @@ -39,7 +49,6 @@ Some hint content ·

Some hint content

-
``` @@ -65,30 +74,22 @@ Hint 2 --- /hints --- ·
-

- I need a hint -

- +

I need a hint

-

Hint 1

- -
- -
-

Hint 2

- -
+

Hint 1

+
+
+

Hint 2

+
-
-
@@ -100,45 +101,149 @@ Hint 2 ## Task -A `task` block renders as a checkable task item. Content inside is parsed as markdown. +A `task` block renders as a checkable task item. Content inside is parsed as markdown. It can contain code fences, hints, and collapse blocks. -```example +````example --- task --- -Complete this step. +Add global direction to your function: + +``` +def joystick_moved(event): + global direction +``` +You can access the direction the joystick was moved in with the help of the event parameter: use the command `event.direction`. --- /task --- ·
-

Complete this step.

+

Add global direction to your function:

+ +
def joystick_moved(event):
+  global direction
+
+

You can access the direction the joystick was moved in with the help of the event parameter: use the command event.direction.

+```` +### With hints + +````example +--- task --- + +Add global direction to your function: + ``` +def joystick_moved(event): + global direction +``` + +You can access the direction the joystick was moved in with the help of the event parameter: use the command `event.direction`. + +--- hints --- +--- hint --- + +Hint 1 + +--- /hint --- +--- hint --- +Hint 2 + +--- /hint --- +--- hint --- + +Hint 3 +--- /hint --- +--- hint --- +Hint 4 +--- /hint --- -A `task` block can contain code fences, hints, and collapse blocks. +--- /hints --- + +--- /task --- +· +
+ +
+

Add global direction to your function:

+
def joystick_moved(event):
+  global direction
+
+

You can access the direction the joystick was moved in with the help of the event parameter: use the command event.direction.

+
+

I need a hint

+
+
+
+
+

Hint 1

+
+
+

Hint 2

+
+
+

Hint 3

+
+
+

Hint 4

+
+
+
+ + + +
+
+
+
+
+
+
+
+```` + +### With ingredient ````example --- task --- -Add this code: +Add global direction to your function: -```python -print('hello') ``` +def joystick_moved(event): + global direction +``` + +You can access the direction the joystick was moved in with the help of the event parameter: use the command `event.direction`. + +--- collapse --- +--- +title: Downloading and installing the Raspberry Pi software +--- + +Content here comes from the ingredient. + +--- /collapse --- --- /task --- ·
- +
-

Add this code:

- -
print('hello')
+    

Add global direction to your function:

+
def joystick_moved(event):
+  global direction
 
- +

You can access the direction the joystick was moved in with the help of the event parameter: use the command event.direction.

+
+

Downloading and installing the Raspberry Pi software

+
+

Content here comes from the ingredient.

+
+
```` @@ -160,7 +265,6 @@ Can you improve your project? --- /challenge --- ·

Challenge: Try something new

-

Can you improve your project?

``` @@ -182,7 +286,7 @@ print("Hello, World!") --- /code --- ·

-print("Hello, World!")
+print("Hello, World!")
 
``` @@ -201,7 +305,7 @@ print("Hello, World!") hello.py

-print("Hello, World!")
+print("Hello, World!")
 
``` @@ -217,7 +321,7 @@ print("Hello, World!") --- /code --- ·

-print("Hello, World!")
+print("Hello, World!")
 
``` @@ -233,7 +337,7 @@ print("Hello, World!") --- /code --- ·

-print("Hello, World!")
+print("Hello, World!")
 
``` @@ -262,10 +366,119 @@ while True: while True: button.wait_for_press() parp = random.choice(trumps) - os.system("aplay {0}".format(parp)) + os.system("aplay {0}".format(parp)) sleep(2)
``` +### Fenced + +A plain fenced code block. + +````example +```python +while True: + button.wait_for_press() + parp = random.choice(trumps) + os.system("aplay {0}".format(parp)) + sleep(2) +``` +· +
while True:
+    button.wait_for_press()
+    parp = random.choice(trumps)
+    os.system("aplay {0}".format(parp))
+    sleep(2)
+
+```` + +### With multi-line content + +```example +--- code --- +--- +language: python +--- +while True: + button.wait_for_press() + parp = random.choice(trumps) + os.system("aplay {0}".format(parp)) + sleep(2) +--- /code --- +· +

+while True:
+    button.wait_for_press()
+    parp = random.choice(trumps)
+    os.system("aplay {0}".format(parp))
+    sleep(2)
+
+``` + +### With line numbers disabled + +```example +--- code --- +--- +language: python +line_numbers: false +--- +while True: + button.wait_for_press() + parp = random.choice(trumps) + os.system("aplay {0}".format(parp)) + sleep(2) +--- /code --- +· +

+while True:
+    button.wait_for_press()
+    parp = random.choice(trumps)
+    os.system("aplay {0}".format(parp))
+    sleep(2)
+
+``` + +### With angle brackets in content + +```example +--- code --- +--- +language: cs +filename: StarController.cs - OnTriggerEnter(Collider other) +line_numbers: true +line_number_start: 21 +line_highlights: 26, 27 +--- + void OnTriggerEnter(Collider other) + { + // Check the tag of the colliding object + if (other.CompareTag("Player")) + { + StarPlayer player = other.gameObject.GetComponent(); + player.stars += 1; // Increase by 1 + AudioSource.PlayClipAtPoint(collectSound, transform.position); + gameObject.SetActive(false); + } + } +--- /code --- +· +
+ StarController.cs - OnTriggerEnter(Collider other) +
+

+    void OnTriggerEnter(Collider other)
+    {
+        // Check the tag of the colliding object
+        if (other.CompareTag("Player"))
+        {
+            StarPlayer player = other.gameObject.GetComponent<StarPlayer>();
+            player.stars += 1; // Increase by 1
+            AudioSource.PlayClipAtPoint(collectSound, transform.position);
+            gameObject.SetActive(false);
+        }
+    }
+
+``` --- @@ -285,17 +498,355 @@ Here is some useful information. --- /collapse --- ·
-

- How to do something -

- +

How to do something

Here is some useful information.

+
+
+``` +### With code block in body + +```example +--- collapse --- +--- +title: Child project +--- +
+ main.py +
+

+vowels = 'AEIOU' # The variable holds a string of vowels
+vowel_list = list(vowels) # Create a list that holds each vowel as a separate item
+print(vowel_list) # Display the list of vowels
+
+ +

The output of this code would be:

+ +
['A', 'E', 'I', 'O', 'U']
+
+ + +--- /collapse --- + +1. Now that you know how to get Steve's position, you can begin your program by storing his poition as three variables. You can use `px`, `py`, and `pz` + +~~~ python +px, py, pz = mc.player.getPos() +~~~ +· +
+

Child project

+
+
+

main.py

+
+

+vowels = 'AEIOU' # The variable holds a string of vowels
+vowel_list = list(vowels) # Create a list that holds each vowel as a separate item
+print(vowel_list) # Display the list of vowels
+
+

The output of this code would be:

+
['A', 'E', 'I', 'O', 'U']
+
+
+
+
    +
  1. Now that you know how to get Steve’s position, you can begin your program by storing his poition as three variables. You can use px, py, and pz
  2. +
+
px, py, pz = mc.player.getPos()
+
+``` + +### Inside a list item + +````example +## Step 2 - Test the PIR motion sensor + +We're going to write some code to print out `Motion detected!` when the PIR sensor detects movement. + +1. Open IDLE, create a new file and save it as **parent-detector.py** + + --- collapse --- + --- + title: Opening IDLE + image: images/idle.png + --- + + [[[idle-opening]]] + + --- /collapse --- + +1. Blab la + ```python + from gpiozero import MotionSensor + + pir = MotionSensor(4) + ``` + +2. Bla bla + + ```python + while True: + if pir.motion_detected: + print("Motion detected!") + ``` +· +

Step 2 - Test the PIR motion sensor

+

We’re going to write some code to print out Motion detected! when the PIR sensor detects movement.

+
    +
  1. +

    Open IDLE, create a new file and save it as parent-detector.py

    +
    +

    Opening IDLE

    +
    +

    [[[idle-opening]]]

    +
    +
    +
  2. +
  3. Blab la +
     from gpiozero import MotionSensor
    +
    + pir = MotionSensor(4)
    +
    +
  4. +
  5. +

    Bla bla

    +
     while True:
    +     if pir.motion_detected:
    +            print("Motion detected!")
    +
    +
  6. +
+```` + +### With HTML body + +```example +--- collapse --- +--- +title: Creating Directories on a Raspberry Pi +--- +

Creating Directories on a Raspberry Pi

+ +

There are two ways to create directories on the Raspberry Pi. The first uses the GUI, and the second uses the Terminal.

+

Method 1 - Using the GUI

+ +

GUI-make-directory

+ +
    +
  1. +

    Open a File Manager window by clicking on the icon in the top left corner of the screen

    + +

    file-manager

    +
  2. +
  3. In the window, right-click and select Create New… and then Folder from the context menu
  4. +
  5. In the dialogue box, type the name of your new directory and then click OK
  6. +
+ +

Method 2 - Using the Terminal

+ +

Terminal-make-directory

+ +
    +
  1. +

    Open a new Terminal window by clicking on the icon in the top left corner of the screen.

    + +

    terminal

    +
  2. +
  3. +

    You can create a new directory using the mkdir command

    + +
    +
     mkdir my-new-directory
    +
    +
    +
    +
  4. +
  5. You can list the contents of the current directory using ls
  6. +
  7. +

    To enter your new directory use the cd command

    + +
    +
     cd my-new-directory
    +
    +
    +
    +
  8. +
+ + +--- /collapse --- +· +
+

Creating Directories on a Raspberry Pi

+
+

Creating Directories on a Raspberry Pi

+

There are two ways to create directories on the Raspberry Pi. The first uses the GUI, and the second uses the Terminal.

+

Method 1 - Using the GUI

+

GUI-make-directory

+
    +
  1. +

    Open a File Manager window by clicking on the icon in the top left corner of the screen

    +

    file-manager

    +
  2. +
  3. In the window, right-click and select Create New… and then Folder from the context menu
  4. +
  5. In the dialogue box, type the name of your new directory and then click OK
  6. +
+

Method 2 - Using the Terminal

+

Terminal-make-directory

+
    +
  1. +

    Open a new Terminal window by clicking on the icon in the top left corner of the screen.

    +

    terminal

    +
  2. +
  3. +

    You can create a new directory using the mkdir command

    +
    +
    +
    +
     mkdir my-new-directory
    +
    +
    +
    +
    +
  4. +
  5. You can list the contents of the current directory using ls
  6. +
  7. +

    To enter your new directory use the cd command

    +
    +
    +
    +
     cd my-new-directory
    +
    +
    +
    +
    +
  8. +
``` +### Inside a challenge + +````example +## Step 5 - Record video to a file + +Seeing the intruder on the screen in a camera preview isn't much help to you with detecting intruders into your room. Instead, let's record a video of the intruder for you to view later on when you get home. + +1. Create a variable called `filename` inside your infinite loop to store the video file name + + ```python + filename = "intruder.h264" + ``` + + In case you are wondering, `.h264` is the video format + +1. Find the line of code where you begin the camera preview and replace it with a line of code to start recording a video + + ```python + camera.start_recording(filename) + ``` + +1. Find the line of code where you stop the camera preview and replace it with a line of code to stop recording. + +--- hints --- + +--- hint --- +Look at the line of code you used to start recording and see if you can work out the code to stop recording +--- /hint --- + +--- hint --- +Here is the finished code +```python +while True: + filename = "intruder.h264" + pir.wait_for_motion() + camera.start_recording(filename) + pir.wait_for_no_motion() + camera.stop_recording() +``` +--- /hint --- + +--- /hints --- + +1. Save and run your program by pressing **F5**. Check that a file called `intruder.h264` appears in the same folder as your `parent-detector.py` file. + +--- challenge --- +Every time a new intruder triggers the motion sensor the video will be overwritten. If you have lots of pesky parents or brothers and sisters intruding into your room, you want to keep videos of all of them. Can you write some code to automatically find out the current date and time and add it to the video filename so that each video we take will have a different filename? + +--- collapse --- +--- +title: Getting the date and time in Python +image: +--- + +[[[generic-python-timestamps]]] + +--- /collapse --- + +--- /challenge --- +· +

Step 5 - Record video to a file

+

Seeing the intruder on the screen in a camera preview isn’t much help to you with detecting intruders into your room. Instead, let’s record a video of the intruder for you to view later on when you get home.

+
    +
  1. +

    Create a variable called filename inside your infinite loop to store the video file name

    +
     filename = "intruder.h264"
    +
    +

    In case you are wondering, .h264 is the video format

    +
  2. +
  3. +

    Find the line of code where you begin the camera preview and replace it with a line of code to start recording a video

    +
     camera.start_recording(filename)
    +
    +
  4. +
  5. +

    Find the line of code where you stop the camera preview and replace it with a line of code to stop recording.

    +
  6. +
+
+

I need a hint

+
+
+
+
+

Look at the line of code you used to start recording and see if you can work out the code to stop recording

+
+
+

Here is the finished code

+
while True:
+    filename = "intruder.h264"
+    pir.wait_for_motion()
+    camera.start_recording(filename)
+    pir.wait_for_no_motion()
+        camera.stop_recording()
+
+
+
+
+ + + +
+
+
+
+
+
+
    +
  1. Save and run your program by pressing F5. Check that a file called intruder.h264 appears in the same folder as your parent-detector.py file.
  2. +
+

Every time a new intruder triggers the motion sensor the video will be overwritten. If you have lots of pesky parents or brothers and sisters intruding into your room, you want to keep videos of all of them. Can you write some code to automatically find out the current date and time and add it to the video filename so that each video we take will have a different filename?

+
+

Getting the date and time in Python

+
+

[[[generic-python-timestamps]]]

+
+
+```` + --- ## Save @@ -306,9 +857,7 @@ The `save` tag renders a "Save your project" panel. It takes no content. --- save --- ·
-

- Save your project -

+

Save your project

``` @@ -326,9 +875,7 @@ First Page Second Page ·

First Page

-
-

Second Page

``` @@ -344,7 +891,6 @@ This won't print. --- /no-print --- ·
-

This won’t print.

``` @@ -361,7 +907,6 @@ This only appears in print. --- /print-only --- ·
-

This only appears in print.

``` @@ -387,19 +932,15 @@ question: How are you feeling? ·
-

- How are you feeling? -

- +

How are you feeling?

- + - + - +
-
@@ -437,22 +978,20 @@ What is 2 + 2?

What is 2 + 2?

-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
- - - + ``` @@ -486,22 +1025,20 @@ What is 2 + 2?

What is 2 + 2?

-
- - -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
- - - + ``` @@ -533,25 +1070,207 @@ Is the sky blue?

Is the sky blue?

-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + + +``` +### With single feedback + +```example +--- question --- + +What food will the bug reach when these instructions are followed? + +![A bug in a crossword-like maze with various bits of food scattered around](./img/q2.svg) + +1. Forward +2. Forward +3. Forward +4. Turn left +5. Forward +6. Forward +7. Turn right +8. Forward +9. Forward +10. Turn right +11. Forward +12. Forward + +--- choices --- + +--- feedback --- +Follow the instructions one at a time. Which food item does the bug reach? +--- /feedback --- + +- (x) Apple +- ( ) Banana +- ( ) Orange +- ( ) Doughnut + +--- /choices --- + +--- /question --- +· +
+
+ Question +
+

What food will the bug reach when these instructions are followed?

+

A bug in a crossword-like maze with various bits of food scattered around

+
    +
  1. Forward
  2. +
  3. Forward
  4. +
  5. Forward
  6. +
  7. Turn left
  8. +
  9. Forward
  10. +
  11. Forward
  12. +
  13. Turn right
  14. +
  15. Forward
  16. +
  17. Forward
  18. +
  19. Turn right
  20. +
  21. Forward
  22. +
  23. Forward
  24. +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
- + +
``` +### With blocks in feedback + +````example +--- question --- + +A dog sprite in Scratch has the following code: + +![A dog with three scratch blocks](./images/q1.svg) + +How would you get the dog sprite to change size? + +--- choices --- + +- ( ) Press the 'space' key + + --- feedback --- + What code is attached to the + ```blocks3 + when [space v] key pressed + ``` + event block? + --- /feedback --- + +- ( ) Make a loud noise + + --- feedback --- + What code is attached to the + ```blocks3 + when [loudness v] > 10 :: events hat + ``` + event block? + --- /feedback --- + +- ( ) Click the green flag + + --- feedback --- + What code is attached to the + ```blocks3 + when flag clicked + ``` + event block? + --- /feedback --- + +- (x) Click on the dog sprite + +--- /choices --- + +--- /question --- +· +
+
+ Question +
+

A dog sprite in Scratch has the following code:

+

A dog with three scratch blocks

+

How would you get the dog sprite to change size?

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+```` + --- ## Microbit code block diff --git a/spec/kramdown_rpf_spec.rb b/spec/kramdown_rpf_spec.rb index 97e5e24..853c2cd 100644 --- a/spec/kramdown_rpf_spec.rb +++ b/spec/kramdown_rpf_spec.rb @@ -10,9 +10,9 @@ code/code_with_all_features code/code_with_angle_brackets code/code_with_filename + code/code_with_line_highlights code/code_with_line_numbers code/code_with_no_line_numbers - code/code_with_line_highlights collapse/collapse collapse/collapse_in_challenge collapse/collapse_music_box diff --git a/spec/specification_spec.rb b/spec/specification_spec.rb index 45c425e..8215cd9 100644 --- a/spec/specification_spec.rb +++ b/spec/specification_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'spec_helper' +require 'English' SPEC_MD = ENV.fetch('SPEC_MD', nil) @@ -8,34 +9,43 @@ # { section: String, subsection: String|nil, number: Integer, # input: String, expected: String } def parse_spec(path) - content = File.read(path) + content = File.readlines(path).map(&:chomp) examples = [] section = 'Unknown' subsection = nil number = 0 - content.scan(/^((?:\#{1,6}) [^\n]+$)|^```+example\n(.*?)\n```+/m) do |heading, block| - if heading - level = heading.match(/^(#+)/)[1].length - title = heading.sub(/^#+\s*/, '').strip + in_example = false + example_lines = [] + + content.each do |line| + if in_example && line == in_example + in_example = false + parts = example_lines.join("\n").split(/\n·\n/, 2) + if parts.length == 2 + number += 1 + examples << { + section: section, + subsection: subsection, + number: number, + input: parts[0].strip, + expected: parts[1].strip + } + end + elsif in_example + example_lines << line + elsif line =~ /^(\#{1,6})\s*(.+)$/ + level = Regexp.last_match(1).length + title = Regexp.last_match(2).strip if level <= 2 section = title subsection = nil else subsection = title end - elsif block - parts = block.split(/\n·\n/, 2) - next unless parts.length == 2 - - number += 1 - examples << { - section: section, - subsection: subsection, - number: number, - input: parts[0].strip, - expected: parts[1].strip - } + elsif line.strip =~ /^(```+)example$/ + in_example = Regexp.last_match(1) + example_lines = [] end end From f43447d15c95a36be9c3bdc906ac5ee1bb885152 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 16:48:51 +0100 Subject: [PATCH 06/11] Rename to legacy spec; make sure all examples are covered --- .github/workflows/ci.yml | 2 +- README.md | 12 ++++++++++++ ...wn-rpf-v0-spec.md => kramdown_rpf-legacy-spec.md} | 0 3 files changed, 13 insertions(+), 1 deletion(-) rename spec/{kramdown-rpf-v0-spec.md => kramdown_rpf-legacy-spec.md} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a006c6..51ec041 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Run tests run: bundle exec rake spec env: - SPEC_MD: spec/kramdown-rpf-v0-spec.md + SPEC_MD: spec/kramdown_rpf-legacy-spec.md publish: needs: [lint, test] diff --git a/README.md b/README.md index 0525736..9568344 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,18 @@ question: Here is a heading for a quiz with three possible answers. How do you f After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +### Testing against the spec + +The formal spec lives at `spec/kramdown-rpf-v0-spec.md`. Run the spec tests with: + +```sh +SPEC_MD=spec/kramdown-rpf-legacy-spec.md bundle exec rake spec +``` + +This is also run automatically in CI. + +**If you add or change examples in `examples/`, you must update the spec file accordingly** — both here in `spec/kramdown_rpf-v0-spec.md` and in the canonical copy in the [documentation repository](https://github.com/RaspberryPiFoundation/documentation) at `docs/technology/codebases-and-products/raspberry-flavoured-markdown/kramdown_rpf-legacy-spec.md`. + To install this gem onto your local machine, run `bundle exec rake install`. ### Release a new version diff --git a/spec/kramdown-rpf-v0-spec.md b/spec/kramdown_rpf-legacy-spec.md similarity index 100% rename from spec/kramdown-rpf-v0-spec.md rename to spec/kramdown_rpf-legacy-spec.md From 6f74ee4f2b0b5d791728fa9fa54cfd7171be4ccc Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 16:55:16 +0100 Subject: [PATCH 07/11] Add more notes to README --- README.md | 10 ++++++++-- spec/i18n_spec.rb | 4 +++- spec/kramdown_rpf_spec.rb | 2 +- spec/specification_spec.rb | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9568344..512abb0 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run ### Testing against the spec -The formal spec lives at `spec/kramdown-rpf-v0-spec.md`. Run the spec tests with: +The formal spec lives at `spec/kramdown_rpf-legacy-spec.md`. This is used to test that the output of the gem matches the expected output for a variety of inputs. To run these tests, run: ```sh -SPEC_MD=spec/kramdown-rpf-legacy-spec.md bundle exec rake spec +bundle exec rspec +``` + +If you wish to use a different spec, you can sent the `SPEC_MD` environment variable: + +``` +SPEC_MD=my-new-spec.md bundle exec rake spec ``` This is also run automatically in CI. diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 7de937a..c12e0e3 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -20,8 +20,10 @@ values = file_contents[1]['kramdown_rpf'] context("with #{locale} locale") do - before do + around do |example| I18n.locale = locale + example.run + I18n.locale = I18n.default_locale end it('converts hint title') do diff --git a/spec/kramdown_rpf_spec.rb b/spec/kramdown_rpf_spec.rb index 853c2cd..c32e87d 100644 --- a/spec/kramdown_rpf_spec.rb +++ b/spec/kramdown_rpf_spec.rb @@ -40,7 +40,7 @@ expect(KramdownRPF::VERSION).not_to be_nil end - describe 'conversions' do + describe 'conversions', skip: 'in favour of specification examples' do conversion_tests.each do |test_name| context test_name do subject(:test_result) do diff --git a/spec/specification_spec.rb b/spec/specification_spec.rb index 8215cd9..dd51771 100644 --- a/spec/specification_spec.rb +++ b/spec/specification_spec.rb @@ -3,7 +3,7 @@ require_relative 'spec_helper' require 'English' -SPEC_MD = ENV.fetch('SPEC_MD', nil) +SPEC_MD = ENV.fetch('SPEC_MD', 'spec/kramdown_rpf-legacy-spec.md') # Parses spec.md and returns an array of: # { section: String, subsection: String|nil, number: Integer, From 06d928d9804c4291d18e2b508e4c87ea1d0f966a Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 17:08:24 +0100 Subject: [PATCH 08/11] Update blocks --- spec/kramdown_rpf-legacy-spec.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/spec/kramdown_rpf-legacy-spec.md b/spec/kramdown_rpf-legacy-spec.md index 7c6eb8e..f38659a 100644 --- a/spec/kramdown_rpf-legacy-spec.md +++ b/spec/kramdown_rpf-legacy-spec.md @@ -4,9 +4,18 @@ title: Kramdown RPF -- Legacy spec # Spec — kramdown-rpf version 0.12.0 +:::info +This spec was built from the example files in the `kramdown-rpf` repository. It +is intended to be a single source of truth for the expected behaviour of the +custom block syntax, and to be used as the basis for test suites in both the +Ruby and TypeScript renderers. Any changes to the syntax or expected output +should be made here first, and then the tests in both repositories should be +updated to match. +::: + This document is the formal specification for the **legacy** custom block syntax used in Raspberry Pi Foundation project content. It is parsed by `kramdown-rpf` (Ruby) -and the `rfm-renderder-core` TypeScript renderer. +and the `rpf-markdown-core` (TypeScript) renderers. Each **example** below shows the markdown input above the `·` separator and the expected HTML output below it. These examples are the canonical test suite — @@ -101,7 +110,7 @@ Hint 2 ## Task -A `task` block renders as a checkable task item. Content inside is parsed as markdown. It can contain code fences, hints, and collapse blocks. +A `task` block renders as a checkable task item. Content inside is parsed as markdown. It can contain code fences, hints, and collapse blocks. ````example --- task --- @@ -129,6 +138,7 @@ You can access the direction the joystick was moved in with the help of the even ```` + ### With hints ````example @@ -370,6 +380,7 @@ while True: sleep(2) ``` + ### Fenced A plain fenced code block. @@ -504,6 +515,7 @@ Here is some useful information. ``` + ### With code block in body ```example @@ -567,7 +579,7 @@ We're going to write some code to print out `Motion detected!` when the PIR sens 1. Open IDLE, create a new file and save it as **parent-detector.py** --- collapse --- - --- + --- title: Opening IDLE image: images/idle.png --- @@ -779,7 +791,7 @@ Every time a new intruder triggers the motion sensor the video will be overwritt --- collapse --- --- title: Getting the date and time in Python -image: +image: --- [[[generic-python-timestamps]]] @@ -937,7 +949,7 @@ question: How are you feeling? - + @@ -1087,6 +1099,7 @@ Is the sky blue? ``` + ### With single feedback ```example From 22d06233984b78052a6fe251f1662fd3287e97c3 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 17:20:48 +0100 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Gemfile | 1 + README.md | 2 +- spec/specification_spec.rb | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index cfc51e3..28cffc6 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ group :development do gem 'rake' gem 'rexml', '~> 3.4' gem 'rspec', require: false + gem 'nokogiri' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false diff --git a/README.md b/README.md index 512abb0..02cca92 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ The formal spec lives at `spec/kramdown_rpf-legacy-spec.md`. This is used to tes bundle exec rspec ``` -If you wish to use a different spec, you can sent the `SPEC_MD` environment variable: +If you wish to use a different spec, you can set the `SPEC_MD` environment variable: ``` SPEC_MD=my-new-spec.md bundle exec rake spec diff --git a/spec/specification_spec.rb b/spec/specification_spec.rb index dd51771..c7126a0 100644 --- a/spec/specification_spec.rb +++ b/spec/specification_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require_relative 'spec_helper' -require 'English' +require 'spec_helper' SPEC_MD = ENV.fetch('SPEC_MD', 'spec/kramdown_rpf-legacy-spec.md') @@ -52,8 +51,6 @@ def parse_spec(path) examples end -return if SPEC_MD.nil? - raise "Spec file not found: #{SPEC_MD}" unless File.exist?(SPEC_MD) RSpec.describe "RPF Markdown Spec: #{File.basename(SPEC_MD)}" do # rubocop:disable RSpec/DescribeClass From 6ce30a072df6f09a3d14e1050e4a7f97a9cf7ba7 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 17:21:27 +0100 Subject: [PATCH 10/11] Fix file name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 02cca92..8706033 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ SPEC_MD=my-new-spec.md bundle exec rake spec This is also run automatically in CI. -**If you add or change examples in `examples/`, you must update the spec file accordingly** — both here in `spec/kramdown_rpf-v0-spec.md` and in the canonical copy in the [documentation repository](https://github.com/RaspberryPiFoundation/documentation) at `docs/technology/codebases-and-products/raspberry-flavoured-markdown/kramdown_rpf-legacy-spec.md`. +**If you add or change examples in `examples/`, you must update the spec file accordingly** — both here in `spec/kramdown_rpf-legacy-spec.md` and in the canonical copy in the [documentation repository](https://github.com/RaspberryPiFoundation/documentation) at `docs/technology/codebases-and-products/raspberry-flavoured-markdown/kramdown_rpf-legacy-spec.md`. To install this gem onto your local machine, run `bundle exec rake install`. From 2f9fbd127fcc1a52bcb17ef1ed9f196064057925 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 1 May 2026 17:34:11 +0100 Subject: [PATCH 11/11] Rubocop --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 28cffc6..ee764c8 100644 --- a/Gemfile +++ b/Gemfile @@ -7,10 +7,10 @@ gemspec group :development do gem 'compare-xml' + gem 'nokogiri' gem 'rake' gem 'rexml', '~> 3.4' gem 'rspec', require: false - gem 'nokogiri' gem 'rubocop', require: false gem 'rubocop-performance', require: false gem 'rubocop-rspec', require: false