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" %} + +
+
+ {{ _title }}
+ {% 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