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



321
322
323
324
325
326
327
# File 'lib/brief/document.rb', line 321

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



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

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

#attachmentsObject



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

def attachments
  Array(data.attachments)
end

#briefcaseObject



138
139
140
# File 'lib/brief/document.rb', line 138

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



111
112
113
114
# File 'lib/brief/document.rb', line 111

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



190
191
192
# File 'lib/brief/document.rb', line 190

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

#dataObject



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

def data
  frontmatter
end

#documentObject



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

def document
  self
end

#document_typeObject



254
255
256
# File 'lib/brief/document.rb', line 254

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

#document_type!Object



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

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

#exist?Boolean

Returns:

  • (Boolean)


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

def exist?
  path && path.exist?
end

#extensionObject



221
222
223
# File 'lib/brief/document.rb', line 221

def extension
  path.extname
end

#extract_content(*args) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/brief/document.rb', line 199

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



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

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

#has_sections?Boolean

Returns:

  • (Boolean)


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

def has_sections?
  model_class.section_mappings.length > 0
end

#in_briefcase(briefcase) ⇒ Object



128
129
130
131
132
133
134
135
136
# File 'lib/brief/document.rb', line 128

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)


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

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



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

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

#model_classObject



239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/brief/document.rb', line 239

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)


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

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

#parent_folder_nameObject



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

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



292
293
294
295
296
297
298
299
300
# File 'lib/brief/document.rb', line 292

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

#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



213
214
215
216
217
218
219
# File 'lib/brief/document.rb', line 213

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)


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

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



100
101
102
103
104
105
106
107
108
109
# File 'lib/brief/document.rb', line 100

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



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

def section_headings
  sections.keys
end

#sectionsObject



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

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



150
151
152
153
154
155
156
157
# File 'lib/brief/document.rb', line 150

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



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

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



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

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



308
309
310
311
# File 'lib/brief/document.rb', line 308

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

#typeObject



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

def type
  document_type
end