brief 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ea25b279da5166d726a9463be10836ab9b6d7bb
4
- data.tar.gz: e52d0270a01c924196c8ab59250df0973320589d
3
+ metadata.gz: f22a8f02957425fed25d8adfa60cd4b9aa2ced77
4
+ data.tar.gz: 5c6e4bbb9a213eeaeac820cef681f17fc308d08a
5
5
  SHA512:
6
- metadata.gz: 250b23f611f467ec9731114557f84d8bf24e498642849eab59dcb79f7a7c1790ea33b8f734a3017f515ad025caf5353625b0ec972c7e016eb7682eafe9aa063a
7
- data.tar.gz: 88a4250edb54523fdb51f18700a149831a5835d031cf67fbc49c142bb6d3abeccec9eff48c32df99f9ded979330c9223b7f7934819b87e4f79f33a67292b2e3e
6
+ metadata.gz: 86ceea62469f2a210e595a03b056cda6297b0adbce06c90659f664b284e9d61a3b1f9be7d125ee759632bd08c8e585f2c8e3fdca74559f92675c9d56b486b067
7
+ data.tar.gz: 0f5fd7ccea4cd89b9243b6409297bc99525c37c2a6c5baca0f0b7aceb65f6ef067d042055f8e7915d28306c2c796f3307a35a4797c72593efedc63244af51c2c
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ ### 1.1.0
2
+
3
+ - Introducing Briefcases
4
+
5
+ Briefcases are top level folders which contain model definition code
6
+ and configuration, and a hierarchy of subfolders each containing
7
+ markdown documents which will map to models.
8
+
9
+ - Model Definition DSL
10
+
11
+ Briefcases can have more than one model class. These models can have
12
+ attributes, and can specify structure rules which allow for the
13
+ document to be parsed in a semantic way.
14
+
15
+ ### 1.2.0
16
+
17
+ - Introducing Document Sections
18
+ - Added `define_section` to the model definition DSL. This will allow certain headings to be used to access a
19
+ section of the document in isolation.
20
+
21
+ Document sections have their own mini-structure, and will
22
+ contain at least one, usually more than one repeatable pattern.
23
+
24
+ These repeatable patterns will usually contain short-hand
25
+ references to other documents, or key attributes that can be used to
26
+ create other documents.
27
+
28
+ Document sections are how one document can be broken apart into many
29
+ other documents.
30
+
31
+ - CLI Actions
32
+ - Added `actions` to the method definition DSL. This will allow a model instance to define a method, and
33
+ then dispatch calls to this method from the Brief CLI interface.
34
+
35
+ - Changed HTML rendering
36
+ - using a custom redcarpet markdown renderer in order to include
37
+ data-attributes on heading tags
38
+ - rendered html retains line number reference to the source markdown
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brief (1.0.0)
4
+ brief (1.2.0)
5
5
  activemodel
6
6
  activesupport
7
7
  commander
@@ -39,7 +39,7 @@ GEM
39
39
  thread_safe (~> 0.3, >= 0.3.1)
40
40
  diff-lcs (1.2.5)
41
41
  equalizer (0.0.9)
42
- faraday (0.9.0)
42
+ faraday (0.9.1)
43
43
  multipart-post (>= 1.2, < 3)
44
44
  github-fs (0.0.1)
45
45
  activesupport (> 3.2.0)
@@ -50,10 +50,10 @@ GEM
50
50
  i18n (0.7.0)
51
51
  ice_nine (0.11.1)
52
52
  inflecto (0.0.2)
53
- json (1.8.1)
53
+ json (1.8.2)
54
54
  method_source (0.8.2)
55
55
  mini_portile (0.6.2)
56
- minitest (5.5.0)
56
+ minitest (5.5.1)
57
57
  multipart-post (2.0.0)
58
58
  nokogiri (1.6.5)
59
59
  mini_portile (~> 0.6.0)
@@ -67,7 +67,7 @@ GEM
67
67
  pry (>= 0.9.10, < 0.11.0)
68
68
  rack (1.6.0)
69
69
  rake (10.4.2)
70
- redcarpet (3.2.0)
70
+ redcarpet (3.2.2)
71
71
  rspec (3.1.0)
72
72
  rspec-core (~> 3.1.0)
73
73
  rspec-expectations (~> 3.1.0)
@@ -87,7 +87,7 @@ GEM
87
87
  thread_safe (0.3.4)
88
88
  tzinfo (1.2.2)
89
89
  thread_safe (~> 0.1)
90
- virtus (1.0.3)
90
+ virtus (1.0.4)
91
91
  axiom-types (~> 0.1)
92
92
  coercible (~> 1.0)
93
93
  descendants_tracker (~> 0.0, >= 0.0.3)
data/README.md CHANGED
@@ -103,9 +103,18 @@ define "Post" do
103
103
  end
104
104
  ```
105
105
 
106
- you can either call that method as you normally would, or you can run
107
- that action from the command line:
106
+ you can either call that method as you normally would:
107
+
108
+ ```ruby
109
+ post = Brief.case.posts.where(:status => "draft")
110
+ post.publish()
111
+ ```
112
+
113
+ or you can run that action from the command line:
108
114
 
109
115
  ```bash
110
116
  brief publish posts ./posts/*.html.md
111
117
  ```
118
+
119
+ this will find all of the post models matching the document, and then
120
+ call the publish method on them.
@@ -0,0 +1,18 @@
1
+ module Brief::Adapters
2
+ class MiddlemanExtension
3
+
4
+ def self.activate_brief_extension
5
+ ::Middleman::Extensions.register(:brief, Brief::Adapters::MiddlemanExtension)
6
+ end
7
+
8
+ def initialize(app, options_hash={}, &block)
9
+ super
10
+
11
+ app.include(ClassMethods)
12
+
13
+ options_hash.each do |key,value|
14
+ app.set(key, value)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -60,7 +60,15 @@ module Brief
60
60
  end
61
61
 
62
62
  def models_path
63
- root.join options.fetch(:models_path) { config.models_path }
63
+ value = options.fetch(:models_path) { config.models_path }
64
+
65
+ if value.to_s.match(/\./)
66
+ Pathname(Dir.pwd).join(value)
67
+ elsif value.to_s.match(/\//)
68
+ Pathname(value)
69
+ else
70
+ root.join(value)
71
+ end
64
72
  end
65
73
 
66
74
  def repository
@@ -25,7 +25,13 @@ module Brief
25
25
  if settings = attribute_set.fetch(meth, nil)
26
26
  if settings.args.length == 1 && settings.args.first.is_a?(String)
27
27
  selector = settings.args.first
28
- document.css(selector).try(:text)
28
+ matches = document.css(selector)
29
+
30
+ if matches.length > 1
31
+ selector.match(/first-of-type/) ? matches.first.text : matches.map(&:text)
32
+ else
33
+ matches.first.try(:text)
34
+ end
29
35
  end
30
36
  end
31
37
  end
@@ -7,11 +7,17 @@ module Brief
7
7
  @frontmatter || load_frontmatter
8
8
  end
9
9
 
10
+ def frontmatter_line_count
11
+ (@raw_frontmatter && @raw_frontmatter.lines.count) || 0
12
+ end
13
+
10
14
  protected
11
15
 
12
16
  def load_frontmatter
13
- if content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
14
- self.content = content[($1.size + $2.size)..-1]
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
15
21
  @frontmatter = YAML.load($1).to_mash
16
22
  end
17
23
  end
@@ -3,26 +3,23 @@ module Brief
3
3
  module Rendering
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def to_html
7
- self.class.renderer.render(content)
8
- end
9
-
10
- def css(*args, &block)
11
- parser.send(:css, *args, &block)
12
- end
13
-
14
- def at(*args, &block)
15
- parser.send(:at, *args, &block)
16
- end
17
-
18
- def parser
19
- @parser ||= Nokogiri::HTML.fragment(to_html)
6
+ # Uses a custom Redcarpet::Render::HTML subclass
7
+ # which simply inserts data attributes on each heading element
8
+ # so that they can be queried with CSS more deliberately.
9
+ class HeadingWrapper < ::Redcarpet::Render::HTML
10
+ def header(text, level)
11
+ "<h#{level} data-level='#{level}' data-heading='#{ text }'>#{text}</h#{level}>"
12
+ end
20
13
  end
21
14
 
22
15
  module ClassMethods
16
+ def renderer_class
17
+ HeadingWrapper
18
+ end
19
+
23
20
  def renderer
24
21
  @renderer ||= begin
25
- r = ::Redcarpet::Render::HTML.new(:tables => true,
22
+ r = renderer_class.new(:tables => true,
26
23
  :autolink => true,
27
24
  :gh_blockcode => true,
28
25
  :fenced_code_blocks => true,
@@ -32,6 +29,39 @@ module Brief
32
29
  end
33
30
  end
34
31
  end
32
+
33
+ # Documents can be rendered into HTML.
34
+ #
35
+ # They will first be put through a Nokogiri processor pipeline
36
+ # which allows us to wrap things in section containers, apply data
37
+ # attributes, and other things to the HTML so that the output HTML retains its
38
+ # relationship to the underlying data and document structure.
39
+ def to_html(options={})
40
+ if options[:wrap] == false
41
+ unwrapped_html
42
+ else
43
+ wrapper = options.fetch(:wrapper, 'div')
44
+ "<#{ wrapper } data-brief-model='#{ model_class.type_alias }' data-brief-path='#{ relative_path_identifier }'>#{ unwrapped_html }</#{wrapper}>"
45
+ end
46
+ end
47
+
48
+ def unwrapped_html
49
+ parser.to_html
50
+ end
51
+
52
+ protected
53
+
54
+ def to_raw_html
55
+ renderer.render(content)
56
+ end
57
+
58
+ def renderer
59
+ @renderer ||= self.class.renderer
60
+ end
61
+
62
+ def renderer=(value)
63
+ @renderer = value
64
+ end
35
65
  end
36
66
  end
37
67
  end
@@ -0,0 +1,89 @@
1
+ class Brief::Document::Section
2
+ class Builder
3
+ def self.run(source, options={})
4
+ new(source, options).to_fragment
5
+ end
6
+
7
+ attr_accessor :source, :nodes, :low, :high
8
+
9
+ def initialize(source, options={})
10
+ @source = source.map do |item|
11
+ level, group = item
12
+ [level, group.map {|f| f.is_a?(String) ? Nokogiri::HTML.fragment(f) : f }]
13
+ end
14
+
15
+ @low = options.fetch(:low, 1)
16
+ @high = options.fetch(:high, 6)
17
+ @nodes = []
18
+ @cycles = 0
19
+
20
+ run
21
+ end
22
+
23
+ def run
24
+ source.length.times do
25
+ source.each_with_index do |item, index|
26
+ n = index + 1
27
+ level, fragments = item
28
+ next_level, next_fragments = source[n]
29
+
30
+ if next_level && (next_level == level) && (level > low)
31
+ new_fragment = (fragments + next_fragments).map(&:to_html).join("")
32
+ source[index] = [level, [Nokogiri::HTML.fragment(new_fragment)]]
33
+ source[n] = nil
34
+ end
35
+ end
36
+
37
+ source.compact!
38
+ end
39
+
40
+ until even? || maxed_out?
41
+ source.map! do |item|
42
+ level, fragments = item
43
+ [level, fragments.first]
44
+ end
45
+
46
+ source.each_with_index do |item, index|
47
+ level, fragment = item
48
+ n = index + 1
49
+ next_level, next_fragment = source[n]
50
+
51
+ if fragment && next_level && (next_level > level)
52
+ parent = fragment.css("section, article").first
53
+ parent.add_child(next_fragment)
54
+ source[index] = [level, fragment]
55
+ source[n] = nil
56
+ end
57
+ end
58
+
59
+ source.compact!
60
+
61
+ @cycles += 1
62
+ end
63
+
64
+ self.nodes = source.map(&:last)
65
+
66
+ self.nodes.each do |node|
67
+ parent = node.css("section, article").first
68
+ if %w(h1 h2 h3 h4 h5 h6).include?(parent.children.first.name)
69
+ parent['data-heading'] = parent.children.first.text
70
+ end
71
+ end
72
+
73
+ self.nodes.map!(&:to_html)
74
+ end
75
+
76
+ def maxed_out?
77
+ @cycles > source.length
78
+ end
79
+
80
+ def even?
81
+ source.map(&:first).uniq.length == 1
82
+ end
83
+
84
+ def to_fragment
85
+ @html = nodes.join("") unless nodes.empty?
86
+ Nokogiri::HTML.fragment(@html || "<div/>")
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,61 @@
1
+ class Brief::Document::Section
2
+ class Mapping
3
+ def initialize(title, options={})
4
+ @title = title
5
+ @options = options
6
+ @config = {}.to_mash
7
+ end
8
+
9
+ def selectors
10
+ selector_config.keys
11
+ end
12
+
13
+ def selector_config
14
+ config.selectors
15
+ end
16
+
17
+ def config
18
+ @config
19
+ end
20
+
21
+ def options
22
+ @options
23
+ end
24
+
25
+ def title
26
+ @title
27
+ end
28
+
29
+ def selector
30
+ @selector || :next
31
+ end
32
+
33
+ def each(*args, &block)
34
+ @selector = args.first
35
+ self
36
+ end
37
+
38
+ def heading(*args, &block)
39
+ send(:each, *args, &block)
40
+ end
41
+
42
+ def has(*args)
43
+ options = args.extract_options!
44
+
45
+ unless options.empty?
46
+ config.selectors ||= {}
47
+ config.selectors.merge!(selector => options)
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ def is_a(*args)
54
+ options = args.extract_options!
55
+ klass = args.first
56
+ options[:is_a] = klass if klass
57
+
58
+ self
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ class Brief::Document::Section
2
+ attr_accessor :title, :fragment
3
+ attr_reader :config
4
+
5
+ Headings = %w(h1 h2 h3 h4 h5 h6)
6
+
7
+ def initialize(title, fragment, config)
8
+ @title = title
9
+ @fragment = fragment
10
+ @config = config
11
+ end
12
+
13
+ def items
14
+ return @items if @items
15
+
16
+ data = []
17
+
18
+ config.selectors.each do |selector|
19
+ settings = config.selector_config[selector]
20
+
21
+ if Headings.include?(selector)
22
+ headings = fragment.css("article > h2")
23
+ articles = headings.map(&:parent)
24
+
25
+ if !settings.empty?
26
+ articles.compact.each do |article|
27
+ data.push(settings.inject({}.to_mash) do |memo, pair|
28
+ attribute, selector = pair
29
+ result = article.css(selector)
30
+ memo[attribute] = result.length > 1 ? result.map(&:text) : result.text
31
+ memo
32
+ end)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ @items = data
39
+ end
40
+ end
41
+
@@ -0,0 +1,146 @@
1
+ module Brief
2
+ class Document::Structure
3
+ attr_accessor :fragment, :content_lines
4
+
5
+ def initialize(fragment,content_lines=[])
6
+ @fragment = fragment
7
+ @content_lines = content_lines
8
+ end
9
+
10
+ def prescan
11
+ content_lines.each_with_index do |line, index|
12
+ if line.match(/^#/)
13
+ line = line.strip
14
+ level = line.count('#')
15
+ text = line.gsub('#','').strip
16
+
17
+ if level > 0 && text.length > 0
18
+ line_number = index + 1
19
+ heading = find_heading_by(level, text)
20
+
21
+ if heading
22
+ heading.element.set_attribute('data-line-number', line_number)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def create_wrappers
30
+ return if @elements_have_been_wrapped
31
+
32
+ elements = fragment.children
33
+
34
+ mapping = []
35
+ bucket = []
36
+
37
+ current_level = Util.level(elements.first)
38
+
39
+ elements.each_cons(2) do |element, next_element|
40
+ bucket << element
41
+
42
+ if Util.is_header?(next_element) && Util.level(next_element) >= current_level
43
+ mapping.push([current_level, bucket])
44
+ bucket = []
45
+ end
46
+
47
+ if Util.is_header?(element)
48
+ current_level = Util.level(element)
49
+ end
50
+ end
51
+
52
+ mapping.push([current_level, bucket]) unless mapping.include?(bucket)
53
+
54
+ base_fragment = Nokogiri::HTML.fragment("<div class='brief top level' />")
55
+
56
+ mapping.map! do |item|
57
+ level, group = item
58
+ group.reject! {|i| i.text == "\n" }
59
+
60
+ if level == 0
61
+ base_fragment = fragment = Nokogiri::HTML.fragment("<div class='brief top level'>#{ group.map(&:to_html).join("") }</div>")
62
+ elsif level <= lowest_level
63
+ fragment = Nokogiri::HTML.fragment("<section>#{ group.map(&:to_html).join("") }</section>")
64
+ elsif level > lowest_level
65
+ # should be able to look at the document section mappings and
66
+ # apply custom css classes to these based on the name of the section
67
+ fragment = Nokogiri::HTML.fragment("<article>#{ group.map(&:to_html).join("") }</article>")
68
+ end
69
+
70
+ [level, [fragment]]
71
+ end
72
+
73
+ self.fragment = Brief::Document::Section::Builder.run(mapping, low: lowest_level, high: highest_level)
74
+ end
75
+
76
+ def levels
77
+ l = fragment.css("[data-level]").map {|el| el.attr('data-level').to_i }
78
+ l.reject!(&:nil?)
79
+ l.reject! {|v| v.to_i == 0 }
80
+ l.uniq!
81
+ l
82
+ end
83
+
84
+ def highest_level
85
+ levels.max
86
+ end
87
+
88
+ def lowest_level
89
+ levels.min
90
+ end
91
+
92
+ def headings_at_level(level, options={})
93
+ matches = heading_elements.select {|el| el.level.to_i == level.to_i }
94
+
95
+ if options[:text]
96
+ matches.map(&:text)
97
+ else
98
+ matches
99
+ end
100
+ end
101
+
102
+ def heading_with_text(text)
103
+ headings_with_text(text).tap do |results|
104
+ raise 'no section found with content: ' + text if results.length == 0
105
+ raise 'more than one section found with content: ' + text if results.length >= 2
106
+
107
+ end.first
108
+ end
109
+
110
+ def headings_with_text(text)
111
+ heading_elements.select do |el|
112
+ el.heading.to_s.strip == text.to_s.strip
113
+ end
114
+ end
115
+
116
+ def find_heading_by(level, heading)
117
+ heading_elements.find do |el|
118
+ el.level.to_s == level.to_s && heading.to_s.strip == el.heading.to_s.strip
119
+ end
120
+ end
121
+
122
+ def heading_elements
123
+ @heading_elements ||= fragment.css("h1,h2,h3,h4,h5,h6").map do |el|
124
+ if el.attr('data-level').to_i > 0
125
+ {
126
+ level: el.attr('data-level'),
127
+ heading: el.attr('data-heading'),
128
+ element: el
129
+ }.to_mash
130
+ end
131
+ end.compact
132
+ end
133
+
134
+ class Util
135
+ class << self
136
+ def is_header?(element)
137
+ element.name.to_s.downcase.match(/^h\d$/)
138
+ end
139
+
140
+ def level(element)
141
+ element.name.to_s.gsub(/^h/i,'').to_i
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -3,36 +3,72 @@ module Brief
3
3
  include Brief::Document::Rendering
4
4
  include Brief::Document::FrontMatter
5
5
 
6
- attr_accessor :path, :content, :frontmatter
6
+ attr_accessor :path, :content, :frontmatter, :raw_content
7
7
 
8
8
  def initialize(path, options={})
9
9
  @path = Pathname(path)
10
10
  @options = options
11
11
 
12
12
  if self.path.exist?
13
- content
13
+ @raw_content = path.read
14
14
  load_frontmatter
15
15
  end
16
16
 
17
17
  self.model_class.try(:models).try(:<<, to_model) unless model_instance_registered?
18
18
  end
19
19
 
20
- def content
21
- @content ||= path.read
20
+ def data
21
+ frontmatter
22
+ end
23
+
24
+ def sections
25
+ mappings = model_class.section_mappings
26
+
27
+ @sections = {}.to_mash
28
+
29
+ mappings.each do |name, mapping|
30
+ fragment = css("section[data-heading='#{name}']").first
31
+ @sections[name.parameterize.downcase.underscore] = Brief::Document::Section.new(name, fragment, mapping)
32
+ end
33
+
34
+ @sections
35
+ end
36
+
37
+ # Shortcut for querying the rendered HTML by css selectors.
38
+ #
39
+ # This will allow for model data attributes to be pulled from the
40
+ # document contents.
41
+ #
42
+ # Returns a Nokogiri::HTML::Element
43
+ def css(*args, &block)
44
+ parser.send(:css, *args, &block)
45
+ end
46
+
47
+ # Returns a Nokogiri::HTML::Element
48
+ def at(*args, &block)
49
+ parser.send(:at, *args, &block)
22
50
  end
23
51
 
24
52
  def extract_content(*args)
25
53
  options = args.extract_options!
26
- args = options[:args] if options.is_a?(Hash) && options.key?(:args)
54
+ args = options.delete(:args) if options.is_a?(Hash) && options.key?(:args)
27
55
 
28
56
  case
29
- when args.length == 1 && args.first.is_a?(String)
30
- css(args.first).try(:text).to_s
57
+ when options.empty? && args.length == 1 && args.first.is_a?(String)
58
+ results = css(args.first)
59
+ results = results.first if results.length > 1 && args.first.match(/:first-of-type/)
60
+ results.try(:text).to_s
61
+ else
62
+ binding.pry
31
63
  end
32
64
  end
33
65
 
34
- def data
35
- frontmatter
66
+ def relative_path_identifier
67
+ if Brief.case
68
+ path.relative_path_from(Brief.case.root)
69
+ else
70
+ path.to_s
71
+ end
36
72
  end
37
73
 
38
74
  def extension
@@ -60,6 +96,21 @@ module Brief
60
96
  super || data.respond_to?(method) || data.key?(method)
61
97
  end
62
98
 
99
+ def structure
100
+ @structure_analyzer ||= Brief::Document::Structure.new(fragment, self.raw_content.lines.to_a)
101
+ end
102
+
103
+ def parser
104
+ @parser ||= begin
105
+ structure.prescan
106
+ structure.create_wrappers
107
+ end
108
+ end
109
+
110
+ def fragment
111
+ @fragment ||= Nokogiri::HTML.fragment(to_raw_html)
112
+ end
113
+
63
114
  def method_missing(meth, *args, &block)
64
115
  if data.respond_to?(meth)
65
116
  data.send(meth, *args, &block)
@@ -67,7 +118,6 @@ module Brief
67
118
  super
68
119
  end
69
120
  end
70
-
71
121
  end
72
122
  end
73
123
 
data/lib/brief/dsl.rb CHANGED
@@ -13,6 +13,16 @@ module Brief
13
13
  definition.validate!
14
14
  end
15
15
 
16
+ # defines a method on the model instance named after the identifier
17
+ # and then creates a CLI command matching that, so for example:
18
+ #
19
+ # given a model called 'Post' and an action named 'publish' the
20
+ # brief CLI executable will respond to:
21
+ #
22
+ # brief publish posts PATH_GLOB
23
+ #
24
+ # this will find all of the Post models from the documents matching PATH_GLOB
25
+ # and call the publish method on them
16
26
  def action(identifier, options={}, &block)
17
27
  Object.class.class_eval do
18
28
  command "#{identifier}" do |c|
@@ -5,13 +5,15 @@ module Brief
5
5
  :metadata_schema,
6
6
  :content_schema,
7
7
  :options,
8
- :defined_helpers
8
+ :defined_helpers,
9
+ :section_mappings
9
10
 
10
11
  def initialize(name, options={})
11
12
  @name = name
12
13
  @options = options
13
14
  @type_alias = options.fetch(:type_alias) { name.downcase.parameterize.gsub(/-/,'_') }
14
15
  @metadata_schema = {}.to_mash
16
+ @section_mappings = {}.to_mash
15
17
  @content_schema = {attributes:{}}.to_mash
16
18
  @model_class = options[:model_class]
17
19
  end
@@ -107,15 +109,22 @@ module Brief
107
109
  @current == :content
108
110
  end
109
111
 
112
+ def section_mapping(identifier)
113
+ section_mappings.fetch(identifier)
114
+ end
115
+
110
116
  def method_missing(meth, *args, &block)
111
117
  args = args.dup
112
118
 
113
119
  if inside_content?
114
- if meth.to_s == :define_section
115
-
120
+ if meth.to_sym == :define_section
121
+ opts = args.extract_options!
122
+ identifier = args.first
123
+ self.section_mappings[identifier] ||= Brief::Document::Section::Mapping.new(identifier, opts)
124
+ section_mapping(identifier).instance_eval(&block) if block
125
+ else
126
+ self.content_schema.attributes[meth] = {args: args, block: block}
116
127
  end
117
-
118
- self.content_schema.attributes[meth] = {args: args, block: block}
119
128
  elsif inside_meta?
120
129
  if args.first.is_a?(Hash)
121
130
  args.unshift(String)
data/lib/brief/model.rb CHANGED
@@ -141,9 +141,17 @@ module Brief
141
141
  @definition
142
142
  end
143
143
 
144
+ def section_mapping(*args)
145
+ definition.send(:section_mapping, *args)
146
+ end
147
+
148
+ def section_mappings(*args)
149
+ definition.send(:section_mappings, *args)
150
+ end
151
+
144
152
  def method_missing(meth, *args, &block)
145
153
  if %w(meta content actions helpers).include?(meth.to_s)
146
- definition.send(meth, &block)
154
+ definition.send(meth, *args, &block)
147
155
  finalize
148
156
  elsif meth.to_s.match(/^on_(.*)_change$/)
149
157
  create_change_handler($1, *args, &block)
@@ -40,7 +40,7 @@ module Brief
40
40
  end
41
41
 
42
42
  def document_paths
43
- Dir[root.join("**/*.md").to_s]
43
+ Dir[root.join("**/*.md").to_s].map {|p| Pathname(p) }
44
44
  end
45
45
 
46
46
  def self.define_document_finder_methods
data/lib/brief/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Brief
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
data/lib/brief.rb CHANGED
@@ -40,6 +40,13 @@ module Brief
40
40
  def self.load_models(from_folder=nil)
41
41
  Brief::Model.load_all(from_folder: from_folder)
42
42
  end
43
+
44
+ # Adapters for Rails, Middleman, or Jekyll apps
45
+ def self.activate_adapter(identifier)
46
+ require "brief/adapters/#{ identifier }"
47
+ adapter = (Brief::Adapters.const_get(identifier.camelize) rescue nil)
48
+ adapter.try(:activate_adapter)
49
+ end
43
50
  end
44
51
 
45
52
  require "brief/core_ext"
@@ -49,6 +56,10 @@ require "brief/configuration"
49
56
  require "brief/document/rendering"
50
57
  require "brief/document/front_matter"
51
58
  require "brief/document/content_extractor"
59
+ require "brief/document/structure"
60
+ require "brief/document/section"
61
+ require "brief/document/section/mapping"
62
+ require "brief/document/section/builder"
52
63
  require "brief/document"
53
64
  require "brief/document_mapper"
54
65
  require "brief/repository"
@@ -14,6 +14,12 @@ which contains user stories and such.
14
14
 
15
15
  ## A user wants to write epics
16
16
 
17
+ As a **User** I want to **write epics** so that I can **write a bunch of user stories in one file**
18
+
17
19
  ## A user wants to annotate wireframes
18
20
 
21
+ As a **User** I want to **annotate wireframes** so that I can **augment diagrams with targeted explanations**
22
+
19
23
  ## A user wants to provide details about domain concepts
24
+
25
+ As a **User** I want to **provide some detailed explanations of domain concepts** so that I can **augment diagrams with targeted explanations**
@@ -8,10 +8,17 @@ class Brief::Epic
8
8
  end
9
9
 
10
10
  content do
11
- title "h1:first-child"
11
+ # have to do this so that the user stories section h1 doesnt get confused
12
+ title "h1:first-of-type"
12
13
 
13
14
  define_section "User Stories" do
14
- has_many :user_stories, "h2" => "title", "p:first-child" => "paragraph"
15
+ # NOT YET Implemented
16
+ each("h2").is_a :user_story
17
+
18
+ each("h2").has(:title => "h2",
19
+ :paragraph => "p:first-of-type",
20
+ :components => "p:first-of-type strong"
21
+ )
15
22
  end
16
23
  end
17
24
 
@@ -0,0 +1,37 @@
1
+ ---
2
+ type: epic
3
+ status: published
4
+ ---
5
+ This is a paragraph
6
+
7
+ This is another paragraph
8
+
9
+ This is a list:
10
+
11
+ - list one
12
+ - list two
13
+ - list three
14
+
15
+ ```ruby
16
+ # SOME CODE
17
+ ```
18
+
19
+ # User Stories
20
+
21
+ ## Story Title One
22
+
23
+ As a **User** I would like to **Behavior** so that I can **goal**
24
+
25
+ ## Story Title Two
26
+
27
+ As a **User** I would like to **Behavior** so that I can **goal**
28
+
29
+ ## Story Title Three
30
+
31
+ As a **User** I would like to **Behavior** so that I can **goal**
32
+
33
+ # Diagrams
34
+
35
+ - diagram one
36
+ - diagram one
37
+ - diagram one
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: epic
3
+ status: published
4
+ ---
5
+
6
+ # This is the title
7
+
8
+ This is a paragraph
9
+
10
+ This is another paragraph
11
+
12
+ This is a list:
13
+
14
+ - list one
15
+ - list two
16
+ - list three
17
+
18
+ ```ruby
19
+ # SOME CODE
20
+ ```
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: epic
3
+ status: published
4
+ ---
5
+
6
+ # This is the title
7
+
8
+ This is a paragraph
9
+
10
+ This is another paragraph
11
+
12
+ This is a list:
13
+
14
+ - list one
15
+ - list two
16
+ - list three
17
+
18
+ ```ruby
19
+ # SOME CODE
20
+ ```
21
+
22
+ # User Stories
23
+
24
+ ## Story Title One
25
+
26
+ As a **User** I would like to **Behavior** so that I can **goal**
27
+
28
+ ## Story Title Two
29
+
30
+ As a **User** I would like to **Behavior** so that I can **goal**
31
+
32
+ ## Story Title Three
33
+
34
+ As a **User** I would like to **Behavior** so that I can **goal**
35
+
36
+ # Diagrams
37
+
38
+ - diagram one
39
+ - diagram one
40
+ - diagram one
@@ -7,21 +7,37 @@ describe "The Brief Document" do
7
7
  end
8
8
 
9
9
  it "renders html" do
10
- expect(sample.to_html).to include("<h1>User Stories</h1>")
10
+ expect(sample.to_html).to match(/h1.*User Stories.*h1\>/)
11
11
  end
12
12
 
13
13
  it "parses the html" do
14
14
  expect(sample.css("h1").length).to eq(2)
15
15
  end
16
16
 
17
+
17
18
  it "deserializes YAML frontmatter into attributes" do
18
19
  expect(sample.frontmatter.type).to eq("epic")
19
20
  end
20
21
 
21
22
  context "Content Extraction" do
22
23
  it "extracts content from a css selector" do
23
- extracted = sample.extract_content(:args => ["h1:first-child"])
24
+ extracted = sample.extract_content(:args => ["h1:first-of-type"])
24
25
  expect(extracted).to eq("Blueprint Epic Example")
25
26
  end
26
27
  end
28
+
29
+ context "defining sections" do
30
+ it "lets me define content sections" do
31
+ expect(sample.sections).not_to be_empty
32
+ expect(sample.sections.user_stories).to be_present
33
+ expect(sample.sections.user_stories.fragment.name).to eq("section")
34
+ expect(sample.sections.user_stories.fragment.css("article").length).to eq(3)
35
+ end
36
+
37
+ it "gives me an array of items underneath the section filled with the key value mappings i laid out" do
38
+ items = sample.sections.user_stories.items
39
+ expect(items.length).to eq(3)
40
+ expect(items.map(&:components).map(&:first).uniq).to eq(["User"])
41
+ end
42
+ end
27
43
  end
@@ -102,4 +102,11 @@ describe "The Brief Model" do
102
102
  expect(user_story.class.defined_actions).to include(:custom_action)
103
103
  end
104
104
  end
105
+
106
+ context "Section Mappings" do
107
+ it "defines a section mapping for User Stories" do
108
+ mapping = epic.class.section_mapping("User Stories")
109
+ expect(mapping).to be_a(Brief::Document::Section::Mapping)
110
+ end
111
+ end
105
112
  end
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ describe "Brief HTML Rendering" do
4
+ let(:sample) do
5
+ path = Brief.example_path.join("docs","epic.html.md")
6
+ Brief::Document.new(path)
7
+ end
8
+
9
+ it "wraps the document with some identifying details" do
10
+ expect(sample.to_html).to include("docs/epic.html.md")
11
+ end
12
+
13
+ it "wraps the higher level headings under section elements" do
14
+ expect(sample.css("section").length).to eq(2)
15
+ end
16
+
17
+ it "wraps the lower level headings under article elements" do
18
+ expect(sample.css("article").length).to eq(3)
19
+ end
20
+
21
+ it "nests the articles under the parent section" do
22
+ expect(sample.css("section article").length).to eq(3)
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe "The Section Builder" do
4
+ let(:builder) do
5
+ inputs = [
6
+ [1, ["<section><h1>Heading</h1></section>"]],
7
+ [1, ["<section><h1>Section Heading</h1></section>"]],
8
+ [2, ["<article><h2>a</h2></article>"]],
9
+ [2, ["<article><h2>b</h2></article>"]],
10
+ [2, ["<article><h2>c</h2></article>"]],
11
+ [1, ["<section><h1>Footer</h1></section>"]]
12
+ ]
13
+
14
+ Brief::Document::Section::Builder.new(inputs)
15
+ end
16
+
17
+ it "collapses the HTML into sections for us" do
18
+ expect(builder.to_fragment.css("section h1").count).to eq(3)
19
+ expect(builder.to_fragment.css("section article").count).to eq(3)
20
+ end
21
+
22
+ end
@@ -0,0 +1,30 @@
1
+ require "spec_helper"
2
+
3
+ describe "Document Structure Information" do
4
+ let(:sample) do
5
+ path = Brief.example_path.join("docs","epic.html.md")
6
+ Brief::Document.new(path)
7
+ end
8
+
9
+ let(:one) do
10
+ path = Brief.spec_root.join("fixtures","structures", "one.html.md")
11
+ Brief::Document.new(path)
12
+ end
13
+
14
+ let(:two) do
15
+ path = Brief.spec_root.join("fixtures","structures", "two.html.md")
16
+ Brief::Document.new(path)
17
+ end
18
+
19
+ let(:three) do
20
+ path = Brief.spec_root.join("fixtures","structures", "three.html.md")
21
+ Brief::Document.new(path)
22
+ end
23
+
24
+ context "Mapping markdown source to output html" do
25
+ it "should put the line numbers on the headings" do
26
+ el = sample.css("h1").first
27
+ expect(el.attr('data-line-number')).to eq("8")
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brief
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Soeder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-07 00:00:00.000000000 Z
11
+ date: 2015-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -216,6 +216,7 @@ extensions: []
216
216
  extra_rdoc_files: []
217
217
  files:
218
218
  - ".gitignore"
219
+ - CHANGELOG.md
219
220
  - Gemfile
220
221
  - Gemfile.lock
221
222
  - LICENSE.txt
@@ -228,6 +229,7 @@ files:
228
229
  - examples/blog/docs/an-intro-to-brief.html.md
229
230
  - lib/.DS_Store
230
231
  - lib/brief.rb
232
+ - lib/brief/adapters/middleman.rb
231
233
  - lib/brief/briefcase.rb
232
234
  - lib/brief/cli/change.rb
233
235
  - lib/brief/cli/init.rb
@@ -238,6 +240,10 @@ files:
238
240
  - lib/brief/document/content_extractor.rb
239
241
  - lib/brief/document/front_matter.rb
240
242
  - lib/brief/document/rendering.rb
243
+ - lib/brief/document/section.rb
244
+ - lib/brief/document/section/builder.rb
245
+ - lib/brief/document/section/mapping.rb
246
+ - lib/brief/document/structure.rb
241
247
  - lib/brief/document_mapper.rb
242
248
  - lib/brief/dsl.rb
243
249
  - lib/brief/model.rb
@@ -255,12 +261,18 @@ files:
255
261
  - spec/fixtures/example/docs/user_story.html.md
256
262
  - spec/fixtures/example/docs/wireframe.html.md
257
263
  - spec/fixtures/example/models/epic.rb
264
+ - spec/fixtures/structures/one.html.md
265
+ - spec/fixtures/structures/three.html.md
266
+ - spec/fixtures/structures/two.html.md
258
267
  - spec/lib/brief/briefcase_spec.rb
259
268
  - spec/lib/brief/document_spec.rb
260
269
  - spec/lib/brief/dsl_spec.rb
261
270
  - spec/lib/brief/model_spec.rb
262
271
  - spec/lib/brief/persistence_spec.rb
272
+ - spec/lib/brief/rendering_spec.rb
263
273
  - spec/lib/brief/repository_spec.rb
274
+ - spec/lib/brief/section_builder_spec.rb
275
+ - spec/lib/brief/structure_spec.rb
264
276
  - spec/spec_helper.rb
265
277
  - spec/support/test_helpers.rb
266
278
  - tasks/brief/release.rake
@@ -298,12 +310,18 @@ test_files:
298
310
  - spec/fixtures/example/docs/user_story.html.md
299
311
  - spec/fixtures/example/docs/wireframe.html.md
300
312
  - spec/fixtures/example/models/epic.rb
313
+ - spec/fixtures/structures/one.html.md
314
+ - spec/fixtures/structures/three.html.md
315
+ - spec/fixtures/structures/two.html.md
301
316
  - spec/lib/brief/briefcase_spec.rb
302
317
  - spec/lib/brief/document_spec.rb
303
318
  - spec/lib/brief/dsl_spec.rb
304
319
  - spec/lib/brief/model_spec.rb
305
320
  - spec/lib/brief/persistence_spec.rb
321
+ - spec/lib/brief/rendering_spec.rb
306
322
  - spec/lib/brief/repository_spec.rb
323
+ - spec/lib/brief/section_builder_spec.rb
324
+ - spec/lib/brief/structure_spec.rb
307
325
  - spec/spec_helper.rb
308
326
  - spec/support/test_helpers.rb
309
327
  has_rdoc: