brief 1.2.0 → 1.3.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +143 -70
  5. data/lib/brief/adapters/middleman.rb +2 -3
  6. data/lib/brief/briefcase.rb +28 -5
  7. data/lib/brief/cli/change.rb +7 -7
  8. data/lib/brief/cli/init.rb +10 -11
  9. data/lib/brief/cli/write.rb +23 -0
  10. data/lib/brief/configuration.rb +3 -4
  11. data/lib/brief/document/content_extractor.rb +2 -4
  12. data/lib/brief/document/front_matter.rb +4 -4
  13. data/lib/brief/document/rendering.rb +7 -9
  14. data/lib/brief/document/section/builder.rb +10 -10
  15. data/lib/brief/document/section/mapping.rb +5 -11
  16. data/lib/brief/document/section.rb +2 -3
  17. data/lib/brief/document/structure.rb +14 -15
  18. data/lib/brief/document/templating.rb +14 -0
  19. data/lib/brief/document.rb +20 -9
  20. data/lib/brief/document_mapper.rb +10 -10
  21. data/lib/brief/dsl.rb +5 -6
  22. data/lib/brief/model/definition.rb +37 -13
  23. data/lib/brief/model/persistence.rb +0 -2
  24. data/lib/brief/model.rb +50 -21
  25. data/lib/brief/repository.rb +2 -3
  26. data/lib/brief/util.rb +4 -4
  27. data/lib/brief/version.rb +1 -1
  28. data/lib/brief.rb +47 -33
  29. data/spec/fixtures/example/brief.rb +3 -0
  30. data/spec/fixtures/example/models/epic.rb +43 -5
  31. data/spec/fixtures/example/templates/user_story.md.erb +22 -0
  32. data/spec/lib/brief/briefcase_spec.rb +1 -1
  33. data/spec/lib/brief/dsl_spec.rb +1 -1
  34. data/spec/lib/brief/model_spec.rb +6 -1
  35. data/spec/lib/brief/persistence_spec.rb +1 -1
  36. data/spec/lib/brief/repository_spec.rb +1 -1
  37. data/spec/lib/brief/template_spec.rb +44 -0
  38. data/spec/spec_helper.rb +2 -2
  39. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f22a8f02957425fed25d8adfa60cd4b9aa2ced77
4
- data.tar.gz: 5c6e4bbb9a213eeaeac820cef681f17fc308d08a
3
+ metadata.gz: 4b51f1b01a40cf21a7cbe99d0c90bf6275ccacb3
4
+ data.tar.gz: df60ebe9cb374863ecd6b3c7df5f066c08e32910
5
5
  SHA512:
6
- metadata.gz: 86ceea62469f2a210e595a03b056cda6297b0adbce06c90659f664b284e9d61a3b1f9be7d125ee759632bd08c8e585f2c8e3fdca74559f92675c9d56b486b067
7
- data.tar.gz: 0f5fd7ccea4cd89b9243b6409297bc99525c37c2a6c5baca0f0b7aceb65f6ef067d042055f8e7915d28306c2c796f3307a35a4797c72593efedc63244af51c2c
6
+ metadata.gz: cef0b7d00b3c7fd405f6bcc3fce826078eb9889d18993e826844e5bfa7ed9caf26933f35fb243703eabc1958a16d398dca723682d40055d9b29e971a3e3a2486
7
+ data.tar.gz: 6aeb9e3cb3f3dd1bdc7ca24d4a956bd18d24ffe0bd5012bd6ad275df99d4f977dc34867b89ff420cc1d116e427859f8a01c1cc5f69c9ea2dbce6dfe8439b4c31
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ### 1.0.0
2
+
3
+ Rewrite.
4
+
5
+ - New style for structure definition
6
+
7
+ Instead of parsing markdown prior to rendering it in order to
8
+ determine the structure, I've taken a new approach where I render the markdown into HTML and then use Nokogiri.
9
+
10
+ This means I don't even really have to parse the markdown, I can just
11
+ let the user tell us how the documents are structured using CSS
12
+ selectors.
13
+
14
+ In addition to this, I've added a DSL that lets a user declare the model
15
+ attributes.
16
+
17
+ - Documents and Models
18
+
19
+ Documents are just YAML frontmatter and markdown.
20
+
21
+ Each document can be represented in model form.
22
+
23
+ The `Brief::Model` is an ActiveModel like object.
24
+
1
25
  ### 1.1.0
2
26
 
3
27
  - Introducing Briefcases
@@ -36,3 +60,49 @@
36
60
  - using a custom redcarpet markdown renderer in order to include
37
61
  data-attributes on heading tags
38
62
  - rendered html retains line number reference to the source markdown
63
+
64
+ ### 1.3.0
65
+
66
+ - Introducing Templates
67
+
68
+ - Templates are ERB strings that can be associated with models. This
69
+ will convert a data structure into the appropriate markdown, so that
70
+ it can be persisted as a `Brief::Document` and do everything a
71
+ normal doc can.
72
+
73
+ - Introducing Examples
74
+
75
+ - Each model class can point to an example, or include example content
76
+ inline as a string.
77
+
78
+ - Introducing the 'write' command.
79
+
80
+ The brief CLI will have a write command for each model, which
81
+ will open up $EDITOR and load the example content for a model
82
+
83
+ so for example, given a model:
84
+
85
+ ```
86
+ define "User Story" do
87
+
88
+ example <<- EOF
89
+ ---
90
+ type: user_story
91
+ status: draft
92
+ ---
93
+
94
+ # User story example
95
+
96
+ As a **user** I would like to **do this thing** so that I can
97
+ **achieve this goal**
98
+ EOF
99
+ end
100
+ ```
101
+
102
+ When I run this on the command line:
103
+
104
+ ```bash
105
+ brief write user story
106
+ ```
107
+
108
+ then it will open up $EDITOR
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brief (1.2.0)
4
+ brief (1.3.0)
5
5
  activemodel
6
6
  activesupport
7
7
  commander
@@ -55,7 +55,7 @@ GEM
55
55
  mini_portile (0.6.2)
56
56
  minitest (5.5.1)
57
57
  multipart-post (2.0.0)
58
- nokogiri (1.6.5)
58
+ nokogiri (1.6.6.2)
59
59
  mini_portile (~> 0.6.0)
60
60
  octokit (3.7.0)
61
61
  sawyer (~> 0.6.0, >= 0.5.3)
data/README.md CHANGED
@@ -1,120 +1,193 @@
1
1
  # Brief
2
2
 
3
- An ActiveRecord style layer on top of a folder of markdown files.
3
+ Brief is a tool for building applications on top of collections of
4
+ documents written in markdown.
4
5
 
5
- Treat your markdown documents as active models, run actions on them,
6
- convert them into HTML, extract fragments of HTML, combine it all in
7
- whatever interesting way you can think of.
6
+ Brief lets you define models very similar to how you would in
7
+ ActiveRecord and Rails, but instead of rows in a database you are
8
+ going to be working with files in a folder.
8
9
 
9
- The end result is a really neat way of being able to use the words that you write
10
- to power all sorts of applications.
10
+ ### Getting Started
11
+ ```
12
+ gem install brief
13
+ brief --help
14
+ ```
15
+
16
+ ### Hypothetical Example
17
+
18
+ ```
19
+ brief init my-cookbook
20
+ cd my-cookbook
21
+ brief generate model Recipe # View the DOCUMENTATION for what you can do with a model
22
+ brief write recipe # => opens up your $EDITOR with an example recipe
23
+ ```
24
+
25
+ Brief treats each markdown file as an active record style model object. It treats a folder of markdown files like a database.
26
+
27
+ ### How does it work?
28
+
29
+ Brief takes a markdown file which looks like:
30
+
31
+ ```markdown
32
+ ---
33
+ type: post
34
+ status: active
35
+ ---
36
+
37
+ # An introduction to brief
38
+ ## Stop writing dead documents
39
+
40
+ Brief is a tool which lets you define different patterns of markdown
41
+ headings (h1,h2,h3 etc). This lets you work with collections of your
42
+ writings, and treat each document as a database.
43
+ ```
44
+
45
+ And turns it into data, not only using the metadata up top, but also the information
46
+ contained in the structure of the document itself. The headings, subheadings, pretty much
47
+ anything you could query from the HTML using CSS can be turned into key value pairs that you can work
48
+ with and build applications on top of.
49
+
50
+ With your enhanced writing, you can do things like:
51
+
52
+ ```ruby
53
+ # Find all the posts which are active:
54
+ posts = briefcase.posts.where(status:"published")
55
+
56
+ # Get their titles, and subheadings:
57
+ posts.map(&:title) #=>['An introduction to brief']
58
+ posts.map(&:subheading) #=>['Stop writing dead documents']
11
59
 
12
- **No more writing dead documents!**
60
+ # Publish the post
61
+ posts.first.publish()
13
62
 
14
- ### Documents as Models
63
+ # Email the drafts to your editor
64
+ posts.where(status:"draft").each &:mail_to_editors
65
+ ```
15
66
 
16
- Brief lets you treat an individual markdown file as if it were a model,
17
- complete with validations, callbacks, and methods you can run. You can
18
- define a `Post` model for all of the files in a 'posts' folder and
19
- define actions like 'publish' on them.
67
+ From the Command line we can:
68
+
69
+ ```bash
70
+ brief publish posts
71
+ brief write post #=> Opens your editor
72
+ ```
73
+
74
+ This type of interactivity is made possible by the `Brief::Model`
75
+
76
+ ### Documents as models
20
77
 
21
78
  ```ruby
22
79
  define "Post" do
23
80
  meta do
24
81
  status
25
- tags Array
26
82
  end
27
83
 
28
84
  content do
29
- has_one :title, "h1"
30
- has_many :subheadings, "h2"
85
+ title "h1:first-of-type"
86
+ subheading "h2:first-of-type"
87
+ sample "p:first-of-type"
31
88
  end
32
89
 
33
90
  actions do
34
91
  def publish
35
- update_attributes(:status => "published")
92
+ update_attributes(status: "published")
36
93
  end
37
94
  end
38
95
  end
39
96
  ```
40
97
 
98
+ ### CLI Tool
41
99
 
42
- ### Model attributes derived from YAML frontmatter
100
+ Brief gives you a CLI called `brief`
43
101
 
44
- Models can get their attributes from headers on the document, aka YAML frontmatter.
45
-
46
- ```markdown
47
- ---
48
- status: draft
49
- tags:
50
- - demo
51
- - sample
52
- ---
102
+ This lets you run some general commands, but also gives you an
103
+ intelligent interface to work with your models. In the above example,
104
+ we defined some actions.
53
105
 
54
- # Title
106
+ This will be available in the CLI:
55
107
 
56
- ## Section One
57
- ## Section Two
58
108
  ```
59
-
60
- which will let you use it like such:
61
-
62
- ```ruby
63
- post = Brief::Document.new(/path/to/doc.html.md)
64
-
65
- post.status #=> "draft"
66
- post.title #=> "Title"
67
- post.tags #=> ['demo','sample']
109
+ brief publish posts ./docs/posts/*.md
68
110
  ```
69
111
 
70
- #### Model attributes derived from the document structure
71
-
72
- Models can also get their attributes from the structure itself.
112
+ This will find all of the matching documents, turn them into `Post`
113
+ models, and then run the publish method on them. You can make your
114
+ models as advanced as you want:
73
115
 
74
- ```ruby
75
- post.title #=> "Title"
76
- post.subheadings #=> ["Section One", "Section Two"]
116
+ ```
117
+ define "Post" do
118
+ actions do
119
+ def submit
120
+ update_attributes(status:"submitted to editors")
121
+ mailer.send(:to => "jon@chicago.com", :subject => "Please review: #{ self.title }")
122
+ end
123
+ end
124
+ end
77
125
  ```
78
126
 
79
- ### Querying Documents
80
-
81
- Given a big folder of markdown files with attributes, we can query them:
127
+ ### YAML Frontmatter
82
128
 
83
- ```
84
- posts = briefcase.posts.where(:status => "published")
85
- posts.map(&:title) #=> ['Title']
86
- ```
129
+ Each markdown document can contain YAML frontmatter. This data will be
130
+ available and associated with each document or model, and will also let
131
+ you query, filter, and sort your documents.
87
132
 
88
- This functionality is based on https://github.com/ralph/document_mapper,
89
- and similar to middleman.
133
+ ### CSS Structure Definition
90
134
 
91
- ### Document Actions
135
+ Since markdown renders into HTML, and HTML Documents can be queried
136
+ using CSS selectors, we use CSS selectors in our model definition DSL so
137
+ that we can isolate certain parts of the document, and use the content
138
+ it contains as metadata.
92
139
 
93
- By defining actions on documents like so:
140
+ A real world example:
94
141
 
95
142
  ```ruby
96
-
97
- define "Post" do
98
- actions do
99
- def publish
100
- # DO Something
143
+ content do
144
+ title "h1:first-of-type"
145
+ define_section "User Stories" do
146
+ each("h2").has(:title => "h2",
147
+ :paragraph => "p:first-of-type",
148
+ :components => "p:first-of-type strong"
149
+ )
150
+
151
+ each("h2").is_a :user_story
101
152
  end
102
153
  end
103
- end
104
154
  ```
105
155
 
106
- you can either call that method as you normally would:
156
+ This lets me turn a markdown file like:
107
157
 
108
- ```ruby
109
- post = Brief.case.posts.where(:status => "draft")
110
- post.publish()
158
+ ```markdown
159
+ ---
160
+ title: Epic Example
161
+ status: published
162
+ ---
163
+ # Epic Example
164
+ # User Stories
165
+
166
+ ## A user wants to do something
167
+ As a **User** I would like to **Do this** so that I can **succeed**
168
+
169
+ ## A user wants to do something else
170
+ As a **User** I would like to **Do this** so that I can **succeed**
111
171
  ```
112
172
 
113
- or you can run that action from the command line:
173
+ into:
114
174
 
115
- ```bash
116
- brief publish posts ./posts/*.html.md
175
+ ```ruby
176
+ {
177
+ title: "Epic Example",
178
+ status: "published",
179
+ type: "epic",
180
+ user_stories:[{
181
+ title: "A user wants to do something",
182
+ paragraph: "As a user I would like to do something so that I can succeed",
183
+ goal: "I can succeed",
184
+ persona: "user",
185
+ behavior: "do something"
186
+ },{
187
+ title: "A user wants to do something else",
188
+ paragraph: "As a user I would like to do something else so that I can succeed"
189
+ }]
190
+ }
117
191
  ```
118
192
 
119
- this will find all of the post models matching the document, and then
120
- call the publish method on them.
193
+ And we can even go in the other direction.
@@ -1,16 +1,15 @@
1
1
  module Brief::Adapters
2
2
  class MiddlemanExtension
3
-
4
3
  def self.activate_brief_extension
5
4
  ::Middleman::Extensions.register(:brief, Brief::Adapters::MiddlemanExtension)
6
5
  end
7
6
 
8
- def initialize(app, options_hash={}, &block)
7
+ def initialize(app, options_hash = {}, &block)
9
8
  super
10
9
 
11
10
  app.include(ClassMethods)
12
11
 
13
- options_hash.each do |key,value|
12
+ options_hash.each do |key, value|
14
13
  app.set(key, value)
15
14
  end
16
15
  end
@@ -5,7 +5,7 @@ module Brief
5
5
  attr_reader :options,
6
6
  :model_definitions
7
7
 
8
- def initialize(options={})
8
+ def initialize(options = {})
9
9
  @options = options.to_mash
10
10
 
11
11
  load_configuration
@@ -16,6 +16,16 @@ module Brief
16
16
  end
17
17
  end
18
18
 
19
+ def use(module_type=:app, module_id)
20
+ if module_type == :app && apps_path.join(module_id).exist?
21
+ config = module_type.join("config.rb")
22
+ models = module_type.join("models")
23
+
24
+ instance_eval(config.read) if config.exist?
25
+ Brief.load_modules_from(models) if models.exist?
26
+ end
27
+ end
28
+
19
29
  def config
20
30
  Brief::Configuration.instance
21
31
  end
@@ -24,19 +34,32 @@ module Brief
24
34
  # or the configured path for the configuration file.
25
35
  def load_configuration
26
36
  config_path = options.fetch(:config_path) do
27
- root.join("brief.rb")
37
+ root.join('brief.rb')
38
+ end
39
+
40
+ if uses_app?
41
+ instance_eval(app_path.join("config.rb").read)
28
42
  end
29
43
 
30
44
  if config_path.exist?
31
- instance_eval(config_path.read)
45
+ instance_eval(config_path.read) rescue nil
32
46
  end
33
47
  end
34
48
 
49
+ def uses_app?
50
+ options.key?(:app) && Brief.apps_path.join(options[:app]).exist?
51
+ end
52
+
53
+ def app_path
54
+ uses_app? && Brief.apps_path.join(options[:app])
55
+ end
56
+
35
57
  def load_model_definitions
36
- if models_path.exist?
37
- Dir[models_path.join("**/*.rb")].each {|f| require(f) }
58
+ if uses_app?
59
+ Brief.load_modules_from(app_path.join("models"))
38
60
  end
39
61
 
62
+ Brief.load_modules_from(models_path) if models_path.exist?
40
63
  Brief::Model.finalize
41
64
  end
42
65
 
@@ -1,11 +1,11 @@
1
- command "change" do |c|
2
- c.syntax = "brief change ATTRIBUTE [OPTIONS]"
3
- c.description = "change attributes of brief documents"
1
+ command 'change' do |c|
2
+ c.syntax = 'brief change ATTRIBUTE [OPTIONS]'
3
+ c.description = 'change attributes of brief documents'
4
4
 
5
- c.option "--from", String, "Only apply when the attributes current value matches."
6
- c.option "--to", String, "Only apply when the attributes current value matches."
7
- c.option "--files", String, "The files you want to change"
8
- c.option "--on", String, "alias for --files. The files you want to change"
5
+ c.option '--from', String, 'Only apply when the attributes current value matches.'
6
+ c.option '--to', String, 'Only apply when the attributes current value matches.'
7
+ c.option '--files', String, 'The files you want to change'
8
+ c.option '--on', String, 'alias for --files. The files you want to change'
9
9
 
10
10
  c.action do |args, options|
11
11
  puts "Args: #{ args.inspect }"
@@ -1,27 +1,26 @@
1
- command "init" do |c|
2
- c.syntax = "brief init NAME [OPTIONS]"
3
- c.description = "Create a new brief project, aka a briefcase"
1
+ command 'init' do |c|
2
+ c.syntax = 'brief init NAME [OPTIONS]'
3
+ c.description = 'Create a new brief project, aka a briefcase'
4
4
 
5
- c.option "--root", String, "The root folder for the new project."
5
+ c.option '--root', String, 'The root folder for the new project.'
6
6
 
7
7
  c.action do |args, options|
8
- options.default(:root => Dir.pwd())
8
+ options.default(root: Dir.pwd)
9
9
 
10
10
  if path = args.first
11
11
  root = Pathname(options.root).join(path)
12
12
  end
13
13
 
14
- [root, root.join("models"), root.join("docs")].each do |p|
14
+ [root, root.join('models'), root.join('docs')].each do |p|
15
15
  puts "== folder #{ p.basename } #{ '. exists' if p.exist? }"
16
16
  FileUtils.mkdir_p(p) unless p.exist?
17
17
  end
18
18
 
19
-
20
- if root.join("brief.rb").exist?
21
- say "== Briefcase config already exists. Skipping."
19
+ if root.join('brief.rb').exist?
20
+ say '== Briefcase config already exists. Skipping.'
22
21
  else
23
- say "== Creating config file brief.rb"
24
- root.join("brief.rb").open("w+") do |fh|
22
+ say '== Creating config file brief.rb'
23
+ root.join('brief.rb').open('w+') do |fh|
25
24
  default_config = <<-EOF
26
25
 
27
26
  config do
@@ -0,0 +1,23 @@
1
+ command 'write' do |c|
2
+ c.syntax = 'brief write MODEL_TYPE [OPTIONS]'
3
+ c.description = 'Create a new document for a given model type'
4
+
5
+ c.action do |args, _options|
6
+ string_args = args.select { |a| a.is_a?(String) }
7
+ model_class = Brief::Model.lookup_class_from_args(string_args)
8
+
9
+ base_content = ''
10
+
11
+ if model_class && model_class.example_body.to_s.length > 0
12
+ base_content = model_class.example_body
13
+ else
14
+ # document_contents = model_class.inspect + model_class.example_body.to_s
15
+ end
16
+
17
+ document_contents = ask_editor(base_content)
18
+ file = ask('enter a name for this file:', String)
19
+
20
+ puts file
21
+ puts document_contents
22
+ end
23
+ end
@@ -14,12 +14,12 @@ module Brief
14
14
 
15
15
  def current
16
16
  @current ||= {
17
- docs_path: "docs",
18
- models_path: "models"
17
+ docs_path: 'docs',
18
+ models_path: 'models'
19
19
  }.to_mash
20
20
  end
21
21
 
22
- def set(attribute, value=nil)
22
+ def set(attribute, value = nil)
23
23
  current[attribute] = value
24
24
  self
25
25
  end
@@ -33,6 +33,5 @@ module Brief
33
33
  nil
34
34
  end
35
35
  end
36
-
37
36
  end
38
37
  end
@@ -5,9 +5,7 @@ module Brief
5
5
  @document = document
6
6
  end
7
7
 
8
- def document
9
- @document
10
- end
8
+ attr_reader :document
11
9
 
12
10
  def model_class
13
11
  Brief::Model.for_type(@model_type)
@@ -21,7 +19,7 @@ module Brief
21
19
  attribute_set.key?(meth) || super
22
20
  end
23
21
 
24
- def method_missing(meth, *args, &block)
22
+ def method_missing(meth, *_args, &_block)
25
23
  if settings = attribute_set.fetch(meth, nil)
26
24
  if settings.args.length == 1 && settings.args.first.is_a?(String)
27
25
  selector = settings.args.first
@@ -15,10 +15,10 @@ module Brief
15
15
 
16
16
  def load_frontmatter
17
17
  if raw_content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
18
- self.content = raw_content[($1.size + $2.size)..-1]
19
- @frontmatter_line_count = $1.lines.size
20
- @raw_frontmatter = $1
21
- @frontmatter = YAML.load($1).to_mash
18
+ self.content = raw_content[(Regexp.last_match[1].size + Regexp.last_match[2].size)..-1]
19
+ @frontmatter_line_count = Regexp.last_match[1].lines.size
20
+ @raw_frontmatter = Regexp.last_match[1]
21
+ @frontmatter = YAML.load(Regexp.last_match[1]).to_mash
22
22
  end
23
23
  end
24
24
  end
@@ -19,11 +19,11 @@ module Brief
19
19
 
20
20
  def renderer
21
21
  @renderer ||= begin
22
- r = renderer_class.new(:tables => true,
23
- :autolink => true,
24
- :gh_blockcode => true,
25
- :fenced_code_blocks => true,
26
- :footnotes => true)
22
+ r = renderer_class.new(tables: true,
23
+ autolink: true,
24
+ gh_blockcode: true,
25
+ fenced_code_blocks: true,
26
+ footnotes: true)
27
27
 
28
28
  ::Redcarpet::Markdown.new(r)
29
29
  end
@@ -36,7 +36,7 @@ module Brief
36
36
  # which allows us to wrap things in section containers, apply data
37
37
  # attributes, and other things to the HTML so that the output HTML retains its
38
38
  # relationship to the underlying data and document structure.
39
- def to_html(options={})
39
+ def to_html(options = {})
40
40
  if options[:wrap] == false
41
41
  unwrapped_html
42
42
  else
@@ -59,9 +59,7 @@ module Brief
59
59
  @renderer ||= self.class.renderer
60
60
  end
61
61
 
62
- def renderer=(value)
63
- @renderer = value
64
- end
62
+ attr_writer :renderer
65
63
  end
66
64
  end
67
65
  end