lookbook 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +233 -5
  3. data/app/assets/lookbook/css/app.css +15 -3
  4. data/app/assets/lookbook/js/app.js +8 -0
  5. data/app/assets/lookbook/js/components/app.js +55 -0
  6. data/app/assets/lookbook/js/components/embed.js +89 -0
  7. data/app/assets/lookbook/js/components/filter.js +13 -2
  8. data/app/assets/lookbook/js/components/nav-group.js +3 -6
  9. data/app/assets/lookbook/js/components/nav-item.js +3 -1
  10. data/app/assets/lookbook/js/components/nav.js +8 -15
  11. data/app/assets/lookbook/js/components/page.js +19 -41
  12. data/app/assets/lookbook/js/components/sidebar.js +16 -1
  13. data/app/assets/lookbook/js/components/splitter.js +1 -1
  14. data/app/assets/lookbook/js/components/tabs.js +3 -1
  15. data/app/assets/lookbook/js/embed.js +1 -0
  16. data/app/assets/lookbook/js/lib/split.js +0 -6
  17. data/app/assets/lookbook/js/stores/layout.js +3 -0
  18. data/app/assets/lookbook/js/stores/pages.js +5 -0
  19. data/app/assets/lookbook/js/stores/sidebar.js +2 -1
  20. data/app/controllers/lookbook/application_controller.rb +28 -0
  21. data/app/controllers/lookbook/page_controller.rb +20 -0
  22. data/app/controllers/lookbook/pages_controller.rb +40 -0
  23. data/app/controllers/lookbook/{app_controller.rb → previews_controller.rb} +39 -67
  24. data/app/helpers/lookbook/application_helper.rb +8 -61
  25. data/app/helpers/lookbook/component_helper.rb +40 -0
  26. data/app/helpers/lookbook/output_helper.rb +15 -0
  27. data/app/helpers/lookbook/page_helper.rb +46 -0
  28. data/app/helpers/lookbook/preview_helper.rb +1 -1
  29. data/app/views/layouts/lookbook/application.html.erb +30 -0
  30. data/app/views/layouts/lookbook/basic.html.erb +7 -0
  31. data/app/views/layouts/lookbook/preview.html.erb +5 -1
  32. data/app/views/layouts/lookbook/skeleton.html.erb +28 -0
  33. data/app/views/lookbook/components/_branding.html.erb +8 -0
  34. data/app/views/lookbook/components/_code.html.erb +2 -2
  35. data/app/views/lookbook/components/_copy_button.html.erb +11 -0
  36. data/app/views/lookbook/components/_drawer.html.erb +4 -16
  37. data/app/views/lookbook/components/_embed.html.erb +39 -0
  38. data/app/views/lookbook/components/_filter.html.erb +8 -5
  39. data/app/views/lookbook/components/_header.html.erb +2 -4
  40. data/app/views/lookbook/components/_icon.html.erb +2 -2
  41. data/app/views/lookbook/components/_nav.html.erb +7 -8
  42. data/app/views/lookbook/components/_nav_group.html.erb +1 -1
  43. data/app/views/lookbook/components/_nav_item.html.erb +4 -3
  44. data/app/views/lookbook/components/_nav_page.html.erb +22 -0
  45. data/app/views/lookbook/components/_nav_preview.html.erb +1 -1
  46. data/app/views/lookbook/components/_not_found.html.erb +11 -0
  47. data/app/views/lookbook/components/_param.html.erb +1 -1
  48. data/app/views/lookbook/components/_preview.html.erb +16 -10
  49. data/app/views/lookbook/components/_sidebar.html.erb +55 -0
  50. data/app/views/lookbook/pages/not_found.html.erb +15 -0
  51. data/app/views/lookbook/pages/show.html.erb +71 -0
  52. data/app/views/lookbook/previews/error.html.erb +1 -0
  53. data/app/views/lookbook/{inputs → previews/inputs}/_select.html.erb +0 -0
  54. data/app/views/lookbook/{inputs → previews/inputs}/_text.html.erb +0 -0
  55. data/app/views/lookbook/{inputs → previews/inputs}/_textarea.html.erb +0 -0
  56. data/app/views/lookbook/{inputs → previews/inputs}/_toggle.html.erb +0 -0
  57. data/app/views/lookbook/{not_found.html.erb → previews/not_found.html.erb} +2 -2
  58. data/app/views/lookbook/{panels → previews/panels}/_notes.html.erb +2 -2
  59. data/app/views/lookbook/{panels → previews/panels}/_output.html.erb +0 -0
  60. data/app/views/lookbook/{panels → previews/panels}/_params.html.erb +0 -0
  61. data/app/views/lookbook/{panels → previews/panels}/_preview.html.erb +0 -0
  62. data/app/views/lookbook/{panels → previews/panels}/_source.html.erb +0 -0
  63. data/app/views/lookbook/{show.html.erb → previews/show.html.erb} +0 -0
  64. data/config/routes.rb +9 -4
  65. data/lib/lookbook/code_formatter.rb +22 -1
  66. data/lib/lookbook/code_inspector.rb +73 -0
  67. data/lib/lookbook/collection.rb +110 -8
  68. data/lib/lookbook/engine.rb +59 -32
  69. data/lib/lookbook/features.rb +1 -1
  70. data/lib/lookbook/markdown.rb +31 -0
  71. data/lib/lookbook/page.rb +146 -0
  72. data/lib/lookbook/page_collection.rb +11 -0
  73. data/lib/lookbook/parser.rb +2 -0
  74. data/lib/lookbook/preview.rb +52 -56
  75. data/lib/lookbook/preview_collection.rb +15 -0
  76. data/lib/lookbook/preview_example.rb +27 -29
  77. data/lib/lookbook/preview_group.rb +12 -6
  78. data/lib/lookbook/utils.rb +74 -0
  79. data/lib/lookbook/version.rb +1 -1
  80. data/lib/lookbook.rb +18 -1
  81. data/public/lookbook-assets/css/app.css +1 -1
  82. data/public/lookbook-assets/css/app.css.map +1 -1
  83. data/public/lookbook-assets/js/app.js +1 -1
  84. data/public/lookbook-assets/js/app.js.map +1 -1
  85. data/public/lookbook-assets/js/embed.js +2 -0
  86. data/public/lookbook-assets/js/embed.js.map +1 -0
  87. metadata +44 -18
  88. data/app/assets/lookbook/js/lib/utils.js +0 -3
  89. data/app/views/layouts/lookbook/app.html.erb +0 -60
  90. data/app/views/lookbook/error.html.erb +0 -1
  91. data/lib/lookbook/taggable.rb +0 -46
@@ -0,0 +1,73 @@
1
+ module Lookbook
2
+ class CodeInspector
3
+ include Utils
4
+
5
+ attr_reader :code_object
6
+ delegate :groups, :source, to: :@code_object, allow_nil: true
7
+
8
+ def initialize(taggable_object_path)
9
+ @code_object = Lookbook::Engine.parser.get_code_object(taggable_object_path)
10
+ end
11
+
12
+ def hidden?
13
+ if code_object&.tag(:hidden)
14
+ code_object.tag(:hidden).text.strip != "false"
15
+ end
16
+ end
17
+
18
+ def id
19
+ if code_object&.tag(:id)&.text&.present?
20
+ generate_id(code_object&.tag(:id)&.text)
21
+ end
22
+ end
23
+
24
+ def label
25
+ code_object&.tag(:label)&.text
26
+ end
27
+
28
+ def notes
29
+ if code_object&.docstring
30
+ code_object.docstring.to_s.strip
31
+ end
32
+ end
33
+
34
+ def group
35
+ code_object&.group
36
+ end
37
+
38
+ def position
39
+ code_object&.tag(:position)&.text&.to_i || 10000
40
+ end
41
+
42
+ def display_params
43
+ display_params = {}.with_indifferent_access
44
+ if code_object&.tags(:display).present?
45
+ code_object.tags(:display).each do |tag|
46
+ parts = tag.text.strip.match(/^([^\s]*)\s?(.*)$/)
47
+ if parts.present?
48
+ begin
49
+ display_params[parts[1]] = YAML.safe_load(parts[2] || "~")
50
+ rescue SyntaxError => err
51
+ Lookbook.logger.error("\n👀 [Lookbook] Invalid JSON in @display tag.\n👀 [Lookbook] (#{err})\n")
52
+ end
53
+ end
54
+ end
55
+ end
56
+ display_params
57
+ end
58
+
59
+ def parameter_defaults
60
+ code_object&.parameters&.map { |str| Params.parse_method_param_str(str) }&.compact&.to_h
61
+ end
62
+
63
+ def params
64
+ code_object&.tags("param")&.map do |param|
65
+ Lookbook::Params.build_param(param, parameter_defaults[param.name])
66
+ end
67
+ end
68
+
69
+ def methods
70
+ code_object&.meths
71
+ end
72
+ end
73
+ end
@@ -1,33 +1,56 @@
1
1
  module Lookbook
2
2
  class Collection
3
+ include Utils
4
+ include Enumerable
5
+
3
6
  attr_reader :path
7
+ delegate :size, :each, to: :items
4
8
 
5
- def initialize(path = "")
9
+ def initialize(path = "", items = [])
10
+ if path.is_a?(Array)
11
+ @items = path
12
+ path = ""
13
+ else
14
+ @items = items
15
+ end
6
16
  @path = path.delete_prefix("/").delete_suffix("/")
7
- @items = []
8
17
  end
9
18
 
10
19
  def id
11
- (@path || "root").underscore.tr("_", "-")
20
+ generate_id(lookup_path || "root")
12
21
  end
13
22
 
14
23
  def name
15
- @path.present? ? @path.split("/").last : "root"
24
+ @name ||= remove_position_prefix(basename)
16
25
  end
17
26
 
18
27
  def label
19
28
  name&.titleize
20
29
  end
21
30
 
31
+ def lookup_path
32
+ @lookup_path ||= to_lookup_path(@path)
33
+ end
34
+
35
+ def position
36
+ @position ||= parse_position_prefix(basename).first
37
+ end
38
+
22
39
  def hierarchy_depth
23
40
  @path ? @path.split("/").size : 0
24
41
  end
25
42
 
26
- def items(sorted = true)
27
- sorted ? @items.sort_by(&:label) : @items
43
+ def items
44
+ @items.sort_by { |item| [item.hierarchy_depth, item.position, item.label] }
45
+ end
46
+
47
+ def visible_items
48
+ reject { |i| i.hidden? }
28
49
  end
29
50
 
30
51
  def add(item)
52
+ @ordered_entities = nil
53
+ @tree = nil
31
54
  if item.is_a?(String)
32
55
  item = Collection.new([@path, item].join("/"))
33
56
  end
@@ -37,15 +60,94 @@ module Lookbook
37
60
 
38
61
  def get(name)
39
62
  name = name.underscore
40
- @items.find { |item| item.name.underscore == name }
63
+ items.find { |item| item.name.underscore == name }
41
64
  end
42
65
 
43
66
  def get_or_create(name)
44
- get(name).presence || add(name)
67
+ get(remove_position_prefix(name)).presence || add(name)
68
+ end
69
+
70
+ def find(lookup = nil, &block)
71
+ if lookup
72
+ lookup.is_a?(Symbol) ? find_by_id(lookup.to_s.tr("_", "-")) : find_by_path(lookup)
73
+ elsif block
74
+ items.find(&block)
75
+ end
76
+ end
77
+
78
+ def find_by_id(id)
79
+ items.find { |i| i.id == id }
80
+ end
81
+
82
+ def find_by_path(path)
83
+ items.find { |i| i.lookup_path == path }
84
+ end
85
+
86
+ def find_parent(child)
87
+ parent_path = child.lookup_path.split("/").pop.join("/")
88
+ find_by_path(parent_path)
89
+ end
90
+
91
+ def ordered_entities
92
+ return @ordered_entities if @ordered_entities.present?
93
+ entities = []
94
+ as_tree.items.each do |item|
95
+ entities.append(item.is_a?(Collection) ? item.ordered_entities : item)
96
+ end
97
+ @ordered_entities ||= entities.flatten
98
+ end
99
+
100
+ def find_first
101
+ ordered_entities.first
102
+ end
103
+
104
+ def find_next(item)
105
+ index = ordered_entities.find_index { |i| i.lookup_path == item.lookup_path }
106
+ ordered_entities[index + 1] unless index.nil?
107
+ end
108
+
109
+ def find_previous(item)
110
+ index = ordered_entities.find_index { |i| i.lookup_path == item.lookup_path }
111
+ ordered_entities[index - 1] if !index.nil? && index > 0
112
+ end
113
+
114
+ def as_tree(filter_hidden: true)
115
+ return self if hierarchy_depth > 0
116
+ return @tree if @tree.present?
117
+ @tree = self.class.new
118
+ candidates = filter_hidden ? visible_items : items
119
+ candidates.each do |item|
120
+ current = @tree
121
+ if item.hierarchy_depth == 1
122
+ current.add(item)
123
+ else
124
+ item.parent_collections_names.each.with_index(1) do |name, i|
125
+ target = current.get_or_create(name)
126
+ if item.hierarchy_depth == i + 1
127
+ target.add(item)
128
+ else
129
+ current = target
130
+ end
131
+ end
132
+ end
133
+ end
134
+ @tree
45
135
  end
46
136
 
47
137
  def type
48
138
  :collection
49
139
  end
140
+
141
+ protected
142
+
143
+ def basename
144
+ @path.present? ? @path.split("/").last : ""
145
+ end
146
+
147
+ class << self
148
+ def describe_as
149
+ "items"
150
+ end
151
+ end
50
152
  end
51
153
  end
@@ -24,11 +24,13 @@ module Lookbook
24
24
  config.lookbook = ActiveSupport::OrderedOptions.new
25
25
  config.lookbook.listen_paths ||= []
26
26
  config.lookbook.preview_paths ||= []
27
+ config.lookbook.page_paths ||= ["test/components/docs"]
27
28
 
28
- initializer "view_component.set_configs" do |app|
29
- options = app.config.lookbook
30
- vc_options = app.config.view_component
29
+ initializer "view_component.set_configs" do
30
+ options = config.lookbook
31
+ vc_options = config.view_component
31
32
 
33
+ options.project_name ||= options.project_name == false ? nil : options.project_name || "Lookbook"
32
34
  options.auto_refresh = true if options.auto_refresh.nil?
33
35
  options.sort_examples = false if options.sort_examples.nil?
34
36
  options.debug = false unless options.debug == true
@@ -36,6 +38,13 @@ module Lookbook
36
38
  options.preview_paths = options.preview_paths.map(&:to_s)
37
39
  options.preview_paths += vc_options.preview_paths
38
40
 
41
+ options.page_paths = options.page_paths.map(&:to_s)
42
+ options.page_controller = "Lookbook::PageController" if options.page_controller.nil?
43
+ options.page_route ||= "pages"
44
+ options.page_options ||= {}.with_indifferent_access
45
+
46
+ options.markdown_options = Markdown::DEFAULT_OPTIONS.merge(options.markdown_options || {})
47
+
39
48
  options.preview_controller = vc_options.preview_controller if options.preview_controller.nil?
40
49
  options.preview_srcdoc = false if options.preview_srcdoc.nil?
41
50
  options.preview_display_params ||= {}.with_indifferent_access
@@ -43,16 +52,14 @@ module Lookbook
43
52
  options.listen_paths = options.listen_paths.map(&:to_s)
44
53
  options.listen_paths += options.preview_paths
45
54
  options.listen_paths << (vc_options.view_component_path || Rails.root.join("app/components"))
55
+ options.listen_paths.filter! { |path| Dir.exist? path }
46
56
 
47
- options.experimental_features = false unless options.experimental_features.present?
48
- end
57
+ options.cable = ActionCable::Server::Configuration.new
58
+ options.cable.cable = {adapter: "async"}.with_indifferent_access
59
+ options.cable.mount_path ||= "/lookbook-cable"
60
+ options.cable.connection_class = -> { Lookbook::Connection }
49
61
 
50
- initializer "lookbook.cable.config" do |app|
51
- if app.config.lookbook.auto_refresh
52
- Lookbook::Engine.cable.cable = {adapter: "async"}.with_indifferent_access
53
- Lookbook::Engine.cable.mount_path = "/cable"
54
- Lookbook::Engine.cable.connection_class = -> { Lookbook::Connection }
55
- end
62
+ options.experimental_features = false unless options.experimental_features.present?
56
63
  end
57
64
 
58
65
  initializer "lookbook.parser.tags" do
@@ -68,23 +75,25 @@ module Lookbook
68
75
 
69
76
  initializer "lookbook.logging" do
70
77
  if config.lookbook.debug == true
71
- Lookbook::Engine.cable.logger ||= Rails.logger
78
+ config.lookbook.cable.logger ||= Rails.logger
72
79
  else
73
- Lookbook::Engine.cable.logger = Lookbook::NullLogger.new
80
+ config.lookbook.cable.logger = Lookbook::NullLogger.new
74
81
  config.action_view.logger = Lookbook::NullLogger.new
75
82
  end
76
83
  end
77
84
 
78
- initializer "lookbook.preview.extend" do
79
- ActiveSupport.on_load(:view_component) do
80
- ViewComponent::Preview.extend Lookbook::Preview
81
- end
85
+ initializer "lookbook.helpers" do
86
+ config.action_controller.include_all_helpers = false
82
87
  end
83
88
 
84
- config.after_initialize do |app|
85
- @listener = Listen.to(*app.config.lookbook.listen_paths, only: /\.(rb|html.*)$/) do |modified, added, removed|
89
+ config.after_initialize do
90
+ Array(config.view_component.preview_paths).each do |preview_path|
91
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
92
+ end
93
+
94
+ @preview_listener = Listen.to(*config.lookbook.listen_paths, only: /\.(rb|html.*)$/) do |modified, added, removed|
86
95
  parser.parse
87
- if app.config.lookbook.auto_refresh
96
+ if Lookbook::Engine.websocket
88
97
  if (modified.any? || removed.any?) && added.none?
89
98
  Lookbook::Engine.websocket.broadcast("reload", {
90
99
  modified: modified,
@@ -93,32 +102,50 @@ module Lookbook
93
102
  end
94
103
  end
95
104
  end
96
- @listener.start
105
+ @preview_listener.start
106
+
107
+ if Lookbook::Features.enabled?(:pages)
108
+ @page_listener = Listen.to(*config.lookbook.page_paths.filter { |dir| Dir.exist? dir }, only: /\.(html.*|md.*)$/) do |modified, added, removed|
109
+ if Lookbook::Engine.websocket
110
+ if modified.any? || removed.any? || added.any?
111
+ Lookbook::Engine.websocket.broadcast("reload", {
112
+ modified: modified,
113
+ removed: removed,
114
+ added: added
115
+ })
116
+ end
117
+ end
118
+ end
119
+ @page_listener.start
120
+ end
121
+
97
122
  parser.parse
98
123
  end
99
124
 
100
125
  at_exit do
101
- @listener&.stop
126
+ @preview_listener&.stop
127
+ @page_listener&.stop
102
128
  end
103
129
 
104
130
  class << self
105
131
  def websocket
106
- if Rails.version.to_f >= 6.0
107
- @websocket ||= ActionCable::Server::Base.new(config: Lookbook::Engine.cable)
108
- else
109
- @websocket = ActionCable::Server::Base.new
110
- @websocket.config = Lookbook::Engine.cable
111
-
112
- @websocket
132
+ if config.lookbook.auto_refresh
133
+ @websocket ||= if Rails.version.to_f >= 6.0
134
+ ActionCable::Server::Base.new(config: config.lookbook.cable)
135
+ else
136
+ websocket ||= ActionCable::Server::Base.new
137
+ websocket.config = config.lookbook.cable
138
+ websocket
139
+ end
113
140
  end
114
141
  end
115
142
 
116
143
  def websocket_mount_path
117
- "#{Lookbook::Engine.routes.find_script_name({})}#{cable.mount_path}"
144
+ "#{mounted_path}#{config.lookbook.cable.mount_path}" if websocket
118
145
  end
119
146
 
120
- def cable
121
- @cable ||= ActionCable::Server::Configuration.new
147
+ def mounted_path
148
+ Lookbook::Engine.routes.find_script_name({})
122
149
  end
123
150
 
124
151
  def parser
@@ -1,6 +1,6 @@
1
1
  module Lookbook
2
2
  module Features
3
- EXPERIMENTAL_FEATURES = []
3
+ EXPERIMENTAL_FEATURES = [:pages]
4
4
 
5
5
  def self.experimental_feature?(name)
6
6
  EXPERIMENTAL_FEATURES.include?(name.to_sym)
@@ -0,0 +1,31 @@
1
+ require "redcarpet"
2
+
3
+ module Lookbook
4
+ class Markdown
5
+ DEFAULT_OPTIONS = {
6
+ tables: true,
7
+ fenced_code_blocks: true,
8
+ disable_indented_code_blocks: true,
9
+ strikethrough: true,
10
+ highlight: true,
11
+ with_toc_data: true
12
+ }
13
+
14
+ def self.render(text)
15
+ markdown = Redcarpet::Markdown.new(Renderer, Lookbook.config.markdown_options)
16
+ markdown.render(text).html_safe
17
+ end
18
+
19
+ class Renderer < Redcarpet::Render::HTML
20
+ def block_code(code, language = "ruby")
21
+ line_numbers = language.to_s.end_with? "-numbered"
22
+ ApplicationController.render "lookbook/components/_code", locals: {
23
+ code: code,
24
+ language: language.to_s.chomp("-numbered"),
25
+ line_numbers: line_numbers
26
+ },
27
+ layout: nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,146 @@
1
+ module Lookbook
2
+ class Page
3
+ include Utils
4
+
5
+ FRONTMATTER_FIELDS = [
6
+ :id,
7
+ :label,
8
+ :title,
9
+ :hidden,
10
+ :landing,
11
+ :position,
12
+ :markdown,
13
+ :header,
14
+ :footer,
15
+ :data
16
+ ]
17
+
18
+ def initialize(path, base_path)
19
+ @pathname = Pathname.new path
20
+ @base_path = base_path
21
+ end
22
+
23
+ def path
24
+ rel_path = @pathname.relative_path_from(@base_path)
25
+ (rel_path.dirname.to_s == "." ? name : "#{rel_path.dirname}/#{name}")
26
+ end
27
+
28
+ def lookup_path
29
+ @lookup_path ||= to_lookup_path(path)
30
+ end
31
+
32
+ def full_path
33
+ Rails.root.join(@pathname.to_s)
34
+ end
35
+
36
+ def name
37
+ remove_position_prefix(path_name)
38
+ end
39
+
40
+ def hidden?
41
+ options[:hidden] == true
42
+ end
43
+
44
+ def markdown?
45
+ options[:markdown] == true
46
+ end
47
+
48
+ def header?
49
+ options[:header] == true
50
+ end
51
+
52
+ def footer?
53
+ options[:footer] == true
54
+ end
55
+
56
+ def get(key)
57
+ options[key]
58
+ end
59
+
60
+ def content
61
+ @content ||= strip_frontmatter(file_contents)
62
+ end
63
+
64
+ def matchers
65
+ normalize_matchers(label)
66
+ end
67
+
68
+ def hierarchy_depth
69
+ path.split("/").size
70
+ end
71
+
72
+ def parent_collections_names
73
+ File.dirname(path).split("/")
74
+ end
75
+
76
+ def type
77
+ :page
78
+ end
79
+
80
+ def method_missing(method_name, *args, &block)
81
+ if args.none? && !block
82
+ options[method_name]
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ def respond_to_missing?(method_name, include_private = false)
89
+ FRONTMATTER_FIELDS.include? method_name
90
+ end
91
+
92
+ protected
93
+
94
+ def file_contents
95
+ File.read(full_path)
96
+ end
97
+
98
+ def options
99
+ return @options if @options
100
+ frontmatter = (get_frontmatter(file_contents) || {}).deep_symbolize_keys
101
+ options = Lookbook.config.page_options.deep_merge(frontmatter).with_indifferent_access
102
+ options[:id] = options[:id] ? generate_id(options[:id]) : generate_id(lookup_path)
103
+ options[:label] ||= name.titleize
104
+ options[:title] ||= options[:label]
105
+ options[:hidden] ||= false
106
+ options[:landing] ||= false
107
+ options[:position] = options[:position] ? options[:position].to_i : get_position_prefix(path_name)
108
+ options[:markdown] ||= markdown_file?
109
+ options[:header] = true unless options.key? :header
110
+ options[:footer] = true unless options.key? :footer
111
+ @options ||= options
112
+ end
113
+
114
+ def path_name
115
+ @pathname.basename(@pathname.extname).to_s.gsub(/\.(html|md)$/, "")
116
+ end
117
+
118
+ def markdown_file?
119
+ @pathname.basename(@pathname.extname).to_s.end_with?(".md")
120
+ end
121
+
122
+ class << self
123
+ def find(path)
124
+ all.find { |p| p.lookup_path == path }
125
+ end
126
+
127
+ def exists?(path)
128
+ !!find(path)
129
+ end
130
+
131
+ def all
132
+ pages = Array(page_paths).map do |dir|
133
+ Dir["#{dir}/**/*.html.*", "#{dir}/**/*.md.*"].sort.map do |page|
134
+ Lookbook::Page.new(page, dir)
135
+ end
136
+ end
137
+ sorted_pages = pages.flatten.uniq { |p| p.path }.sort_by { |page| [page.position, page.label] }
138
+ PageCollection.new(sorted_pages)
139
+ end
140
+
141
+ def page_paths
142
+ Lookbook.config.page_paths.filter { |dir| Dir.exist? dir }
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,11 @@
1
+ module Lookbook
2
+ class PageCollection < Collection
3
+ def id
4
+ generate_id("page-collection", lookup_path || "root")
5
+ end
6
+
7
+ def self.describe_as
8
+ "pages"
9
+ end
10
+ end
11
+ end
@@ -28,6 +28,8 @@ module Lookbook
28
28
  YARD::Tags::Library.define_tag("Hidden status", :hidden)
29
29
  YARD::Tags::Library.define_tag("Label", :label)
30
30
  YARD::Tags::Library.define_tag("Display", :display)
31
+ YARD::Tags::Library.define_tag("Position", :position)
32
+ YARD::Tags::Library.define_tag("ID", :id)
31
33
  end
32
34
  end
33
35
  end