lookbooklet 0.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +234 -0
  4. data/bin/booklet +6 -0
  5. data/lib/booklet/analyzer.rb +28 -0
  6. data/lib/booklet/cli/cli.rb +14 -0
  7. data/lib/booklet/cli/commands/analyze_command.rb +45 -0
  8. data/lib/booklet/cli/commands/version_command.rb +13 -0
  9. data/lib/booklet/cli/concerns/colorful.rb +51 -0
  10. data/lib/booklet/concerns/locatable.rb +22 -0
  11. data/lib/booklet/file.rb +90 -0
  12. data/lib/booklet/helpers.rb +19 -0
  13. data/lib/booklet/node.rb +297 -0
  14. data/lib/booklet/nodes/anon_node.rb +9 -0
  15. data/lib/booklet/nodes/asset_node.rb +15 -0
  16. data/lib/booklet/nodes/directory_node.rb +17 -0
  17. data/lib/booklet/nodes/document_node.rb +11 -0
  18. data/lib/booklet/nodes/file_node.rb +15 -0
  19. data/lib/booklet/nodes/folder_node.rb +13 -0
  20. data/lib/booklet/nodes/prose_node.rb +7 -0
  21. data/lib/booklet/nodes/scenario_node.rb +13 -0
  22. data/lib/booklet/nodes/spec_node.rb +19 -0
  23. data/lib/booklet/object.rb +23 -0
  24. data/lib/booklet/options.rb +31 -0
  25. data/lib/booklet/value.rb +16 -0
  26. data/lib/booklet/values/code_snippet.rb +25 -0
  27. data/lib/booklet/values/method_snippet.rb +20 -0
  28. data/lib/booklet/values/node_type.rb +55 -0
  29. data/lib/booklet/values/parser_result.rb +11 -0
  30. data/lib/booklet/values/source_location.rb +17 -0
  31. data/lib/booklet/values/text_snippet.rb +21 -0
  32. data/lib/booklet/values.rb +31 -0
  33. data/lib/booklet/version.rb +5 -0
  34. data/lib/booklet/visitor.rb +58 -0
  35. data/lib/booklet/visitors/ascii_tree_renderer.rb +36 -0
  36. data/lib/booklet/visitors/entity_transformer.rb +18 -0
  37. data/lib/booklet/visitors/filesystem_loader.rb +18 -0
  38. data/lib/booklet/visitors/frontmatter_extractor.rb +10 -0
  39. data/lib/booklet/visitors/preview_class_parser.rb +42 -0
  40. data/lib/booklet/yard_parser.rb +23 -0
  41. data/lib/booklet.rb +29 -0
  42. data/lib/lookbooklet.rb +3 -0
  43. metadata +182 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0068d04263ac310030a31b1269136504f3149292365ecf2b230ff5a0dcb50f80'
4
+ data.tar.gz: 4b9d16e8e6b9a340c52f61ea95fa7e4b260e2d927e60c3955cdefb9363f1ddb6
5
+ SHA512:
6
+ metadata.gz: 7ddb7794ef885c0c23991692bf096b79fad733a494737a20d3db02656154741b45f1bf41bf2d799a1b66fffad08a6967db24bb1ed60b20c326624af4ed2c1fd3
7
+ data.tar.gz: dc2286fe4440f0d391fb9be637eb0d655d8c92e4b86c2ee7c60375940f22275f9303eaa32c8479e4730e40167012a0bb3e0f689ef30973aa1d6abfbe743fa1bb
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Mark Perkins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # Booklet
2
+
3
+ An experimental new standalone, extendable **parser-analyzer engine** for [Lookbook](https://lookbook.build).
4
+
5
+ The aim is for Booklet to eventually replace the existing file parsing/analyzing code in Lookbook and thus provide a more robust foundation for future releases to build upon.
6
+
7
+ Additionally, Booklet is being developed as a standalone gem in part so that it can also be used as a springboard for building additional tools (CLIs, MCP servers etc) to help Lookbook integrate more seamlessly into a variety of Rails frontend development workflows.
8
+
9
+ > [!WARNING]
10
+ > Booklet is in a very early stage of development and is **not ready for public use.**<br>It is currently incomplete and will likely see many breaking changes before a stable release candidate is available.
11
+
12
+
13
+ ### Aims and objectives
14
+
15
+ * Use common, tried-and-tested parser/transformer implementation patterns over custom workflows where possible.
16
+ * Implement the analyzing process as a series of small, incremental steps to aid comprehension and testabilty.
17
+ * Be backwards compatable with existing Lookbook parser behaviour as surfaced in the app (although underlying APIs and conceptual definitions may differ).
18
+ * Make it straightforward to customise and extend the processing pipeline using plugins/middleware.
19
+ * Minimise any 'special-casing' of core functionality over that provided by third party extensions.
20
+ * Do not have any dependency on Rails (outside of conveniences provided by the `ActiveSupport` gem).
21
+
22
+ ### Development status
23
+
24
+ Booklet is a brand new project and is not yet ready for public use.
25
+
26
+ The [issues list](https://github.com/lookbook-hq/booklet/issues) contains an (incomplete) set of work that is planned before an initial beta release will be made available.
27
+
28
+ Issues specifically related to achieving compatability with Lookbook's current parser output are tagged with the `compatability` label and the [Lookbook compatability](https://github.com/orgs/lookbook-hq/projects/3) project board has been set up to track progress on this metric.
29
+
30
+ ## Implementation details
31
+
32
+ Booklet generates a **traversable tree of entity node objects** from an input directory of files, via a number of intermediate steps.
33
+
34
+ Trees can contain a number of different node types. These include **folder nodes**, **file-based entity nodes** and entity **child nodes** that represent the contents of key resource types.
35
+
36
+ The hierarchy of the nodes in the tree broadly reflects the grouping of input files into folders and subfolders within the root directory as well as parent-child entity-content relationships where present.
37
+
38
+ All tree mutations and transformations are performed by ['double dispatch'-style](https://www.bigbinary.com/blog/visitor-pattern-and-double-dispatch) **node visitors**.
39
+
40
+ ### File processing pipeline
41
+
42
+ Booklet breaks up the processing of files into four steps:
43
+
44
+ 1. File tree creation
45
+ 2. File tree mutation
46
+ 3. File tree &rarr; entity tree transformation
47
+ 4. Entity tree mutation
48
+
49
+ #### 1. File tree creation
50
+
51
+ In the first step a tree of generic file and directory nodes is constructed by recursively scanning the root directory and adding a node for every file and folder found. This is handled by the [`FilesystemLoader` visitor](./lib/booklet/visitors/filesystem_loader.rb).
52
+
53
+ ```ruby
54
+ file_tree = DirectoryNode.from("test/fixtures/demo").accept(FilesystemLoader.new)
55
+ ```
56
+
57
+ <details>
58
+ <summary>Resulting file tree</summary>
59
+
60
+ > _ASCII tree visualisation generated using the [`AsciiTreeRenderer` visitor](./lib/booklet/visitors/ascii_tree_renderer.rb)_
61
+
62
+ ```
63
+ [DirectoryNode] demo
64
+ ├── [DirectoryNode] docs
65
+ │ ├── [FileNode] _tmp_notes.txt
66
+ │ ├── [FileNode] banner.png
67
+ │ ├── [FileNode] overview.md
68
+ │ ├── [FileNode] resources.md
69
+ │ └── [DirectoryNode] usage
70
+ │ ├── [FileNode] getting_started.md
71
+ │ ├── [FileNode] installation.md
72
+ │ └── [FileNode] screenshot.svg
73
+ └── [DirectoryNode] view_specs
74
+ ├── [DirectoryNode] elements
75
+ │ ├── [FileNode] button_component_spec.rb
76
+ │ └── [FileNode] card_component_spec.rb
77
+ ├── [FileNode] helpers_spec.rb
78
+ └── [DirectoryNode] layouts
79
+ ├── [FileNode] article_spec.rb
80
+ └── [FileNode] landing_page_spec.rb
81
+ ```
82
+ </details>
83
+
84
+ #### 2. File tree mutation
85
+
86
+ This is an optional step where additional node visitors can be provided to mutate the file tree before it is handed off to the entity transformer.
87
+
88
+ For example you might want to create a custom Visitor class that removes all files with names beginning with an underscore (such as `_tmp_notes.txt` in the example above) to clean up the tree before the entity transformer is run.
89
+
90
+ ```ruby
91
+ file_tree.accept(SomeCustomFileTreeProcessor.new)
92
+ ```
93
+
94
+ #### 3. File tree &rarr; Entity tree transformation
95
+
96
+ In this step the [`EntityTransformer`](./lib/booklet/visitors/entity_transformer.rb) visitor is applied to the raw file tree. The transformer visits each of the generic file/directory nodes in the file tree and converts all 'recognized' file types to their corresponding Lookbook entity node type.
97
+
98
+ For example, files with `.md` extensions are transformed into `DocumentNode` instances, whilst component preview class files (names ending in `_preview.rb`) are transformed into `SpecNode` instances.
99
+
100
+ ```ruby
101
+ entity_tree = file_tree.accept(EntityTransformer.new)
102
+ ```
103
+
104
+ <details>
105
+ <summary>Resulting entity tree</summary>
106
+
107
+ ```
108
+ [FolderNode] Demo
109
+ ├── [FolderNode] Docs
110
+ │ ├── [AnonNode] _tmp_notes.txt
111
+ │ ├── [AssetNode] banner.png
112
+ │ ├── [DocumentNode] Overview
113
+ │ ├── [DocumentNode] Resources
114
+ │ └── [FolderNode] Usage
115
+ │ ├── [DocumentNode] Getting Started
116
+ │ ├── [DocumentNode] Installation
117
+ │ └── [AssetNode] screenshot.svg
118
+ └── [FolderNode] Previews
119
+ ├── [FolderNode] Elements
120
+ │ ├── [SpecNode] Button Component Preview
121
+ │ └── [SpecNode] Card Component Preview
122
+ ├── [SpecNode] Helpers Preview
123
+ └── [FolderNode] Layouts
124
+ ├── [SpecNode] Article Preview
125
+ └── [SpecNode] Landing Page Preview
126
+ ```
127
+ </details>
128
+
129
+
130
+ #### 4. Entity tree mutation
131
+
132
+ This final step is where enity node visitors can be applied to perform tasks such as parsing file contents and generally 'building out' the skeleton entity node objects created in the previous step.
133
+
134
+ By default Booklet will apply the [`PreviewClassParser`](./lib/booklet/visitors/preview_class_parser.rb) and [`FrontmatterExtractor`](./lib/booklet/visitors/frontmatter_extractor.rb) visitors at this stage.
135
+
136
+ ```ruby
137
+ entity_tree
138
+ .accept(PreviewClassParser.new)
139
+ .accept(FrontMatterExtractor.new)
140
+ ```
141
+
142
+ * The `PreviewClassParser` visitor uses the [YARD parser](https://yardoc.org/) to extract annotations data from preview class files and creates and appends corresponding `ScenarioNode` and `ProseNode` children to the appropriate `SpecNode` instance.
143
+ * The `FrontmatterExtractor` visitor _(not yet implemented!)_ extracts YAML-formatted 'frontmatter' from the contents of markdown files and updates the related `DocumentNode` instances with the parsed data.
144
+
145
+ Additional entity node vistors can be applied here as needed to make changes to the entity tree nodes before the finalised entity tree is returned for use by the calling code.
146
+
147
+ <details>
148
+ <summary>Final entity tree</summary>
149
+
150
+ _Note that the `docs` branch has been omitted for brevity._
151
+
152
+ ```
153
+ ...
154
+ └── [FolderNode] Previews
155
+ ├── [FolderNode] Elements
156
+ │ ├── [SpecNode] Button Component Preview
157
+ │ │ ├── [ScenarioNode] Default
158
+ │ │ ├── [ScenarioNode] Secondary
159
+ │ │ └── [ScenarioNode] Danger
160
+ │ └── [SpecNode] Card Component Preview
161
+ │ ├── [ScenarioNode] No Title
162
+ │ └── [ScenarioNode] With Title
163
+ ├── [SpecNode] Helpers Preview
164
+ │ ├── [ScenarioNode] Blah Generator
165
+ │ └── [ScenarioNode] Char Tag Wrapper
166
+ └── [FolderNode] Layouts
167
+ ├── [SpecNode] Article Preview
168
+ │ └── [ScenarioNode] Default
169
+ └── [SpecNode] Landing Page Preview
170
+ └── [ScenarioNode] Default
171
+ ```
172
+
173
+ </details>
174
+
175
+ ## Installation
176
+
177
+ > [!IMPORTANT]
178
+ > Booklet is not yet ready for public use - these instructions are for illustrative purposes only at this point.
179
+ > See the [Development status](#development-status) section for more info.
180
+
181
+ Booklet is both a **command line tool** and a **library**.
182
+
183
+ ### Using as a dependency
184
+
185
+ Add Booklet to your Gemfile:
186
+
187
+ ```ruby
188
+ gem "lookbooklet"
189
+ ```
190
+
191
+ After running `bundle install` you can then make use of the Booklet API in your codebase:
192
+
193
+ ```ruby
194
+ require "lookbooklet"
195
+
196
+ result = Booklet.analyze("path/to/root/directory")
197
+ ```
198
+
199
+ ### CLI interface
200
+
201
+ To use the booklet CLI you can install the gem globally:
202
+
203
+ ```sh
204
+ gem install lookbooklet
205
+ ```
206
+
207
+ You can then view the available booklet CLI commands using:
208
+
209
+ ```sh
210
+ booklet -h
211
+ ```
212
+
213
+ ## API
214
+
215
+ > _Details coming soon._
216
+
217
+
218
+ ## Testing
219
+
220
+ Booklet uses Minitest for its test framework.
221
+
222
+ Run the tests:
223
+
224
+ ```sh
225
+ bin/test
226
+ ```
227
+
228
+ ## Acknowlegments
229
+
230
+ [Marco Roth](https://marcoroth.dev/)'s fantastic work on [Herb](https://herb-tools.dev/) (and my subsequent deep-dive into the world of ASTs) was been instrumental in sparking the initial idea for Booklet and for shaping its approach.
231
+
232
+ Booklet's double-dispatch style node visitor base class is based on the very nice [BasicVisitor](https://github.com/yippee-fun/refract/blob/main/lib/refract/basic_visitor.rb) class from the [Refract gem](https://github.com/yippee-fun/refract).
233
+
234
+ In addition much of the original incarnation of Booklet's `Node` class was based on code adapted from the excellent [RubyTree](https://github.com/evolve75/RubyTree) gem.
data/bin/booklet ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "booklet"
5
+
6
+ Booklet::CLI.call
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class Analyzer < Booklet::Object
5
+ DEFAULT_FILE_VISITORS = []
6
+ DEFAULT_ENTITY_VISITORS = [
7
+ PreviewClassParser,
8
+ FrontmatterExtractor
9
+ ]
10
+
11
+ prop :loader, Visitor, default: -> { FilesystemLoader.new }
12
+ prop :after_load, _Array(Visitor), reader: :public, default: -> { DEFAULT_FILE_VISITORS.map(&:new) }
13
+
14
+ prop :transformer, Visitor, default: -> { EntityTransformer.new }
15
+ prop :after_transform, _Array(Visitor), reader: :public, default: -> { DEFAULT_ENTITY_VISITORS.map(&:new) }
16
+
17
+ def analyze(path)
18
+ path = Pathname(path.to_s).expand_path unless path.nil?
19
+ files = DirectoryNode.from(path).accept(@loader)
20
+ @after_load.each { files.accept(_1) }
21
+
22
+ entities = files.accept(@transformer)
23
+ @after_transform.each { entities.accept(_1) }
24
+
25
+ ParserResult.new(path:, files:, entities:)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ require "dry/cli"
2
+
3
+ module Booklet
4
+ module CLI
5
+ extend Dry::CLI::Registry
6
+
7
+ register "version", VersionCommand, aliases: ["v", "-v", "--version"]
8
+ register "analyze", AnalyzeCommand
9
+
10
+ def self.call
11
+ Dry::CLI.new(self).call
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class AnalyzeCommand < Dry::CLI::Command
5
+ include Colorful
6
+
7
+ desc "Analyze a directory of files and display a summary of the results"
8
+
9
+ argument :path, required: true, desc: "Root directory path"
10
+
11
+ def call(path:, **)
12
+ result = Booklet.analyze(path)
13
+
14
+ files = result.files.count
15
+ entities = result.entities.count
16
+ warnings = result.warnings.count
17
+ errors = result.errors.count
18
+
19
+ hr = "⎯" * 20
20
+
21
+ puts <<~RESULT
22
+
23
+ #{hr}
24
+
25
+ #{cyan("#{files} #{"file".pluralize(files)}...")}
26
+
27
+ #{result.files.accept(AsciiTreeRenderer.new(indent: 1))}
28
+
29
+ #{cyan("...converted into #{entities} #{"entity".pluralize(entities)}:")}
30
+
31
+ #{result.entities.accept(AsciiTreeRenderer.new(indent: 1))}
32
+
33
+ #{hr}
34
+
35
+ #{green("Booklet analysis complete", :underline)}
36
+
37
+ #{cyan("Root:".ljust(9))} #{path}
38
+ #{cyan(("File".pluralize(files) + ":").ljust(9))} #{files}
39
+ #{cyan(("Entity".pluralize(entities) + ":").ljust(9))} #{entities}
40
+ #{yellow(("Warning".pluralize(warnings) + ":").ljust(9))} #{warnings}
41
+ #{red(("Error".pluralize(errors) + ":").ljust(9))} #{errors}
42
+ RESULT
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ class VersionCommand < Dry::CLI::Command
5
+ include Colorful
6
+
7
+ desc "Print version"
8
+
9
+ def call(*)
10
+ puts "#{pink("booklet")} v#{VERSION}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "paint"
4
+
5
+ module Booklet
6
+ module Colorful
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def pink(str, *)
11
+ paint(str, "#FF1493", *)
12
+ end
13
+
14
+ def cyan(str, *)
15
+ paint(str, :cyan, *)
16
+ end
17
+
18
+ def magenta(str, *)
19
+ paint(str, :cyan, *)
20
+ end
21
+
22
+ def gray(str, *)
23
+ paint(str, :gray, *)
24
+ end
25
+
26
+ def green(str, *)
27
+ paint(str, :green, *)
28
+ end
29
+
30
+ def orange(str, *)
31
+ paint(str, "#FFA500", *)
32
+ end
33
+
34
+ def yellow(str, *)
35
+ paint(str, "#DAA520", *)
36
+ end
37
+
38
+ def gold(str, *)
39
+ paint(str, "#DAA520", *)
40
+ end
41
+
42
+ def red(str, *)
43
+ paint(str, :red, *)
44
+ end
45
+
46
+ def paint(*)
47
+ Paint[*]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ module Booklet
2
+ module Locatable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ prop :file, File, reader: :public, writer: false
7
+
8
+ delegate :name, :path, to: :file
9
+
10
+ def label
11
+ name.titleize
12
+ end
13
+ end
14
+
15
+ class_methods do
16
+ def from(file_or_path)
17
+ file = file_or_path.is_a?(File) ? file_or_path : File.new(file_or_path)
18
+ new(file.path, file:)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "marcel"
4
+
5
+ module Booklet
6
+ class File < Booklet::Object
7
+ prop :path, Pathname, :positional do |value|
8
+ Pathname(value.to_s) unless value.nil?
9
+ end
10
+
11
+ prop :contents, _Nilable(String), :positional
12
+
13
+ after_initialize do
14
+ raise ArgumentError, "File paths must be absolute" unless @path.absolute?
15
+ end
16
+
17
+ def path(strip_extension: false)
18
+ strip_extension ? Pathname(@path.to_s.delete_suffix(ext)) : @path
19
+ end
20
+
21
+ def relative_path(root = Dir.pwd, strip_extension: false)
22
+ path(strip_extension).relative_path_from(root)
23
+ end
24
+
25
+ def relative_path_segments
26
+ relative_path.to_s.split("/")
27
+ end
28
+
29
+ def ext
30
+ basename.to_s.gsub(/^([^.]+)/, "") unless directory?
31
+ end
32
+
33
+ def ext?(*extensions)
34
+ basename.end_with?(*extensions)
35
+ end
36
+
37
+ def name
38
+ (directory? ? basename : basename(ext)).to_s
39
+ end
40
+
41
+ def basename(*)
42
+ path.basename(*).to_s
43
+ end
44
+
45
+ def mime_type
46
+ Marcel::MimeType.for(path, name: basename) unless directory?
47
+ end
48
+
49
+ def mtime
50
+ ::File.mtime(path)
51
+ end
52
+
53
+ def contents
54
+ raise "Cannot read contents of a directory" if directory?
55
+
56
+ @contents || ::File.read(path)
57
+ end
58
+
59
+ def parent_directory_of?(child_path)
60
+ return false unless directory?
61
+
62
+ self.class.new(child_path).dirname == path
63
+ end
64
+
65
+ def anscestor_directory_of?(descendant_path)
66
+ return false unless directory?
67
+
68
+ self.class.new(descendant_path).path.to_s.start_with?("#{path}/")
69
+ end
70
+
71
+ def to_h
72
+ {path:, basename:, name:, ext:, directory: directory?}.compact
73
+ end
74
+
75
+ def to_s
76
+ path.to_s
77
+ end
78
+
79
+ alias_method :value, :path
80
+ alias_method :to_pathname, :path
81
+
82
+ delegate_missing_to :path
83
+
84
+ class << self
85
+ def file?(file)
86
+ file.is_a?(File)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booklet
4
+ module Helpers
5
+ def strip_indent(value)
6
+ value.to_s.gsub(/^#{value.scan(/^[ \t]*(?=\S)/).min}/, "").strip
7
+ end
8
+
9
+ def strip_whitespace(value)
10
+ value.to_s.strip
11
+ end
12
+
13
+ def hexdigest(str)
14
+ Digest::MD5.hexdigest(str.to_s)[0..6] if str.present?
15
+ end
16
+
17
+ extend self
18
+ end
19
+ end