brief 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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