brief 1.1.0 → 1.2.0

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