trifle-docs 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b52d0fa673ad7999994caf2fda33169ac1815587cbf3023cce7a3a5ec65373a2
4
- data.tar.gz: 56c8c894a50aef901915dee9eab80a2f7b7e5a6fe3b46cbe344f4360f0c639b1
3
+ metadata.gz: 9f51e6c6b892eaca5b790d652ccb970146c9e98b624cf3353c84af3ffb95ed4c
4
+ data.tar.gz: f5058c9fa39c81a635db554fabffaab5535f57a76a078138319df0e72732cd49
5
5
  SHA512:
6
- metadata.gz: 59e6d106be4c79000a4470429308ce833ba8a6e0e3d02d8b1bdfacafd638030e14687b53aa5f5d81f55b91158b8873d9eb624bc0665d04d3bb781afc31927c8d
7
- data.tar.gz: d7ca56cd6809bd75891a5bf6b8fd49847007539dced62b5c69b43d9c779e00d963c3986fba84ff457d68edd52947cc5a554333584325ac56bb42e2f95d4e5579
6
+ metadata.gz: 48ce9ec16b882be46f90825f0e12bb13c783fa16e5b22f5eae27551418a5e58d112a321496fdf46810474c68de5f8933661931a696b6378bc836ffd7a8e74192
7
+ data.tar.gz: 9721a213e1f2cef167e0adf4ed18025b0740ba6594e26f6f0f326e4e6412631f5957447f503cd2ca45c4ce4e2df5efb497a15a8aaf2943a29b246a2c373869a8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trifle-docs (0.5.0)
4
+ trifle-docs (0.7.0)
5
5
  redcarpet
6
6
  rouge
7
7
  sinatra
@@ -14,7 +14,8 @@ GEM
14
14
  base64 (0.3.0)
15
15
  byebug (11.1.3)
16
16
  diff-lcs (1.5.0)
17
- mustermann (3.0.3)
17
+ logger (1.7.0)
18
+ mustermann (3.0.4)
18
19
  ruby2_keywords (~> 0.0.1)
19
20
  nio4r (2.7.4)
20
21
  parallel (1.22.1)
@@ -22,18 +23,20 @@ GEM
22
23
  ast (~> 2.4.1)
23
24
  puma (6.6.0)
24
25
  nio4r (~> 2.0)
25
- rack (3.1.16)
26
- rack-protection (4.0.0)
26
+ rack (3.2.4)
27
+ rack-protection (4.2.1)
27
28
  base64 (>= 0.1.0)
29
+ logger (>= 1.6.0)
28
30
  rack (>= 3.0.0, < 4)
29
- rack-session (2.0.0)
31
+ rack-session (2.1.1)
32
+ base64 (>= 0.1.0)
30
33
  rack (>= 3.0.0)
31
34
  rainbow (3.1.1)
32
35
  rake (13.0.6)
33
- redcarpet (3.6.0)
36
+ redcarpet (3.6.1)
34
37
  regexp_parser (2.5.0)
35
38
  rexml (3.2.5)
36
- rouge (4.2.0)
39
+ rouge (4.7.0)
37
40
  rspec (3.11.0)
38
41
  rspec-core (~> 3.11.0)
39
42
  rspec-expectations (~> 3.11.0)
@@ -60,15 +63,16 @@ GEM
60
63
  parser (>= 3.1.1.0)
61
64
  ruby-progressbar (1.11.0)
62
65
  ruby2_keywords (0.0.5)
63
- sinatra (4.0.0)
66
+ sinatra (4.2.1)
67
+ logger (>= 1.6.0)
64
68
  mustermann (~> 3.0)
65
69
  rack (>= 3.0.0, < 4)
66
- rack-protection (= 4.0.0)
70
+ rack-protection (= 4.2.1)
67
71
  rack-session (>= 2.0.0, < 3)
68
72
  tilt (~> 2.0)
69
- tilt (2.3.0)
73
+ tilt (2.7.0)
70
74
  unicode-display_width (2.1.0)
71
- yaml (0.3.0)
75
+ yaml (0.4.0)
72
76
 
73
77
  PLATFORMS
74
78
  arm64-darwin-21
@@ -25,32 +25,92 @@ module Trifle
25
25
  )
26
26
  end
27
27
 
28
+ get '/llms.txt' do
29
+ meta = Trifle::Docs.meta(url: 'llms.txt')
30
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
31
+
32
+ content = Trifle::Docs::Helper::Llms.homepage_markdown
33
+ halt(404, 'Not Found') if content.nil?
34
+
35
+ content_type 'text/markdown'
36
+ content
37
+ end
38
+
39
+ get '/llms-full.txt' do
40
+ meta = Trifle::Docs.meta(url: 'llms-full.txt')
41
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
42
+
43
+ content = Trifle::Docs::Helper::Llms.full_markdown
44
+ halt(404, 'Not Found') if content.nil? || content.strip.empty?
45
+
46
+ content_type 'text/markdown'
47
+ content
48
+ end
49
+
50
+ get '/sitemap.xml' do
51
+ meta = Trifle::Docs.meta(url: 'sitemap.xml')
52
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
53
+
54
+ render_generated_sitemap
55
+ end
56
+
28
57
  get '/*' do
58
+ handle_request(params, request)
59
+ end
60
+
61
+ def markdown_requested?(request, params)
62
+ return true if params['format'].to_s.downcase == 'md'
63
+
64
+ accept = request.env['HTTP_ACCEPT'].to_s
65
+ accept.include?('text/markdown')
66
+ end
67
+
68
+ def render_markdown?(meta, request, params)
69
+ return false if meta['type'] == 'file'
70
+
71
+ markdown_requested?(request, params) ||
72
+ Trifle::Docs::Helper::AiDetection.ai_scraper?(request.user_agent)
73
+ end
74
+
75
+ def handle_request(params, request)
29
76
  url = params['splat'].first.chomp('/')
30
77
  meta = Trifle::Docs.meta(url: url)
31
78
  halt(404, 'Not Found') if meta.nil?
32
79
 
33
- if Trifle::Docs::Helper::AiDetection.ai_scraper?(request.user_agent) && meta['type'] != 'file'
34
- content_type 'text/markdown'
35
- return Trifle::Docs::Helper::MarkdownLayout.render(
36
- meta: meta,
37
- raw_content: Trifle::Docs.raw_content(url: url),
38
- sitemap: Trifle::Docs.sitemap
39
- )
40
- end
41
-
42
- if meta['type'] == 'file'
43
- send_file meta['path']
44
- else
45
- erb (meta['template'] || 'page').to_sym, {}, {
46
- sitemap: Trifle::Docs.sitemap,
47
- collection: Trifle::Docs.collection(url: url),
48
- content: Trifle::Docs.content(url: url),
49
- meta: meta,
50
- url: url
51
- }
52
- end
80
+ return render_markdown(meta, url) if render_markdown?(meta, request, params)
81
+ return send_file(meta['path']) if meta['type'] == 'file'
82
+
83
+ render_html(meta, url)
53
84
  end
85
+
86
+ def render_markdown(meta, url)
87
+ content_type 'text/markdown'
88
+ Trifle::Docs::Helper::MarkdownLayout.render(
89
+ meta: meta,
90
+ raw_content: Trifle::Docs.raw_content(url: url),
91
+ sitemap: Trifle::Docs.sitemap
92
+ )
93
+ end
94
+
95
+ def render_html(meta, url)
96
+ erb (meta['template'] || 'page').to_sym, {}, {
97
+ sitemap: Trifle::Docs.sitemap,
98
+ collection: Trifle::Docs.collection(url: url),
99
+ content: Trifle::Docs.content(url: url),
100
+ meta: meta,
101
+ url: url
102
+ }
103
+ end
104
+
105
+ def render_generated_sitemap
106
+ content = Trifle::Docs::Helper::Sitemap.xml
107
+ halt(404, 'Not Found') if content.nil? || content.strip.empty?
108
+
109
+ content_type 'application/xml'
110
+ content
111
+ end
112
+
113
+ private :handle_request
54
114
  end
55
115
  end
56
116
  end
@@ -17,47 +17,37 @@ if Object.const_defined?('Rails')
17
17
  def self.draw
18
18
  Trifle::Docs::Engine.routes.draw do
19
19
  root to: 'page#show'
20
+ get 'llms.txt', to: 'page#llms'
21
+ get 'llms-full.txt', to: 'page#llms_full'
22
+ get 'sitemap.xml', to: 'page#sitemap'
20
23
  get 'search', to: 'page#search'
21
24
  get '*url', to: 'page#show'
22
25
  end
23
26
  end
24
27
  end
25
28
 
26
- class PageController < ActionController::Base
27
- layout :docs_layout
28
-
29
- def configuration
30
- params[:configuration] || Trifle::Docs.default
31
- end
32
-
33
- def docs_layout
34
- "layouts/trifle/docs/#{configuration.layout}"
35
- end
29
+ module PageControllerHelpers
30
+ private
36
31
 
37
- def show # rubocop:disable Metrics/AbcSize
38
- url = [params[:url], params[:format]].compact.join('.')
32
+ def render_llms(url, allow_empty: false)
39
33
  meta = Trifle::Docs.meta(url: url, config: configuration)
40
- render_not_found and return if meta.nil?
34
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
41
35
 
42
- if Trifle::Docs::Helper::AiDetection.ai_scraper?(request.user_agent) && meta['type'] != 'file'
43
- render_markdown(url: url, meta: meta)
44
- return
45
- end
46
- render_file(meta: meta) and return if meta['type'] == 'file'
36
+ content = yield
37
+ return render_not_found if content.nil?
38
+ return render_not_found if !allow_empty && content.strip.empty?
47
39
 
48
- render_content(url: url, meta: meta)
40
+ render plain: content, content_type: 'text/markdown'
49
41
  end
50
42
 
51
- def search
52
- results = Trifle::Docs.search(query: params[:query], scope: params[:scope])
43
+ def render_sitemap(url)
44
+ meta = Trifle::Docs.meta(url: url, config: configuration)
45
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
53
46
 
54
- render 'search', locals: {
55
- results: results,
56
- query: params[:query],
57
- scope: params[:scope],
58
- sitemap: Trifle::Docs.sitemap,
59
- meta: { description: 'Search' }
60
- }
47
+ content = yield
48
+ return render_not_found if content.nil? || content.strip.empty?
49
+
50
+ render plain: content, content_type: 'application/xml'
61
51
  end
62
52
 
63
53
  def render_not_found
@@ -78,13 +68,100 @@ if Object.const_defined?('Rails')
78
68
  }
79
69
  end
80
70
 
81
- def render_markdown
71
+ def render_markdown(url:, meta:)
82
72
  render plain: Trifle::Docs::Helper::MarkdownLayout.render(
83
73
  meta: meta,
84
74
  raw_content: Trifle::Docs.raw_content(url: url, config: configuration),
85
75
  sitemap: Trifle::Docs.sitemap(config: configuration)
86
76
  ), content_type: 'text/markdown'
87
77
  end
78
+
79
+ def fetch_meta(url)
80
+ Trifle::Docs.meta(url: url, config: configuration)
81
+ end
82
+
83
+ def render_for_meta(meta, url, wants_md, request)
84
+ return render_markdown(url: url, meta: meta) if render_markdown?(meta, wants_md, request)
85
+ return render_file(meta: meta) if file_meta?(meta)
86
+
87
+ render_content(url: url, meta: meta)
88
+ end
89
+
90
+ def file_meta?(meta)
91
+ meta['type'] == 'file'
92
+ end
93
+
94
+ def resolve_url(params)
95
+ raw_url = params[:url]
96
+ format = params[:format]
97
+ wants_md = markdown_requested?(format, request)
98
+ url = wants_md ? raw_url : [raw_url, format].compact.join('.')
99
+ [url, wants_md]
100
+ end
101
+
102
+ def render_markdown?(meta, wants_md, request)
103
+ return false if meta['type'] == 'file'
104
+
105
+ wants_md || Trifle::Docs::Helper::AiDetection.ai_scraper?(request.user_agent)
106
+ end
107
+
108
+ def markdown_requested?(format, request)
109
+ return true if format.to_s.downcase == 'md'
110
+
111
+ request.headers['Accept'].to_s.include?('text/markdown')
112
+ end
113
+ end
114
+
115
+ class PageController < ActionController::Base
116
+ include PageControllerHelpers
117
+
118
+ layout :docs_layout
119
+
120
+ def configuration
121
+ params[:configuration] || Trifle::Docs.default
122
+ end
123
+
124
+ def docs_layout
125
+ "layouts/trifle/docs/#{configuration.layout}"
126
+ end
127
+
128
+ def show
129
+ url, wants_md = resolve_url(params)
130
+ meta = fetch_meta(url)
131
+ return render_not_found if meta.nil?
132
+
133
+ render_for_meta(meta, url, wants_md, request)
134
+ end
135
+
136
+ def search
137
+ results = Trifle::Docs.search(query: params[:query], scope: params[:scope])
138
+
139
+ render 'search', locals: {
140
+ results: results,
141
+ query: params[:query],
142
+ scope: params[:scope],
143
+ sitemap: Trifle::Docs.sitemap,
144
+ meta: { description: 'Search' }
145
+ }
146
+ end
147
+
148
+ def llms
149
+ render_llms('llms.txt', allow_empty: true) do
150
+ Trifle::Docs::Helper::Llms.homepage_markdown(config: configuration)
151
+ end
152
+ end
153
+
154
+ def llms_full
155
+ render_llms('llms-full.txt') do
156
+ Trifle::Docs::Helper::Llms.full_markdown(config: configuration)
157
+ end
158
+ end
159
+
160
+ def sitemap
161
+ render_sitemap('sitemap.xml') do
162
+ Trifle::Docs::Helper::Sitemap.xml(config: configuration)
163
+ end
164
+ end
88
165
  end
89
166
  end
90
167
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trifle
4
+ module Docs
5
+ module Helper
6
+ module Llms
7
+ module_function
8
+
9
+ def homepage_markdown(config: nil)
10
+ meta = Trifle::Docs.meta(url: '', config: config)
11
+ return nil if meta.nil?
12
+
13
+ Trifle::Docs::Helper::MarkdownLayout.render(
14
+ meta: meta,
15
+ raw_content: Trifle::Docs.raw_content(url: '', config: config),
16
+ sitemap: Trifle::Docs.sitemap(config: config)
17
+ )
18
+ end
19
+
20
+ def full_markdown(config: nil)
21
+ sitemap = Trifle::Docs.sitemap(config: config)
22
+ pages = flatten_sitemap(sitemap)
23
+
24
+ chunks = pages.filter_map do |page|
25
+ meta = page[:meta]
26
+ next if meta.nil? || meta['type'] == 'file'
27
+
28
+ raw = Trifle::Docs.raw_content(url: page[:url], config: config)
29
+ next if raw.nil? || raw.strip.empty?
30
+
31
+ format_page(meta: meta, url: page[:url], raw_content: raw)
32
+ end
33
+
34
+ chunks.join("\n\n")
35
+ end
36
+
37
+ def flatten_sitemap(node, path = [])
38
+ return [] unless node.is_a?(Hash)
39
+
40
+ entries = []
41
+ meta = node['_meta']
42
+ entries << { url: path.join('/'), meta: meta } if meta
43
+
44
+ node.keys.reject { |key| key == '_meta' }.sort.each do |key|
45
+ entries.concat(flatten_sitemap(node[key], path + [key]))
46
+ end
47
+
48
+ entries
49
+ end
50
+
51
+ def format_page(meta:, url:, raw_content:)
52
+ lines = page_header(meta: meta, url: url)
53
+ lines << raw_content.to_s.strip
54
+ lines << ''
55
+ lines << '---'
56
+
57
+ lines.join("\n")
58
+ end
59
+
60
+ def page_header(meta:, url:)
61
+ title = meta['title'] || Trifle::Docs::Helper::MarkdownLayout.derive_title_from_url(url)
62
+ page_url = meta['url'] || "/#{url}"
63
+
64
+ [
65
+ "# #{title}",
66
+ '',
67
+ "Source: #{page_url}",
68
+ ''
69
+ ]
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'time'
5
+
6
+ module Trifle
7
+ module Docs
8
+ module Helper
9
+ module Sitemap
10
+ module_function
11
+
12
+ def xml(config: nil)
13
+ sitemap = Trifle::Docs.sitemap(config: config)
14
+ urls = sitemap_urls(sitemap)
15
+ return nil if urls.empty?
16
+
17
+ build_document(urls)
18
+ end
19
+
20
+ def flatten_sitemap(node, path = [])
21
+ return [] unless node.is_a?(Hash)
22
+
23
+ entries = []
24
+ meta = node['_meta']
25
+ entries << { url: path.join('/'), meta: meta } if meta
26
+
27
+ node.keys.reject { |key| key == '_meta' }.sort.each do |key|
28
+ entries.concat(flatten_sitemap(node[key], path + [key]))
29
+ end
30
+
31
+ entries
32
+ end
33
+
34
+ def sitemap_urls(sitemap)
35
+ entries = flatten_sitemap(sitemap)
36
+ entries.filter_map do |entry|
37
+ meta = entry[:meta]
38
+ next if meta.nil? || meta['type'] == 'file'
39
+
40
+ build_url_entry(entry[:url], meta)
41
+ end
42
+ end
43
+
44
+ def build_document(urls)
45
+ [
46
+ %(<?xml version="1.0" encoding="UTF-8"?>),
47
+ %(<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">),
48
+ urls.join("\n"),
49
+ %(</urlset>)
50
+ ].join("\n")
51
+ end
52
+
53
+ def build_url_entry(url, meta)
54
+ loc = normalize_loc(url, meta)
55
+ lastmod = format_lastmod(meta['updated_at'])
56
+ lastmod_tag = lastmod ? "<lastmod>#{lastmod}</lastmod>" : ''
57
+
58
+ "<url><loc>#{loc}</loc>#{lastmod_tag}</url>"
59
+ end
60
+
61
+ def normalize_loc(url, meta)
62
+ loc = meta['url']
63
+ loc = "/#{url}" if loc.nil? || loc.empty?
64
+ loc = '/' if loc == '//'
65
+ CGI.escapeHTML(loc)
66
+ end
67
+
68
+ def format_lastmod(value)
69
+ return nil if value.nil?
70
+
71
+ return value.utc.iso8601 if value.respond_to?(:utc) && value.respond_to?(:iso8601)
72
+ return value.iso8601 if value.respond_to?(:iso8601)
73
+
74
+ nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Trifle
4
4
  module Docs
5
- VERSION = '0.5.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
data/lib/trifle/docs.rb CHANGED
@@ -4,6 +4,8 @@ require_relative 'docs/configuration'
4
4
  require_relative 'docs/helper/tree'
5
5
  require_relative 'docs/helper/markdown_layout'
6
6
  require_relative 'docs/helper/ai_detection'
7
+ require_relative 'docs/helper/llms'
8
+ require_relative 'docs/helper/sitemap'
7
9
  require_relative 'docs/harvester'
8
10
  require_relative 'docs/harvester/file'
9
11
  require_relative 'docs/harvester/markdown'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trifle-docs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jozef Vaclavik
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-11 00:00:00.000000000 Z
11
+ date: 2026-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -201,7 +201,9 @@ files:
201
201
  - lib/trifle/docs/harvester/file.rb
202
202
  - lib/trifle/docs/harvester/markdown.rb
203
203
  - lib/trifle/docs/helper/ai_detection.rb
204
+ - lib/trifle/docs/helper/llms.rb
204
205
  - lib/trifle/docs/helper/markdown_layout.rb
206
+ - lib/trifle/docs/helper/sitemap.rb
205
207
  - lib/trifle/docs/helper/tree.rb
206
208
  - lib/trifle/docs/operations/collection.rb
207
209
  - lib/trifle/docs/operations/content.rb