Class: Brief::Document

Inherits:
Object
  • Object
show all
Includes:
Attachments, FrontMatter, Rendering, SourceMap, Templating
Defined in:
lib/brief/document.rb,
lib/brief/document/rendering.rb,
lib/brief/document/attachments.rb,
lib/brief/document/front_matter.rb

Defined Under Namespace

Modules: Attachments, FrontMatter, Rendering, SourceMap, Templating Classes: ContentExtractor, Section, Structure, Transformer

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SourceMap

#content_under_heading, #heading_details, #heading_element_tags, #heading_line_numbers, #line_numbers_for_heading, #next_sibling_heading_for

Methods included from Attachments

#attachment_paths, #has_attachments?, #render_attachments

Methods included from Templating

#generate_content

Methods included from FrontMatter

#data=, #frontmatter_line_count

Methods included from Rendering

#script_contents, #script_preamble, #to_html, #unwrapped_html

Constructor Details

#initialize(path, options = {}) ⇒ Document

Returns a new instance of Document.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/brief/document.rb', line 14

def initialize(path, options = {})
  if path.respond_to?(:key?) && options.empty?
    @frontmatter = path.to_mash
  else
    @path = Pathname(path)
  end

  @options = options.to_mash

  if @path && self.path.exist?
    @raw_content = self.path.read
    load_frontmatter
  elsif options[:contents]
    @raw_content = options[:contents]
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object



326
327
328
329
330
331
332
# File 'lib/brief/document.rb', line 326

def method_missing(meth, *args, &block)
  if data.respond_to?(meth)
    data.send(meth, *args, &block)
  else
    super
  end
end

Instance Attribute Details

#contentObject

Returns the value of attribute content.



12
13
14
# File 'lib/brief/document.rb', line 12

def content
  @content
end

#frontmatterObject

Returns the value of attribute frontmatter.



12
13
14
# File 'lib/brief/document.rb', line 12

def frontmatter
  @frontmatter
end

#optionsObject

Returns the value of attribute options.



12
13
14
# File 'lib/brief/document.rb', line 12

def options
  @options
end

#pathObject

Returns the value of attribute path.



12
13
14
# File 'lib/brief/document.rb', line 12

def path
  @path
end

#raw_contentObject

Returns the value of attribute raw_content.



12
13
14
# File 'lib/brief/document.rb', line 12

def raw_content
  @raw_content
end

Class Method Details

.from_contents(content, frontmatter, &block) ⇒ Object



9
10
# File 'lib/brief/document.rb', line 9

def self.from_contents(content, frontmatter, &block)
end

Instance Method Details

#at(*args, &block) ⇒ Object

Returns a Nokogiri::HTML::Element



200
201
202
# File 'lib/brief/document.rb', line 200

def at(*args, &block)
  parser.send(:at, *args, &block)
end

#attachmentsObject



129
130
131
# File 'lib/brief/document.rb', line 129

def attachments
  Array(data.attachments)
end

#briefcaseObject



143
144
145
# File 'lib/brief/document.rb', line 143

def briefcase
  (@briefcase_root && Brief.cases[@briefcase_root.basename.to_s]) || Brief.case(true)
end

#cloneObject



35
36
37
# File 'lib/brief/document.rb', line 35

def clone
  new(path, options)
end

#combined_data_and_contentObject



116
117
118
119
# File 'lib/brief/document.rb', line 116

def combined_data_and_content
  return content if data.nil? || data.empty?
  frontmatter.to_hash.to_yaml + "---\n\n#{ content }"
end

#content_hashObject



55
56
57
# File 'lib/brief/document.rb', line 55

def content_hash
  Digest::MD5.hexdigest(@content.to_s)
end

#content_stale?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'lib/brief/document.rb', line 63

def content_stale?
  content_hash != file_hash
end

#css(*args, &block) ⇒ Object

Shortcut for querying the rendered HTML by css selectors.

This will allow for model data attributes to be pulled from the document contents.

Returns a Nokogiri::HTML::Element



195
196
197
# File 'lib/brief/document.rb', line 195

def css(*args, &block)
  parser.send(:css, *args, &block)
end

#dataObject



121
122
123
# File 'lib/brief/document.rb', line 121

def data
  frontmatter
end

#documentObject



31
32
33
# File 'lib/brief/document.rb', line 31

def document
  self
end

#document_typeObject



259
260
261
# File 'lib/brief/document.rb', line 259

def document_type
  options.fetch(:type) { document_type! }
end

#document_type!Object



263
264
265
266
267
# File 'lib/brief/document.rb', line 263

def document_type!
  existing = data && data.type
  return existing if existing
  parent_folder_name.try(:singularize)
end

#exist?Boolean

Returns:

  • (Boolean)


240
241
242
# File 'lib/brief/document.rb', line 240

def exist?
  path && path.exist?
end

#extensionObject



226
227
228
# File 'lib/brief/document.rb', line 226

def extension
  path.extname
end

#extract_content(*args) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/brief/document.rb', line 204

def extract_content(*args)
  options = args.extract_options!
  args    = options.delete(:args) if options.is_a?(Hash) && options.key?(:args)

  case
  when options.empty? && args.length == 1 && args.first.is_a?(String)
    results = css(args.first)
    results = results.first if results.length > 1 && args.first.match(/:first-of-type/)
    results.try(:text).to_s
  else
    binding.pry
  end
end

#file_hashObject



59
60
61
# File 'lib/brief/document.rb', line 59

def file_hash
  Digest::MD5.hexdigest(path.read.to_s)
end

#fragmentObject



318
319
320
# File 'lib/brief/document.rb', line 318

def fragment
  @fragment ||= Nokogiri::HTML.fragment(to_raw_html)
end

#has_sections?Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/brief/document.rb', line 147

def has_sections?
  model_class.section_mappings.length > 0
end

#in_briefcase(briefcase) ⇒ Object



133
134
135
136
137
138
139
140
141
# File 'lib/brief/document.rb', line 133

def in_briefcase(briefcase)
  @briefcase_root = briefcase.root

  unless Brief::Util.ensure_child_path(briefcase.docs_path, path)
    raise 'Invalid document path'
  end

  self
end

#include_attachments?Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/brief/document.rb', line 125

def include_attachments?
  attachments.length > 0
end

#inspectObject



47
48
49
# File 'lib/brief/document.rb', line 47

def inspect
  "#{ model_class }.at_path(#{relative_path})"
end

#model_attributesObject



230
231
232
233
234
# File 'lib/brief/document.rb', line 230

def model_attributes
  (data || {}).to_hash
    .merge(path: path, document: self)
    .reverse_merge(type: document_type)
end

#model_classObject



244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/brief/document.rb', line 244

def model_class
  case
  when @model_class
    @model_class
  when briefcase
    briefcase.model_class_for(self)
  when data && data.type
    Brief::Model.for_type(data.type)
  when parent_folder_name.length > 0
    Brief::Model.for_folder_name(parent_folder_name)
  else
    raise 'Could not determine the model class to use for this document. Specify the type, or put it in a folder that maps to the correct type.'
  end
end

#model_instance_registered?Boolean

Each model class tracks the instances of the models created and ensures that there is a 1-1 relationship between a document path and the model.

Returns:

  • (Boolean)


276
277
278
279
280
# File 'lib/brief/document.rb', line 276

def model_instance_registered?
  model_class && model_class.models.any? do |model|
    model.path == path
  end
end

#parent_folder_nameObject



269
270
271
# File 'lib/brief/document.rb', line 269

def parent_folder_name
  path.parent.basename.to_s.downcase
end

#parserObject

The Parser wraps the rendered HTML in a nokogiri element so we can easily manipulate it. Prior to doing so, we use the structure analyzer to build more metadata into the markup



297
298
299
300
301
302
303
304
305
# File 'lib/brief/document.rb', line 297

def parser
  @parser ||= begin
                structure.prescan

                structure.create_wrappers.tap do |f|
                  transformer_for(f).all
                end
              end
end

#raw=(val) ⇒ Object



67
68
69
70
71
72
# File 'lib/brief/document.rb', line 67

def raw= val
  @raw_set = true
  @raw_content = val
  #document.load_frontmatter
  @raw_content
end

#refresh!Object



89
90
91
92
93
94
95
96
97
98
# File 'lib/brief/document.rb', line 89

def refresh!
  @content = nil
  @raw_content = path.read
  @frontmatter = nil
  @raw_frontmatter = nil
  @refreshing = true
  @content_hash = nil
  load_frontmatter
  true
end

#refreshedObject



100
101
102
103
# File 'lib/brief/document.rb', line 100

def refreshed
  refresh!
  self
end

#relative_pathObject



51
52
53
# File 'lib/brief/document.rb', line 51

def relative_path
  briefcase.present? ? path.relative_path_from(briefcase.docs_path) : path
end

#relative_path_identifierObject



218
219
220
221
222
223
224
# File 'lib/brief/document.rb', line 218

def relative_path_identifier
  if Brief.case
    path.relative_path_from(Brief.case.root)
  else
    path.to_s
  end
end

#respond_to?(*args) ⇒ Boolean

Returns:

  • (Boolean)


282
283
284
285
# File 'lib/brief/document.rb', line 282

def respond_to?(*args)
  method = args.first
  super || (data && data.respond_to?(method)) || (data && data.key?(method))
end

#saveObject



78
79
80
81
82
83
84
85
86
87
# File 'lib/brief/document.rb', line 78

def save
  if set_raw?
    file_contents = raw_content
  else
    file_contents = combined_data_and_content
  end

  path.open('w') {|fh| fh.write(file_contents) }
  refresh!
end

#save!Object



105
106
107
108
109
110
111
112
113
114
# File 'lib/brief/document.rb', line 105

def save!
  if set_raw?
    file_contents = raw_content
  else
    file_contents = combined_data_and_content
  end

  path.open('w+') {|fh| fh.write(file_contents) }
  refresh!
end

#section_headingsObject



151
152
153
# File 'lib/brief/document.rb', line 151

def section_headings
  sections.keys
end

#sectionsObject



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/brief/document.rb', line 164

def sections
  mappings = model_class.section_mappings

  @sections = {}.to_mash

  mappings.each do |name, mapping|
    fragment = css("section[data-heading='#{name}']").first
    @sections[name.parameterize.downcase.underscore] = Brief::Document::Section.new(name, fragment, mapping)
  end

  @sections
end

#sections_dataObject



155
156
157
158
159
160
161
162
# File 'lib/brief/document.rb', line 155

def sections_data
  section_headings.reduce({}) do |memo, heading|
    section = sections.send(heading)
    items = section.items rescue nil
    memo[heading] = items if items
    memo
  end
end

#set_raw?Boolean

Returns:

  • (Boolean)


74
75
76
# File 'lib/brief/document.rb', line 74

def set_raw?
  !!@raw_set
end

#structureObject

The structure analyzer of the document is responsible for grouping the content under the headings by wrapping them in divs, and creating relationships between the nodes. This is what lets us provide an easy iteration API on top of the parsed document



291
292
293
# File 'lib/brief/document.rb', line 291

def structure
  @structure_analyzer ||= Brief::Document::Structure.new(fragment, raw_content.lines.to_a)
end

#titleObject



39
40
41
# File 'lib/brief/document.rb', line 39

def title
  (data && data.title) || css('h1:first-of-type').text || path.to_s.split("/").last.gsub(/\..*/,'')
end

#to_modelObject



236
237
238
# File 'lib/brief/document.rb', line 236

def to_model
  model_class.try(:new, model_attributes)
end

#to_sObject



43
44
45
# File 'lib/brief/document.rb', line 43

def to_s
  "#{ model_class }.at_path(#{relative_path})"
end

#transformer_for(doc_fragment = nil) ⇒ Object

The transformer is responsible for performing content modifications on the rendered document. This is useful for supporting extensions that are driven by the markdown language.

TODO: This is hidden behind a feature flag, and requires the document to have metadata that specifies transform = true



313
314
315
316
# File 'lib/brief/document.rb', line 313

def transformer_for(doc_fragment=nil)
  doc_fragment ||= fragment
  @transformer ||= Brief::Document::Transformer.new(doc_fragment, self)
end

#typeObject



322
323
324
# File 'lib/brief/document.rb', line 322

def type
  document_type
end