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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +234 -0
- data/bin/booklet +6 -0
- data/lib/booklet/analyzer.rb +28 -0
- data/lib/booklet/cli/cli.rb +14 -0
- data/lib/booklet/cli/commands/analyze_command.rb +45 -0
- data/lib/booklet/cli/commands/version_command.rb +13 -0
- data/lib/booklet/cli/concerns/colorful.rb +51 -0
- data/lib/booklet/concerns/locatable.rb +22 -0
- data/lib/booklet/file.rb +90 -0
- data/lib/booklet/helpers.rb +19 -0
- data/lib/booklet/node.rb +297 -0
- data/lib/booklet/nodes/anon_node.rb +9 -0
- data/lib/booklet/nodes/asset_node.rb +15 -0
- data/lib/booklet/nodes/directory_node.rb +17 -0
- data/lib/booklet/nodes/document_node.rb +11 -0
- data/lib/booklet/nodes/file_node.rb +15 -0
- data/lib/booklet/nodes/folder_node.rb +13 -0
- data/lib/booklet/nodes/prose_node.rb +7 -0
- data/lib/booklet/nodes/scenario_node.rb +13 -0
- data/lib/booklet/nodes/spec_node.rb +19 -0
- data/lib/booklet/object.rb +23 -0
- data/lib/booklet/options.rb +31 -0
- data/lib/booklet/value.rb +16 -0
- data/lib/booklet/values/code_snippet.rb +25 -0
- data/lib/booklet/values/method_snippet.rb +20 -0
- data/lib/booklet/values/node_type.rb +55 -0
- data/lib/booklet/values/parser_result.rb +11 -0
- data/lib/booklet/values/source_location.rb +17 -0
- data/lib/booklet/values/text_snippet.rb +21 -0
- data/lib/booklet/values.rb +31 -0
- data/lib/booklet/version.rb +5 -0
- data/lib/booklet/visitor.rb +58 -0
- data/lib/booklet/visitors/ascii_tree_renderer.rb +36 -0
- data/lib/booklet/visitors/entity_transformer.rb +18 -0
- data/lib/booklet/visitors/filesystem_loader.rb +18 -0
- data/lib/booklet/visitors/frontmatter_extractor.rb +10 -0
- data/lib/booklet/visitors/preview_class_parser.rb +42 -0
- data/lib/booklet/yard_parser.rb +23 -0
- data/lib/booklet.rb +29 -0
- data/lib/lookbooklet.rb +3 -0
- 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 → 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 → 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,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,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
|
data/lib/booklet/file.rb
ADDED
|
@@ -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
|