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



330
331
332
333
334
335
336
# File 'lib/brief/document.rb', line 330

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



204
205
206
# File 'lib/brief/document.rb', line 204

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

#attachmentsObject



133
134
135
# File 'lib/brief/document.rb', line 133

def attachments
  Array(data.attachments)
end

#briefcaseObject



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

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



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

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

#content_hashObject



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

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

#content_stale?Boolean

Returns:

  • (Boolean)


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

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



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

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

#dataObject



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

def data
  frontmatter
end

#documentObject



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

def document
  self
end

#document_typeObject



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

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

#document_type!Object



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

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

#exist?Boolean

Returns:

  • (Boolean)


244
245
246
# File 'lib/brief/document.rb', line 244

def exist?
  path && path.exist?
end

#extensionObject



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

def extension
  path.extname
end

#extract_content(*args) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/brief/document.rb', line 208

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



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

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

#fragmentObject



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

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

#has_sections?Boolean

Returns:

  • (Boolean)


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

def has_sections?
  model_class.section_mappings.length > 0
end

#in_briefcase(briefcase) ⇒ Object



137
138
139
140
141
142
143
144
145
# File 'lib/brief/document.rb', line 137

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)


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

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



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

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

#model_classObject



248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/brief/document.rb', line 248

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)


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

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

#parent_folder_nameObject



273
274
275
# File 'lib/brief/document.rb', line 273

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



301
302
303
304
305
306
307
308
309
# File 'lib/brief/document.rb', line 301

def parser
  @parser ||= begin
                structure.prescan

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

#path_aliasObject



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

def path_alias
  relative_path.to_s.gsub(relative_path.extname.to_s, '')
end

#raw=(val) ⇒ Object



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

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

#refresh!Object



93
94
95
96
97
98
99
100
101
102
# File 'lib/brief/document.rb', line 93

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

#refreshedObject



104
105
106
107
# File 'lib/brief/document.rb', line 104

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



222
223
224
225
226
227
228
# File 'lib/brief/document.rb', line 222

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)


286
287
288
289
# File 'lib/brief/document.rb', line 286

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

#saveObject



82
83
84
85
86
87
88
89
90
91
# File 'lib/brief/document.rb', line 82

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



109
110
111
112
113
114
115
116
117
118
# File 'lib/brief/document.rb', line 109

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



155
156
157
# File 'lib/brief/document.rb', line 155

def section_headings
  sections.keys
end

#sectionsObject



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/brief/document.rb', line 168

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



159
160
161
162
163
164
165
166
# File 'lib/brief/document.rb', line 159

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)


78
79
80
# File 'lib/brief/document.rb', line 78

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



295
296
297
# File 'lib/brief/document.rb', line 295

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



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

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



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

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

#typeObject



326
327
328
# File 'lib/brief/document.rb', line 326

def type
  document_type
end