slideck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +555 -0
- data/README.md +475 -0
- data/exe/slideck +5 -0
- data/lib/slideck/alignment.rb +201 -0
- data/lib/slideck/cli.rb +268 -0
- data/lib/slideck/converter.rb +88 -0
- data/lib/slideck/errors.rb +61 -0
- data/lib/slideck/loader.rb +45 -0
- data/lib/slideck/margin.rb +345 -0
- data/lib/slideck/metadata.rb +153 -0
- data/lib/slideck/metadata_converter.rb +95 -0
- data/lib/slideck/metadata_defaults.rb +84 -0
- data/lib/slideck/metadata_parser.rb +188 -0
- data/lib/slideck/metadata_wrapper.rb +66 -0
- data/lib/slideck/parser.rb +139 -0
- data/lib/slideck/presenter.rb +272 -0
- data/lib/slideck/renderer.rb +326 -0
- data/lib/slideck/runner.rb +152 -0
- data/lib/slideck/tracker.rb +167 -0
- data/lib/slideck/transformer.rb +42 -0
- data/lib/slideck/version.rb +5 -0
- data/lib/slideck.rb +30 -0
- metadata +203 -0
| @@ -0,0 +1,188 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Slideck
         | 
| 4 | 
            +
              # Responsible for parsing metadata in YAML format
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @api private
         | 
| 7 | 
            +
              class MetadataParser
         | 
| 8 | 
            +
                # The symbolize names parameter
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @return [Symbol]
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # @api private
         | 
| 13 | 
            +
                SYMBOLIZE_NAMES_PARAMETER = :symbolize_names
         | 
| 14 | 
            +
                private_constant :SYMBOLIZE_NAMES_PARAMETER
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # The permitted classes parameter
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @return [Symbol]
         | 
| 19 | 
            +
                #
         | 
| 20 | 
            +
                # @api private
         | 
| 21 | 
            +
                PERMITTED_CLASSES_PARAMETER = :permitted_classes
         | 
| 22 | 
            +
                private_constant :PERMITTED_CLASSES_PARAMETER
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                # The whitelist classes parameter
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @return [Symbol]
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @api private
         | 
| 29 | 
            +
                WHITELIST_CLASSES_PARAMETER = :whitelist_classes
         | 
| 30 | 
            +
                private_constant :WHITELIST_CLASSES_PARAMETER
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Create a MetadataParser instance
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                # @example
         | 
| 35 | 
            +
                #   MetadataParser.new(YAML, symbolize_names: true, permitted_classes: [])
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @param [YAML] yaml_parser
         | 
| 38 | 
            +
                #   the YAML parser
         | 
| 39 | 
            +
                # @param [Boolean] symbolize_names
         | 
| 40 | 
            +
                #   whether or not to symobolize names
         | 
| 41 | 
            +
                # @param [Array<Object>] permitted_classes
         | 
| 42 | 
            +
                #   the classes allowed to be deserialized
         | 
| 43 | 
            +
                #
         | 
| 44 | 
            +
                # @api public
         | 
| 45 | 
            +
                def initialize(yaml_parser, symbolize_names: nil, permitted_classes: nil)
         | 
| 46 | 
            +
                  @yaml_parser = yaml_parser
         | 
| 47 | 
            +
                  @symbolize_names = symbolize_names
         | 
| 48 | 
            +
                  @permitted_classes = permitted_classes
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                # Parse metadata from content
         | 
| 52 | 
            +
                #
         | 
| 53 | 
            +
                # @example
         | 
| 54 | 
            +
                #   parser.parse("align: center\nfooter: footer content")
         | 
| 55 | 
            +
                #
         | 
| 56 | 
            +
                # @param [String] content
         | 
| 57 | 
            +
                #   the content to parse metadata from
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @return [Hash{String, Symbol => Object}]
         | 
| 60 | 
            +
                #   the deserialized metadata
         | 
| 61 | 
            +
                #
         | 
| 62 | 
            +
                # @api public
         | 
| 63 | 
            +
                def parse(content)
         | 
| 64 | 
            +
                  parse_method = select_parse_method
         | 
| 65 | 
            +
                  parse_params = parse_method_params(parse_method)
         | 
| 66 | 
            +
                  arguments = parser_arguments(parse_params)
         | 
| 67 | 
            +
                  options = parser_options(parse_params)
         | 
| 68 | 
            +
                  metadata = @yaml_parser.send(parse_method, content, *arguments, **options)
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  return metadata if symbolize_names?(options)
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  @symbolize_names ? symbolize_keys(metadata) : metadata
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                private
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                # Select metadata parse method
         | 
| 78 | 
            +
                #
         | 
| 79 | 
            +
                # @return [Symbol]
         | 
| 80 | 
            +
                #
         | 
| 81 | 
            +
                # @api private
         | 
| 82 | 
            +
                def select_parse_method
         | 
| 83 | 
            +
                  @yaml_parser.respond_to?(:safe_load) ? :safe_load : :load
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # Parse method parameters
         | 
| 87 | 
            +
                #
         | 
| 88 | 
            +
                # @param [Symbol] parse_method
         | 
| 89 | 
            +
                #   the parse method name
         | 
| 90 | 
            +
                #
         | 
| 91 | 
            +
                # @return [Array<Symbol>]
         | 
| 92 | 
            +
                #
         | 
| 93 | 
            +
                # @api private
         | 
| 94 | 
            +
                def parse_method_params(parse_method)
         | 
| 95 | 
            +
                  @yaml_parser.method(parse_method).parameters.map(&:last)
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                # Generate parser arguments
         | 
| 99 | 
            +
                #
         | 
| 100 | 
            +
                # @param [Array<Symbol>] parse_method_params
         | 
| 101 | 
            +
                #   the parse method parameters
         | 
| 102 | 
            +
                #
         | 
| 103 | 
            +
                # @return [Array<Object>]
         | 
| 104 | 
            +
                #
         | 
| 105 | 
            +
                # @api private
         | 
| 106 | 
            +
                def parser_arguments(parse_method_params)
         | 
| 107 | 
            +
                  return [] unless parse_method_params.include?(WHITELIST_CLASSES_PARAMETER)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  [@permitted_classes]
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                # Generate parser options
         | 
| 113 | 
            +
                #
         | 
| 114 | 
            +
                # @param [Array<Symbol>] parse_method_params
         | 
| 115 | 
            +
                #   the parse method parameters
         | 
| 116 | 
            +
                #
         | 
| 117 | 
            +
                # @return [Hash{Symbol => Object}]
         | 
| 118 | 
            +
                #
         | 
| 119 | 
            +
                # @api private
         | 
| 120 | 
            +
                def parser_options(parse_method_params)
         | 
| 121 | 
            +
                  {}.tap do |opts|
         | 
| 122 | 
            +
                    if parse_method_params.include?(PERMITTED_CLASSES_PARAMETER)
         | 
| 123 | 
            +
                      opts[:permitted_classes] = @permitted_classes
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    if parse_method_params.include?(SYMBOLIZE_NAMES_PARAMETER)
         | 
| 127 | 
            +
                      opts[:symbolize_names] = @symbolize_names
         | 
| 128 | 
            +
                    end
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                # Check whether the YAML parser can symbolize names or not
         | 
| 133 | 
            +
                #
         | 
| 134 | 
            +
                # @param [Hash{Symbol => Object}] parse_options
         | 
| 135 | 
            +
                #   the parse method options
         | 
| 136 | 
            +
                #
         | 
| 137 | 
            +
                # @return [Boolean]
         | 
| 138 | 
            +
                #
         | 
| 139 | 
            +
                # @api private
         | 
| 140 | 
            +
                def symbolize_names?(parse_options)
         | 
| 141 | 
            +
                  parse_options.key?(:symbolize_names)
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                # Symbolize metadata keys
         | 
| 145 | 
            +
                #
         | 
| 146 | 
            +
                # @param [Object] object
         | 
| 147 | 
            +
                #   the object with keys to symbolize
         | 
| 148 | 
            +
                #
         | 
| 149 | 
            +
                # @return [Hash{Symbol => Object}]
         | 
| 150 | 
            +
                #
         | 
| 151 | 
            +
                # @api private
         | 
| 152 | 
            +
                def symbolize_keys(object)
         | 
| 153 | 
            +
                  case object
         | 
| 154 | 
            +
                  when Hash then symbolize_hash_keys(object)
         | 
| 155 | 
            +
                  when Array then symbolize_array_hashes(object)
         | 
| 156 | 
            +
                  else object
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                # Symbolize hash keys
         | 
| 161 | 
            +
                #
         | 
| 162 | 
            +
                # @param [Hash] object
         | 
| 163 | 
            +
                #   the hash object with keys to symbolize
         | 
| 164 | 
            +
                #
         | 
| 165 | 
            +
                # @return [Hash{Symbol => Object}]
         | 
| 166 | 
            +
                #
         | 
| 167 | 
            +
                # @api private
         | 
| 168 | 
            +
                def symbolize_hash_keys(object)
         | 
| 169 | 
            +
                  object.each_with_object({}) do |(key, val), new_hash|
         | 
| 170 | 
            +
                    new_hash[key.to_sym] = symbolize_keys(val)
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                # Symbolize array hash values
         | 
| 175 | 
            +
                #
         | 
| 176 | 
            +
                # @param [Array] object
         | 
| 177 | 
            +
                #   the array object with hash values to symbolize
         | 
| 178 | 
            +
                #
         | 
| 179 | 
            +
                # @return [Array<Object>]
         | 
| 180 | 
            +
                #
         | 
| 181 | 
            +
                # @api private
         | 
| 182 | 
            +
                def symbolize_array_hashes(object)
         | 
| 183 | 
            +
                  object.each_with_object([]) do |val, new_array|
         | 
| 184 | 
            +
                    new_array << symbolize_keys(val)
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
              end # MetadataParser
         | 
| 188 | 
            +
            end # Slideck
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Slideck
         | 
| 4 | 
            +
              # Responsible for wrapping parsed global and slide metadata
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @api private
         | 
| 7 | 
            +
              class MetadataWrapper
         | 
| 8 | 
            +
                # Create a MetadataWrapper instance
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @example
         | 
| 11 | 
            +
                #   MetadataWrapper.new(metadata, metadata_converter, metadata_defaults)
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @param [Slideck::Metadata] metadata
         | 
| 14 | 
            +
                #   the metadata initialiser
         | 
| 15 | 
            +
                # @param [Slideck::MetadataConverter] metadata_converter
         | 
| 16 | 
            +
                #   the metadata converter
         | 
| 17 | 
            +
                # @param [Slideck::MetadataDefaults] metadata_defaults
         | 
| 18 | 
            +
                #   the metadata defaults
         | 
| 19 | 
            +
                #
         | 
| 20 | 
            +
                # @api public
         | 
| 21 | 
            +
                def initialize(metadata, metadata_converter, metadata_defaults)
         | 
| 22 | 
            +
                  @metadata = metadata
         | 
| 23 | 
            +
                  @metadata_converter = metadata_converter
         | 
| 24 | 
            +
                  @metadata_defaults = metadata_defaults
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                # Wrap parsed global and slide metadata
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @example
         | 
| 30 | 
            +
                #   metadata_wrapper.wrap({metadata: {}, slides: []})
         | 
| 31 | 
            +
                #
         | 
| 32 | 
            +
                # @param [Hash{Symbol => Hash, String}] deck
         | 
| 33 | 
            +
                #   the deck of parsed metadata and slides
         | 
| 34 | 
            +
                #
         | 
| 35 | 
            +
                # @return [Array<Slideck::Metadata, Hash>]
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @api public
         | 
| 38 | 
            +
                def wrap(deck)
         | 
| 39 | 
            +
                  [
         | 
| 40 | 
            +
                    build_metadata(deck[:metadata], @metadata_defaults),
         | 
| 41 | 
            +
                    deck[:slides].map do |slide|
         | 
| 42 | 
            +
                      {
         | 
| 43 | 
            +
                        content: slide[:content],
         | 
| 44 | 
            +
                        metadata: build_metadata(slide[:metadata], {})
         | 
| 45 | 
            +
                      }
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  ]
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                private
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # Build metadata
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @param [Hash{Symbol => Object}] custom_metadata
         | 
| 55 | 
            +
                #   the custom metadata
         | 
| 56 | 
            +
                # @param [#merge] defaults
         | 
| 57 | 
            +
                #   the defaults to merge with
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @return [Slideck::Metadata]
         | 
| 60 | 
            +
                #
         | 
| 61 | 
            +
                # @api private
         | 
| 62 | 
            +
                def build_metadata(custom_metadata, defaults)
         | 
| 63 | 
            +
                  @metadata.from(@metadata_converter, custom_metadata, defaults)
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end # MetadataWrapper
         | 
| 66 | 
            +
            end # Slideck
         | 
| @@ -0,0 +1,139 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Slideck
         | 
| 4 | 
            +
              # Responsible for extracting metadata and slides from content
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @api private
         | 
| 7 | 
            +
              class Parser
         | 
| 8 | 
            +
                # The pattern to detect metadata configuration
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @return [Regexp]
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # @api private
         | 
| 13 | 
            +
                METADATA_PATTERN = /^\s*:?[^:]+:[^:]+/.freeze
         | 
| 14 | 
            +
                private_constant :METADATA_PATTERN
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # The pattern to detect slide separator
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @return [Regexp]
         | 
| 19 | 
            +
                #
         | 
| 20 | 
            +
                # @api private
         | 
| 21 | 
            +
                SLIDE_SEPARATOR = /\n?-{3,}([^\n]*)\n/.freeze
         | 
| 22 | 
            +
                private_constant :SLIDE_SEPARATOR
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                # The pattern to match entire lines
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @return [Regexp]
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @api private
         | 
| 29 | 
            +
                LINE_PATTERN = /^[^\n]+$/.freeze
         | 
| 30 | 
            +
                private_constant :LINE_PATTERN
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Create a Parser instance
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                # @example
         | 
| 35 | 
            +
                #   Parser.new(StringScanner, Slideck::MetadataParser)
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @param [StringScanner] string_scanner
         | 
| 38 | 
            +
                #   the content scanner
         | 
| 39 | 
            +
                # @param [Slideck::MetadataParser] metadata_parser
         | 
| 40 | 
            +
                #   the metadata parser
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @api public
         | 
| 43 | 
            +
                def initialize(string_scanner, metadata_parser)
         | 
| 44 | 
            +
                  @string_scanner = string_scanner
         | 
| 45 | 
            +
                  @metadata_parser = metadata_parser
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Parse metadata and slides from content
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # @example
         | 
| 51 | 
            +
                #   parser.parse("align: center\n---\nSlide1\n---\nSlide2\n---")
         | 
| 52 | 
            +
                #
         | 
| 53 | 
            +
                # @param [String] content
         | 
| 54 | 
            +
                #   the content to parse slides from
         | 
| 55 | 
            +
                #
         | 
| 56 | 
            +
                # @return [Hash{Symbol => Hash, Array<String>}]
         | 
| 57 | 
            +
                #   the metadata and slides content
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @api public
         | 
| 60 | 
            +
                def parse(content)
         | 
| 61 | 
            +
                  scanner = @string_scanner.new(content)
         | 
| 62 | 
            +
                  slides = split_into_slides(scanner)
         | 
| 63 | 
            +
                  metadata = extract_metadata(slides.first && slides.first[:content])
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  {metadata: metadata, slides: metadata.empty? ? slides : slides[1..-1]}
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                private
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                # Split content into slides
         | 
| 71 | 
            +
                #
         | 
| 72 | 
            +
                # @param [StringScanner] scanner
         | 
| 73 | 
            +
                #   the slides content scanner
         | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
                # @return [Array<String>]
         | 
| 76 | 
            +
                #
         | 
| 77 | 
            +
                # @api private
         | 
| 78 | 
            +
                def split_into_slides(scanner)
         | 
| 79 | 
            +
                  slides, slide, slide_metadata = [], [], {}
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  until scanner.eos?
         | 
| 82 | 
            +
                    if scanner.scan(SLIDE_SEPARATOR)
         | 
| 83 | 
            +
                      slides = add_slide(slides, slide.join, slide_metadata)
         | 
| 84 | 
            +
                      slide_metadata = extract_metadata(scanner[1])
         | 
| 85 | 
            +
                      slide.clear
         | 
| 86 | 
            +
                    elsif scanner.scan(LINE_PATTERN)
         | 
| 87 | 
            +
                      slide << scanner.matched
         | 
| 88 | 
            +
                    else
         | 
| 89 | 
            +
                      slide << scanner.getch
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  add_slide(slides, slide.join.chomp, slide_metadata)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                # Add a slide to slides
         | 
| 97 | 
            +
                #
         | 
| 98 | 
            +
                # @param [Array<String>] slides
         | 
| 99 | 
            +
                #   the slides array
         | 
| 100 | 
            +
                # @param [String] slide
         | 
| 101 | 
            +
                #   the slide to add to slides
         | 
| 102 | 
            +
                # @param [Hash{String, Symbol => Object}] slide_metadata
         | 
| 103 | 
            +
                #   the slide metadata
         | 
| 104 | 
            +
                #
         | 
| 105 | 
            +
                # @return [Array<Hash{Symbol => Hash, String}>]
         | 
| 106 | 
            +
                #
         | 
| 107 | 
            +
                # @api private
         | 
| 108 | 
            +
                def add_slide(slides, slide, slide_metadata)
         | 
| 109 | 
            +
                  return slides if slide.empty?
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  slides + [{content: slide, metadata: slide_metadata}]
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                # Extract metadata from a slide
         | 
| 115 | 
            +
                #
         | 
| 116 | 
            +
                # @param [String, nil] slide
         | 
| 117 | 
            +
                #
         | 
| 118 | 
            +
                # @return [Hash]
         | 
| 119 | 
            +
                #
         | 
| 120 | 
            +
                # @api private
         | 
| 121 | 
            +
                def extract_metadata(slide)
         | 
| 122 | 
            +
                  return {} if slide.nil? || !metadata_given?(slide)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  @metadata_parser.parse(slide)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                # Check whether or not metadata is given
         | 
| 128 | 
            +
                #
         | 
| 129 | 
            +
                # @param [String] content
         | 
| 130 | 
            +
                #   the slide content to check
         | 
| 131 | 
            +
                #
         | 
| 132 | 
            +
                # @return [Boolean]
         | 
| 133 | 
            +
                #
         | 
| 134 | 
            +
                # @api private
         | 
| 135 | 
            +
                def metadata_given?(content)
         | 
| 136 | 
            +
                  !(content.lines.first =~ METADATA_PATTERN).nil?
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
              end # Parser
         | 
| 139 | 
            +
            end # Slideck
         | 
| @@ -0,0 +1,272 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Slideck
         | 
| 4 | 
            +
              # Responsible for presenting slides
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @api private
         | 
| 7 | 
            +
              class Presenter
         | 
| 8 | 
            +
                # Terminal screen size change signal
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @return [String]
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # @api private
         | 
| 13 | 
            +
                TERM_SCREEN_SIZE_CHANGE_SIG = "WINCH"
         | 
| 14 | 
            +
                private_constant :TERM_SCREEN_SIZE_CHANGE_SIG
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # Create a Presenter
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @param [TTY::Reader] reader
         | 
| 19 | 
            +
                #   the keyboard input reader
         | 
| 20 | 
            +
                # @param [Slideck::Renderer] renderer
         | 
| 21 | 
            +
                #   the slides renderer
         | 
| 22 | 
            +
                # @param [Slideck::Tracker] tracker
         | 
| 23 | 
            +
                #   the tracker for slides
         | 
| 24 | 
            +
                # @param [TTY::Screen] screen
         | 
| 25 | 
            +
                #   the terminal screen size
         | 
| 26 | 
            +
                # @param [IO] output
         | 
| 27 | 
            +
                #   the output stream for the slides
         | 
| 28 | 
            +
                # @param [Proc] reloader
         | 
| 29 | 
            +
                #   the metadata and slides reloader
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @api public
         | 
| 32 | 
            +
                def initialize(reader, renderer, tracker, screen, output, &reloader)
         | 
| 33 | 
            +
                  @reader = reader
         | 
| 34 | 
            +
                  @renderer = renderer
         | 
| 35 | 
            +
                  @tracker = tracker
         | 
| 36 | 
            +
                  @screen = screen
         | 
| 37 | 
            +
                  @output = output
         | 
| 38 | 
            +
                  @reloader = reloader
         | 
| 39 | 
            +
                  @stop = false
         | 
| 40 | 
            +
                  @buffer = []
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                # Reload presentation
         | 
| 44 | 
            +
                #
         | 
| 45 | 
            +
                # @example
         | 
| 46 | 
            +
                #   presenter.reload
         | 
| 47 | 
            +
                #
         | 
| 48 | 
            +
                # @return [Slideck::Presenter]
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # @api public
         | 
| 51 | 
            +
                def reload
         | 
| 52 | 
            +
                  @metadata, @slides = *@reloader.()
         | 
| 53 | 
            +
                  @tracker = @tracker.resize(@slides.size)
         | 
| 54 | 
            +
                  self
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                # Start presentation
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @example
         | 
| 60 | 
            +
                #   presenter.start
         | 
| 61 | 
            +
                #
         | 
| 62 | 
            +
                # @return [void]
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                # @api public
         | 
| 65 | 
            +
                def start
         | 
| 66 | 
            +
                  reload
         | 
| 67 | 
            +
                  @reader.subscribe(self)
         | 
| 68 | 
            +
                  hide_cursor
         | 
| 69 | 
            +
                  subscribe_to_screen_resize { resize.render }
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  until @stop
         | 
| 72 | 
            +
                    render
         | 
| 73 | 
            +
                    @reader.read_keypress
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                ensure
         | 
| 76 | 
            +
                  show_cursor
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                # Stop presentation
         | 
| 80 | 
            +
                #
         | 
| 81 | 
            +
                # @example
         | 
| 82 | 
            +
                #   presenter.stop
         | 
| 83 | 
            +
                #
         | 
| 84 | 
            +
                # @return [Slideck::Presenter]
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # @api public
         | 
| 87 | 
            +
                def stop
         | 
| 88 | 
            +
                  @stop = true
         | 
| 89 | 
            +
                  self
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                # Render presentation on cleared screen
         | 
| 93 | 
            +
                #
         | 
| 94 | 
            +
                # @example
         | 
| 95 | 
            +
                #   presenter.render
         | 
| 96 | 
            +
                #
         | 
| 97 | 
            +
                # @return [void]
         | 
| 98 | 
            +
                #
         | 
| 99 | 
            +
                # @api public
         | 
| 100 | 
            +
                def render
         | 
| 101 | 
            +
                  clear_screen
         | 
| 102 | 
            +
                  render_slide
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                # Clear terminal screen
         | 
| 106 | 
            +
                #
         | 
| 107 | 
            +
                # @return [void]
         | 
| 108 | 
            +
                #
         | 
| 109 | 
            +
                # @api private
         | 
| 110 | 
            +
                def clear_screen
         | 
| 111 | 
            +
                  @output.print @renderer.clear
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                # Render the current slide
         | 
| 115 | 
            +
                #
         | 
| 116 | 
            +
                # @return [void]
         | 
| 117 | 
            +
                #
         | 
| 118 | 
            +
                # @api private
         | 
| 119 | 
            +
                def render_slide
         | 
| 120 | 
            +
                  @output.print @renderer.render(
         | 
| 121 | 
            +
                    @metadata,
         | 
| 122 | 
            +
                    @slides[@tracker.current],
         | 
| 123 | 
            +
                    @tracker.current + 1,
         | 
| 124 | 
            +
                    @tracker.total)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                # Hide cursor
         | 
| 128 | 
            +
                #
         | 
| 129 | 
            +
                # @return [void]
         | 
| 130 | 
            +
                #
         | 
| 131 | 
            +
                # @api private
         | 
| 132 | 
            +
                def hide_cursor
         | 
| 133 | 
            +
                  @output.print @renderer.cursor.hide
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                # Show cursor
         | 
| 137 | 
            +
                #
         | 
| 138 | 
            +
                # @return [void]
         | 
| 139 | 
            +
                #
         | 
| 140 | 
            +
                # @api private
         | 
| 141 | 
            +
                def show_cursor
         | 
| 142 | 
            +
                  @output.print @renderer.cursor.show
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                # Subscribe to the terminal screen size change signal
         | 
| 146 | 
            +
                #
         | 
| 147 | 
            +
                # @param [Proc] resizer
         | 
| 148 | 
            +
                #   the presentation resizer
         | 
| 149 | 
            +
                #
         | 
| 150 | 
            +
                # @return [void]
         | 
| 151 | 
            +
                #
         | 
| 152 | 
            +
                # @api private
         | 
| 153 | 
            +
                def subscribe_to_screen_resize(&resizer)
         | 
| 154 | 
            +
                  return if @screen.windows?
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  Signal.trap(TERM_SCREEN_SIZE_CHANGE_SIG, &resizer)
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                # Resize presentation
         | 
| 160 | 
            +
                #
         | 
| 161 | 
            +
                # @return [Slideck::Presenter]
         | 
| 162 | 
            +
                #
         | 
| 163 | 
            +
                # @api private
         | 
| 164 | 
            +
                def resize
         | 
| 165 | 
            +
                  @renderer = @renderer.resize(@screen.width, @screen.height)
         | 
| 166 | 
            +
                  self
         | 
| 167 | 
            +
                end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                # Handle a keypress event
         | 
| 170 | 
            +
                #
         | 
| 171 | 
            +
                # @param [TTY::Reader::KeyEvent] event
         | 
| 172 | 
            +
                #   the key event
         | 
| 173 | 
            +
                #
         | 
| 174 | 
            +
                # @return [void]
         | 
| 175 | 
            +
                #
         | 
| 176 | 
            +
                # @api private
         | 
| 177 | 
            +
                def keypress(event)
         | 
| 178 | 
            +
                  case event.value
         | 
| 179 | 
            +
                  when "n", "l" then keyright
         | 
| 180 | 
            +
                  when "p", "h" then keyleft
         | 
| 181 | 
            +
                  when "f", "^" then go_to_first
         | 
| 182 | 
            +
                  when "t", "$" then go_to_last
         | 
| 183 | 
            +
                  when "g" then go_to_slide
         | 
| 184 | 
            +
                  when /\d/ then add_to_buffer(event.value)
         | 
| 185 | 
            +
                  when "r" then keyctrl_l
         | 
| 186 | 
            +
                  when "q" then keyctrl_x
         | 
| 187 | 
            +
                  end
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                # Navigate to the next slide
         | 
| 191 | 
            +
                #
         | 
| 192 | 
            +
                # @return [void]
         | 
| 193 | 
            +
                #
         | 
| 194 | 
            +
                # @api private
         | 
| 195 | 
            +
                def keyright(*)
         | 
| 196 | 
            +
                  @tracker = @tracker.next
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
                alias keyspace keyright
         | 
| 199 | 
            +
                alias keypage_down keyright
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                # Navigate to the previous slide
         | 
| 202 | 
            +
                #
         | 
| 203 | 
            +
                # @return [void]
         | 
| 204 | 
            +
                #
         | 
| 205 | 
            +
                # @api private
         | 
| 206 | 
            +
                def keyleft(*)
         | 
| 207 | 
            +
                  @tracker = @tracker.previous
         | 
| 208 | 
            +
                end
         | 
| 209 | 
            +
                alias keybackspace keyleft
         | 
| 210 | 
            +
                alias keypage_up keyleft
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                # Reload presentation
         | 
| 213 | 
            +
                #
         | 
| 214 | 
            +
                # @return [void]
         | 
| 215 | 
            +
                #
         | 
| 216 | 
            +
                # @api private
         | 
| 217 | 
            +
                def keyctrl_l(*)
         | 
| 218 | 
            +
                  reload
         | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                # Exit presentation
         | 
| 222 | 
            +
                #
         | 
| 223 | 
            +
                # @return [void]
         | 
| 224 | 
            +
                #
         | 
| 225 | 
            +
                # @api private
         | 
| 226 | 
            +
                def keyctrl_x(*)
         | 
| 227 | 
            +
                  clear_screen
         | 
| 228 | 
            +
                  stop
         | 
| 229 | 
            +
                end
         | 
| 230 | 
            +
                alias keyescape keyctrl_x
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                # Navigate to the fist slide
         | 
| 233 | 
            +
                #
         | 
| 234 | 
            +
                # @return [void]
         | 
| 235 | 
            +
                #
         | 
| 236 | 
            +
                # @api private
         | 
| 237 | 
            +
                def go_to_first
         | 
| 238 | 
            +
                  @tracker = @tracker.first
         | 
| 239 | 
            +
                end
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                # Navigate to the last slide
         | 
| 242 | 
            +
                #
         | 
| 243 | 
            +
                # @return [void]
         | 
| 244 | 
            +
                #
         | 
| 245 | 
            +
                # @api private
         | 
| 246 | 
            +
                def go_to_last
         | 
| 247 | 
            +
                  @tracker = @tracker.last
         | 
| 248 | 
            +
                end
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                # Navigate to a given slide
         | 
| 251 | 
            +
                #
         | 
| 252 | 
            +
                # @return [void]
         | 
| 253 | 
            +
                #
         | 
| 254 | 
            +
                # @api private
         | 
| 255 | 
            +
                def go_to_slide
         | 
| 256 | 
            +
                  @tracker = @tracker.go_to(@buffer.join.to_i - 1)
         | 
| 257 | 
            +
                  @buffer.clear
         | 
| 258 | 
            +
                end
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                # Add to the input buffer
         | 
| 261 | 
            +
                #
         | 
| 262 | 
            +
                # @param [String] input_key
         | 
| 263 | 
            +
                #   the input key
         | 
| 264 | 
            +
                #
         | 
| 265 | 
            +
                # @return [void]
         | 
| 266 | 
            +
                #
         | 
| 267 | 
            +
                # @api private
         | 
| 268 | 
            +
                def add_to_buffer(input_key)
         | 
| 269 | 
            +
                  @buffer += [input_key]
         | 
| 270 | 
            +
                end
         | 
| 271 | 
            +
              end # Presenter
         | 
| 272 | 
            +
            end # Slideck
         |