diff --git a/source/_includes/code_example.html b/source/_includes/code_example.html
new file mode 100644
index 000000000..030ed33d7
--- /dev/null
+++ b/source/_includes/code_example.html
@@ -0,0 +1,10 @@
+{% assign _title = include.title | default: 'Full JSON example' %}
+{% assign _target = include.target | default: "_blank" %}
+
+
+
+ {% highlight json %}
+ {% include_lines presentation/4.0/example/{{ include.src }} from:{{ include.from }} to:{{ include.to }} %}
+ {% endhighlight %}
+
diff --git a/source/_plugins/include_lines.rb b/source/_plugins/include_lines.rb
new file mode 100644
index 000000000..be8a2616f
--- /dev/null
+++ b/source/_plugins/include_lines.rb
@@ -0,0 +1,140 @@
+require 'liquid'
+
+module Jekyll
+ class IncludeLinesTag < Liquid::Tag
+ def initialize(tag_name, markup, tokens)
+ super
+ @markup = markup.to_s.strip
+ end
+
+ def render(context)
+ site = context.registers[:site]
+ source = site.source
+
+ rendered_markup = Liquid::Template.parse(@markup).render(context)
+ path, options = parse_markup(rendered_markup)
+ raise ArgumentError, "include_lines: missing file path" if path.nil? || path.empty?
+
+ from = integer_option(options, 'from')
+ to = integer_option(options, 'to')
+ start = integer_option(options, 'start')
+ finish = integer_option(options, 'end')
+
+ from ||= start
+ to ||= finish
+
+ format = true
+ if options.key?('format')
+ format = boolean_option(options, 'format')
+ end
+ indent = integer_option(options, 'indent')
+
+ unless from && to
+ raise ArgumentError, "include_lines: must specify from/to (1-indexed), e.g. {% include_lines path from:11 to:35 %}"
+ end
+
+ if from < 1 || to < 1
+ raise ArgumentError, "include_lines: from/to must be >= 1 (got from:#{from} to:#{to})"
+ end
+
+ if to < from
+ raise ArgumentError, "include_lines: to must be >= from (got from:#{from} to:#{to})"
+ end
+
+ absolute_path = File.expand_path(path, source)
+ unless absolute_path.start_with?(File.expand_path(source) + File::SEPARATOR)
+ raise ArgumentError, "include_lines: path must be within the site source directory"
+ end
+
+ unless File.file?(absolute_path)
+ raise ArgumentError, "include_lines: file not found: #{path}"
+ end
+
+ lines = File.read(absolute_path, encoding: 'UTF-8').split("\n", -1)
+ max_line = lines.length
+
+ if from > max_line
+ raise ArgumentError, "include_lines: from (#{from}) is beyond end of file (#{max_line} lines): #{path}"
+ end
+
+ to = [to, max_line].min
+
+ selected = lines[(from - 1)..(to - 1)]
+
+ if format
+ selected = dedent_lines(selected)
+ end
+
+ if indent && indent > 0
+ prefix = ' ' * indent
+ selected = selected.map { |l| l.strip.empty? ? l : (prefix + l) }
+ end
+
+ selected.join("\n")
+ rescue StandardError => e
+ if defined?(Jekyll) && Jekyll.respond_to?(:logger) && Jekyll.logger
+ Jekyll.logger.error("include_lines:", e.message)
+ end
+ raise
+ end
+
+ private
+
+ def parse_markup(markup)
+ tokens = markup.scan(/\"[^\"]+\"|\'[^\']+\'|\S+/)
+ return [nil, {}] if tokens.empty?
+
+ path_token = tokens.shift
+ path = unquote(path_token)
+
+ options = {}
+ tokens.each do |t|
+ key, value = t.split(':', 2)
+ next if value.nil?
+ options[key] = unquote(value)
+ end
+
+ [path, options]
+ end
+
+ def unquote(value)
+ v = value.to_s
+ if (v.start_with?('"') && v.end_with?('"')) || (v.start_with?("'") && v.end_with?("'"))
+ v[1..-2]
+ else
+ v
+ end
+ end
+
+ def integer_option(options, key)
+ return nil unless options.key?(key)
+ Integer(options[key])
+ rescue ArgumentError
+ raise ArgumentError, "include_lines: #{key} must be an integer (got #{options[key].inspect})"
+ end
+
+ def boolean_option(options, key)
+ return nil unless options.key?(key)
+
+ v = options[key].to_s.strip.downcase
+ return true if %w[1 true yes y on].include?(v)
+ return false if %w[0 false no n off].include?(v)
+
+ raise ArgumentError, "include_lines: #{key} must be a boolean (got #{options[key].inspect})"
+ end
+
+ def dedent_lines(lines)
+ non_empty = lines.reject { |l| l.strip.empty? }
+ return lines if non_empty.empty?
+
+ min_indent = non_empty.map { |l| l[/\A[ \t]*/].length }.min
+ return lines if min_indent.nil? || min_indent.zero?
+
+ lines.map do |l|
+ l.strip.empty? ? l : l[min_indent..]
+ end
+ end
+ end
+end
+
+Liquid::Template.register_tag('include_lines', Jekyll::IncludeLinesTag)
diff --git a/source/presentation/4.0/example/02_timeline.json b/source/presentation/4.0/example/02_timeline.json
new file mode 100644
index 000000000..a5490fbc9
--- /dev/null
+++ b/source/presentation/4.0/example/02_timeline.json
@@ -0,0 +1,37 @@
+{
+ "@context": "http://iiif.io/api/presentation/4/context.json",
+ "id": "https://iiif.io/api/presentation/4.0/example/02_timeline.json",
+ "type": "Manifest",
+ "label": {
+ "en": [
+ "Simplest Audio Example (IIIF Presentation v4)"
+ ]
+ },
+ "items": [
+ {
+ "id": "https://iiif.io/api/presentation/4.0/example/02",
+ "type": "Timeline",
+ "duration": 1985.024,
+ "items": [
+ {
+ "id": "https://iiif.io/api/presentation/4.0/example/02/page",
+ "type": "AnnotationPage",
+ "items": [
+ {
+ "id": "https://iiif.io/api/presentation/4.0/example/02/page/anno",
+ "type": "Annotation",
+ "motivation": "painting",
+ "body": {
+ "id": "https://fixtures.iiif.io/audio/indiana/mahler-symphony-3/CD1/medium/128Kbps.mp4",
+ "type": "Sound",
+ "format": "audio/mp4",
+ "duration": 1985.024
+ },
+ "target": "https://iiif.io/api/presentation/4.0/example/02"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/source/presentation/4.0/index.md b/source/presentation/4.0/index.md
index 3540e937b..4c55a90aa 100644
--- a/source/presentation/4.0/index.md
+++ b/source/presentation/4.0/index.md
@@ -73,6 +73,42 @@ a:hover > code {
text-decoration: underline;
}
+ .code-example {
+ margin: 0 0 1em 0;
+ }
+ .code-example__header {
+ background-color: #edf0f0;
+ border-top-left-radius: 1em;
+ border-top-right-radius: 1em;
+ padding: 0.6em 1.5em;
+ font-weight: bold;
+ }
+ .code-example__header a {
+ color: #171717;
+ text-decoration: underline;
+ }
+ .code-example__header + .highlight pre {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ figure.highlight pre {
+ border-bottom-left-radius: 1em;
+ border-bottom-right-radius: 1em;
+ }
+.code-example > figure.highlight {
+ margin-left: 0px;
+ margin-right: 0px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ text-align: left;
+
+ }
+
+ code.language-json {
+ font-size: 0.9rem;
+ line-height: 1.0;
+ font-family: "Courier Prime", monospace;
+ }
# Status of this Document
@@ -170,33 +206,7 @@ A Container that represents a bounded temporal range, without any spatial coordi
Timelines have an additional required property of [`duration`][prezi-40-model-duration], which gives the extent of the Timeline as a floating point number of seconds.
-```json
-{
- "id": "https://example.org/iiif/presentation/examples/manifest-with-containers/timeline",
- "type": "Timeline",
- "duration": 32.76,
- "items": [
- {
- "id": "https://example.org/iiif/presentation/examples/manifest-with-containers/page/p1",
- "type": "AnnotationPage",
- "items": [
- {
- "id": "https://example.org/iiif/presentation/examples/manifest-with-containers/annotation/t1",
- "type": "Annotation",
- "motivation": [ "painting" ],
- "body": {
- "id": "https://iiif.io/api/presentation/example-content-resources/audio/clip.mp3",
- "type": "Audio",
- "format": "audio/mp3",
- "duration": 32.76
- },
- "target": "https://example.org/iiif/presentation/examples/manifest-with-containers/timeline"
- }
- ]
- }
- ]
-}
-```
+{% include code_example.html src="02_timeline.json" from=11 to=35 %}
### Canvas