pikuri-core 0.0.7 → 0.1.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 +4 -4
- data/README.md +1 -1
- data/lib/pikuri/agent/chat_transport.rb +73 -93
- data/lib/pikuri/agent/configurator.rb +46 -106
- data/lib/pikuri/agent/context_window_detector.rb +44 -85
- data/lib/pikuri/agent/control/cancellable.rb +87 -66
- data/lib/pikuri/agent/control/interloper.rb +127 -105
- data/lib/pikuri/agent/control/step_limit.rb +25 -41
- data/lib/pikuri/agent/control.rb +14 -34
- data/lib/pikuri/agent/event.rb +123 -188
- data/lib/pikuri/agent/extension.rb +118 -94
- data/lib/pikuri/agent/extension_context.rb +50 -77
- data/lib/pikuri/agent/history.rb +653 -0
- data/lib/pikuri/agent/listener/rate_limited.rb +40 -66
- data/lib/pikuri/agent/listener/terminal.rb +143 -117
- data/lib/pikuri/agent/listener/token_log.rb +101 -140
- data/lib/pikuri/agent/listener.rb +23 -43
- data/lib/pikuri/agent/listener_list.rb +26 -47
- data/lib/pikuri/agent/synthesizer.rb +45 -87
- data/lib/pikuri/agent.rb +816 -474
- data/lib/pikuri/bundler_env.rb +68 -0
- data/lib/pikuri/extractor/html.rb +63 -110
- data/lib/pikuri/extractor/passthrough.rb +20 -30
- data/lib/pikuri/extractor.rb +93 -154
- data/lib/pikuri/file_type.rb +63 -135
- data/lib/pikuri/finalizers.rb +32 -47
- data/lib/pikuri/paths.rb +104 -13
- data/lib/pikuri/ruby_llm_patches.rb +106 -0
- data/lib/pikuri/sanitizer.rb +45 -67
- data/lib/pikuri/subprocess.rb +75 -119
- data/lib/pikuri/testing.rb +296 -0
- data/lib/pikuri/tool/calculator.rb +56 -66
- data/lib/pikuri/tool/execute_context.rb +42 -0
- data/lib/pikuri/tool/fetch.rb +51 -77
- data/lib/pikuri/tool/parameters.rb +21 -29
- data/lib/pikuri/tool/scraper.rb +55 -97
- data/lib/pikuri/tool/search/brave.rb +52 -80
- data/lib/pikuri/tool/search/duckduckgo.rb +59 -91
- data/lib/pikuri/tool/search/engines.rb +230 -97
- data/lib/pikuri/tool/search/exa.rb +56 -90
- data/lib/pikuri/tool/search/rate_limiter.rb +61 -38
- data/lib/pikuri/tool/search/result.rb +10 -15
- data/lib/pikuri/tool/trifecta_legs.rb +217 -0
- data/lib/pikuri/tool/web_scrape.rb +38 -54
- data/lib/pikuri/tool/web_search.rb +100 -24
- data/lib/pikuri/tool.rb +140 -65
- data/lib/pikuri/trifecta/contribution.rb +43 -0
- data/lib/pikuri/trifecta/node.rb +47 -0
- data/lib/pikuri/trifecta/report.rb +230 -0
- data/lib/pikuri/trifecta.rb +127 -0
- data/lib/pikuri/url_cache.rb +33 -49
- data/lib/pikuri/version.rb +1 -1
- data/lib/pikuri-core.rb +72 -88
- data/prompts/agent-loop.txt +5 -0
- data/prompts/pikuri-chat.txt +3 -12
- metadata +14 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pikuri
|
|
4
|
+
# The environment a child process should see when pikuri itself booted under
|
|
5
|
+
# Bundler. Merge it under whatever the child's own configuration asks for:
|
|
6
|
+
#
|
|
7
|
+
# Open3.popen3(BundlerEnv.clean_delta.merge(entry.env), *entry.command)
|
|
8
|
+
#
|
|
9
|
+
# Bundler implements +bundler/setup+ by rewriting the process environment —
|
|
10
|
+
# it injects +BUNDLE_GEMFILE+ and +RUBYOPT+, repoints +GEM_HOME+ /
|
|
11
|
+
# +GEM_PATH+, prepends the bundle's bin dir to +PATH+, and stashes each
|
|
12
|
+
# original under +BUNDLER_ORIG_<NAME>+. A child inherits all of it, and for a
|
|
13
|
+
# child that is itself Ruby that reads as the child being broken: a
|
|
14
|
+
# +bundle exec+ the agent runs in another project resolves against *pikuri's*
|
|
15
|
+
# Gemfile, and +ruby-lsp+ refuses to start at all ("can't find executable
|
|
16
|
+
# ruby-lsp for gem ruby-lsp") unless pikuri's Gemfile happens to list it. Such
|
|
17
|
+
# a child serves the *target project*, so it must see the environment the user
|
|
18
|
+
# had before pikuri started.
|
|
19
|
+
#
|
|
20
|
+
# == Implementation details
|
|
21
|
+
#
|
|
22
|
+
# A delta rather than a scrub, for two reasons. A spawn's +env:+ is additive,
|
|
23
|
+
# so removal has to be spelled as an explicit +nil+; and Bundler *changed*
|
|
24
|
+
# variables it did not invent, so the fix restores each +BUNDLER_ORIG_*+
|
|
25
|
+
# backup — a host-level +GEM_HOME+ from an apt-installed Ruby passes through,
|
|
26
|
+
# where a blanket +GEM_*+ wipe would have eaten it.
|
|
27
|
+
#
|
|
28
|
+
# This does not break the "environment is not a secret store" seam, which
|
|
29
|
+
# says pikuri never filters +ENV+ before spawning: nothing the user set is
|
|
30
|
+
# removed, only pikuri's own boot undone.
|
|
31
|
+
module BundlerEnv
|
|
32
|
+
# The delta that, merged onto the current (bundler-polluted) +ENV+,
|
|
33
|
+
# reconstructs +Bundler.unbundled_env+.
|
|
34
|
+
#
|
|
35
|
+
# Empty when pikuri runs without Bundler (gem-installed, or a host that
|
|
36
|
+
# never required +bundler/setup+), so the child inherits +ENV+ unchanged
|
|
37
|
+
# and no caller needs a branch of its own.
|
|
38
|
+
#
|
|
39
|
+
# @return [Hash{String=>(String,nil)}] an +env:+ for
|
|
40
|
+
# {Pikuri::Subprocess.spawn}, +Open3.popen3+ or +Process.spawn+; a +nil+
|
|
41
|
+
# value unsets that variable in the child.
|
|
42
|
+
def self.clean_delta
|
|
43
|
+
return {} unless defined?(Bundler) && Bundler.respond_to?(:unbundled_env)
|
|
44
|
+
|
|
45
|
+
env_delta(current: ENV.to_h, clean: Bundler.unbundled_env)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The additive delta that turns +current+ into +clean+ when passed as a
|
|
49
|
+
# spawn's +env:+.
|
|
50
|
+
#
|
|
51
|
+
# env_delta(current: { 'BUNDLE_GEMFILE' => '/pikuri/Gemfile', 'PATH' => '/bin' },
|
|
52
|
+
# clean: { 'PATH' => '/bin' })
|
|
53
|
+
# # => { 'BUNDLE_GEMFILE' => nil }
|
|
54
|
+
#
|
|
55
|
+
# Keys equal in both are omitted, so a child's inherited environment is
|
|
56
|
+
# otherwise untouched.
|
|
57
|
+
#
|
|
58
|
+
# @param current [Hash{String=>String}]
|
|
59
|
+
# @param clean [Hash{String=>String}]
|
|
60
|
+
# @return [Hash{String=>(String,nil)}] +nil+ where +clean+ lacks the key.
|
|
61
|
+
def self.env_delta(current:, clean:)
|
|
62
|
+
delta = {}
|
|
63
|
+
clean.each { |k, v| delta[k] = v unless current[k] == v }
|
|
64
|
+
current.each_key { |k| delta[k] = nil unless clean.key?(k) }
|
|
65
|
+
delta
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -9,42 +9,32 @@ module Pikuri
|
|
|
9
9
|
module Extractor
|
|
10
10
|
# HTML → Markdown extractor.
|
|
11
11
|
#
|
|
12
|
-
# Matched by content-type only (+text/html+ /
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# source. Local HTML stays on the {Passthrough} arm until a
|
|
18
|
-
# consumer genuinely needs otherwise.
|
|
12
|
+
# Matched by content-type only (+text/html+ / +application/xhtml+xml+) —
|
|
13
|
+
# no byte sniff: the web path always has the header, and sniffing would
|
|
14
|
+
# route a +Workspace::Read+ of a local +.html+ *source* file through
|
|
15
|
+
# readability when the developer wants the source. Local HTML stays on the
|
|
16
|
+
# {Passthrough} arm.
|
|
19
17
|
#
|
|
20
|
-
#
|
|
18
|
+
# Renders both views when available, concatenated with a horizontal rule,
|
|
19
|
+
# so the LLM gets structured metadata and rendered body and picks whichever
|
|
20
|
+
# fits:
|
|
21
21
|
#
|
|
22
|
-
# 1. JSON-LD
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# +reverse_markdown+, with a +<main>+/+<article>+ fallback for
|
|
29
|
-
# pages whose content sits mostly outside +<p>+ tags.
|
|
22
|
+
# 1. JSON-LD — the first +<script type="application/ld+json">+ node whose
|
|
23
|
+
# +@type+ is a substantive schema.org type ({INTERESTING_TYPES}),
|
|
24
|
+
# rendered as title + metadata bullets + body copy.
|
|
25
|
+
# 2. Readability — +Readability+ + +reverse_markdown+, with a
|
|
26
|
+
# +<main>+/+<article>+ fallback for pages whose content sits outside
|
|
27
|
+
# +<p>+ tags.
|
|
30
28
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# is more useful for the task. Trades some duplication (when a
|
|
34
|
-
# publisher embeds the article body in JSON-LD AND in HTML) for
|
|
35
|
-
# fewer type-based heuristics on which branch should win — the
|
|
36
|
-
# earlier "is this Article's +description+ a teaser or the real
|
|
37
|
-
# body?" carve-out is no longer needed because both end up in
|
|
38
|
-
# the output regardless.
|
|
29
|
+
# Emitting both trades some duplication (a body embedded in JSON-LD *and*
|
|
30
|
+
# HTML) for dropping type-based "teaser or real body?" heuristics.
|
|
39
31
|
module HTML
|
|
40
32
|
# @return [Array<String>] content-types this extractor claims.
|
|
41
33
|
CONTENT_TYPES = %w[text/html application/xhtml+xml].freeze
|
|
42
34
|
|
|
43
|
-
# @return [Array<String>] schema.org +@type+ values
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# node wins. Skips noise nodes (Organization, BreadcrumbList,
|
|
47
|
-
# WebSite, ...) that ship on most pages but carry no page
|
|
35
|
+
# @return [Array<String>] schema.org +@type+ values treated as the page's
|
|
36
|
+
# primary entity when picking a JSON-LD node (first match wins). Skips
|
|
37
|
+
# noise nodes (Organization, BreadcrumbList, WebSite) that carry no
|
|
48
38
|
# content.
|
|
49
39
|
INTERESTING_TYPES = %w[
|
|
50
40
|
Product Article NewsArticle BlogPosting Recipe Event Book Movie
|
|
@@ -63,20 +53,16 @@ module Pikuri
|
|
|
63
53
|
# is dropped before Markdown conversion
|
|
64
54
|
READABILITY_ATTRS = %w[href src alt title].freeze
|
|
65
55
|
|
|
66
|
-
# @return [Float] minimum +<main>+/+<article
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# ~5x) but high enough that pages where both produce
|
|
72
|
-
# comparable output keep Readability's noise filtering.
|
|
56
|
+
# @return [Float] minimum +<main>+/+<article>+-to-Readability text-length
|
|
57
|
+
# ratio that triggers the semantic-container fallback in
|
|
58
|
+
# {.readability_to_markdown}. Low enough to catch Readability collapsing
|
|
59
|
+
# a div/list-based page (vaadin.com/company, ~5x), high enough to keep
|
|
60
|
+
# its noise filtering where both outputs are comparable.
|
|
73
61
|
MAIN_FALLBACK_RATIO = 2.0
|
|
74
62
|
|
|
75
|
-
# @return [Integer] minimum text length the
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# ratio comparison is dominated by noise and we'd swap on
|
|
79
|
-
# tiny pages where Readability is doing the right thing.
|
|
63
|
+
# @return [Integer] minimum text length the +<main>+/+<article>+ container
|
|
64
|
+
# must hold before the {.readability_to_markdown} fallback can fire —
|
|
65
|
+
# below it the ratio is noise-dominated and would swap on tiny pages.
|
|
80
66
|
MAIN_FALLBACK_MIN_CHARS = 500
|
|
81
67
|
|
|
82
68
|
# @return [Symbol] {Page#kind} tag.
|
|
@@ -92,12 +78,9 @@ module Pikuri
|
|
|
92
78
|
CONTENT_TYPES.include?(content_type)
|
|
93
79
|
end
|
|
94
80
|
|
|
95
|
-
# Render the HTML
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
# rule. Either section may be missing — pages with no JSON-LD
|
|
99
|
-
# return only the readability output, and a malformed page with
|
|
100
|
-
# no extractable body returns only the JSON-LD render.
|
|
81
|
+
# Render the HTML behind +io+ as Markdown: the JSON-LD section (when an
|
|
82
|
+
# interesting node is present) and the readability/+<main>+ section,
|
|
83
|
+
# joined by a horizontal rule. Either may be missing.
|
|
101
84
|
#
|
|
102
85
|
# @param io [IO, StringIO] IO over the HTML document.
|
|
103
86
|
# @return [String] Markdown representation
|
|
@@ -108,22 +91,13 @@ module Pikuri
|
|
|
108
91
|
sections.join("\n\n---\n\n")
|
|
109
92
|
end
|
|
110
93
|
|
|
111
|
-
# Pick the first JSON-LD node whose +@type+
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# the
|
|
115
|
-
#
|
|
116
|
-
# No content-field gating: a node carrying just +name+/+author+/
|
|
117
|
-
# +datePublished+ still renders (as a metadata-only header),
|
|
118
|
-
# because the readability pass independently produces the page
|
|
119
|
-
# body. That is the trade-off that lets us drop the type-based
|
|
120
|
-
# "is this teaser or article copy?" heuristics — duplication is
|
|
121
|
-
# acceptable when both views are available, and the LLM can
|
|
122
|
-
# pick whichever it needs.
|
|
94
|
+
# Pick the first JSON-LD node whose +@type+ is in {INTERESTING_TYPES} and
|
|
95
|
+
# render it. +nil+ when none matches. No content-field gating — a
|
|
96
|
+
# metadata-only node (name/author/date) still renders, since the
|
|
97
|
+
# readability pass independently produces the body.
|
|
123
98
|
#
|
|
124
99
|
# @param html [String] HTML document body
|
|
125
|
-
# @return [String, nil] Markdown render
|
|
126
|
-
# node, or +nil+ when nothing matched
|
|
100
|
+
# @return [String, nil] Markdown render, or +nil+ when nothing matched
|
|
127
101
|
def self.jsonld_section(html)
|
|
128
102
|
node = parse_jsonld(html).find do |n|
|
|
129
103
|
Array(n['@type']).any? { |t| INTERESTING_TYPES.include?(t) }
|
|
@@ -131,14 +105,12 @@ module Pikuri
|
|
|
131
105
|
node ? jsonld_to_markdown(node) : nil
|
|
132
106
|
end
|
|
133
107
|
|
|
134
|
-
# Collect every JSON-LD payload
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
# frequently ship broken JSON-LD and we only need at least one
|
|
138
|
-
# parseable block.
|
|
108
|
+
# Collect every JSON-LD payload in +html+, flattening +@graph+ wrappers
|
|
109
|
+
# into one flat array. Malformed JSON blocks are skipped (sites ship
|
|
110
|
+
# broken JSON-LD; one parseable block suffices).
|
|
139
111
|
#
|
|
140
112
|
# @param html [String] HTML document body
|
|
141
|
-
# @return [Array<Hash>] parsed
|
|
113
|
+
# @return [Array<Hash>] parsed nodes; possibly empty
|
|
142
114
|
def self.parse_jsonld(html)
|
|
143
115
|
doc = Nokogiri::HTML(html)
|
|
144
116
|
blobs = doc.css('script[type="application/ld+json"]').map(&:text)
|
|
@@ -156,18 +128,12 @@ module Pikuri
|
|
|
156
128
|
end
|
|
157
129
|
end
|
|
158
130
|
|
|
159
|
-
# Render a
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
# When the node carries +articleBody+ (the full publisher-supplied
|
|
165
|
-
# article text), that wins over +description+ — the description
|
|
166
|
-
# is typically a lede teaser and would just repeat the article's
|
|
167
|
-
# opening lines.
|
|
131
|
+
# Render a JSON-LD +node+ as Markdown: title from +name+/+headline+, a
|
|
132
|
+
# bullet list of common fields (brand, SKU, price, rating, author, date),
|
|
133
|
+
# body copy, and the lead image. +articleBody+ wins over +description+
|
|
134
|
+
# (the description is usually a lede teaser).
|
|
168
135
|
#
|
|
169
|
-
# @param node [Hash] JSON-LD node
|
|
170
|
-
# {.jsonld_section}
|
|
136
|
+
# @param node [Hash] JSON-LD node
|
|
171
137
|
# @return [String] Markdown representation
|
|
172
138
|
def self.jsonld_to_markdown(node)
|
|
173
139
|
out = +''
|
|
@@ -212,19 +178,13 @@ module Pikuri
|
|
|
212
178
|
out
|
|
213
179
|
end
|
|
214
180
|
|
|
215
|
-
# Run +Readability+ over +html
|
|
216
|
-
#
|
|
217
|
-
# +<
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
# extraction to a sliver of the page. In that case we render the
|
|
223
|
-
# +<main>+/+<article>+ container directly. The fallback only
|
|
224
|
-
# fires when the container holds substantially more text than
|
|
225
|
-
# Readability picked up (see {MAIN_FALLBACK_RATIO} /
|
|
226
|
-
# {MAIN_FALLBACK_MIN_CHARS}); on pages where both agree we keep
|
|
227
|
-
# Readability so its noise filtering still strips nav/ads/etc.
|
|
181
|
+
# Run +Readability+ over +html+, convert to Markdown via
|
|
182
|
+
# +reverse_markdown+, +<title>+ as a top-level heading. When the page
|
|
183
|
+
# uses HTML5 semantics (+<main>+/+<article>+) but leaves content outside
|
|
184
|
+
# +<p>+ tags, Readability's paragraph-density scoring collapses the
|
|
185
|
+
# extraction; then the container is rendered directly. The fallback fires
|
|
186
|
+
# only when the container holds substantially more text (see
|
|
187
|
+
# {MAIN_FALLBACK_RATIO} / {MAIN_FALLBACK_MIN_CHARS}).
|
|
228
188
|
#
|
|
229
189
|
# @param html [String] HTML document body
|
|
230
190
|
# @return [String] Markdown representation
|
|
@@ -247,20 +207,14 @@ module Pikuri
|
|
|
247
207
|
out
|
|
248
208
|
end
|
|
249
209
|
|
|
250
|
-
#
|
|
251
|
-
# substantially more text than Readability extracted,
|
|
252
|
-
# container
|
|
253
|
-
# +nil+ when the fallback should not fire — when there is no
|
|
254
|
-
# semantic container, when it's too small to be meaningful, or
|
|
255
|
-
# when Readability's output is already comparable.
|
|
210
|
+
# Return the +<main>+/+<article>+ container's HTML when it holds
|
|
211
|
+
# substantially more text than Readability extracted, else +nil+ (no
|
|
212
|
+
# container, too small, or Readability comparable).
|
|
256
213
|
#
|
|
257
|
-
# @param html [String] full HTML document body
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
# baseline
|
|
262
|
-
# @return [String, nil] container HTML when the fallback should
|
|
263
|
-
# fire, +nil+ otherwise
|
|
214
|
+
# @param html [String] full HTML document body
|
|
215
|
+
# @param readability_html [String] +Readability::Document#content+ output,
|
|
216
|
+
# the comparison baseline
|
|
217
|
+
# @return [String, nil] container HTML when the fallback should fire
|
|
264
218
|
def self.main_fallback_html(html, readability_html)
|
|
265
219
|
doc = Nokogiri::HTML(html)
|
|
266
220
|
container = doc.at_css('main') || doc.at_css('article')
|
|
@@ -276,20 +230,19 @@ module Pikuri
|
|
|
276
230
|
end
|
|
277
231
|
private_class_method :main_fallback_html
|
|
278
232
|
|
|
279
|
-
# JSON-LD
|
|
280
|
-
#
|
|
281
|
-
# callers can +.dig+ safely.
|
|
233
|
+
# Normalize a JSON-LD field (string, hash, or array of either) to a single
|
|
234
|
+
# hash (first if a list) so callers can +.dig+ safely.
|
|
282
235
|
#
|
|
283
236
|
# @param value [Object] raw JSON-LD field value
|
|
284
|
-
# @return [Hash] empty hash when +value+
|
|
237
|
+
# @return [Hash] empty hash when +value+ contains no hash
|
|
285
238
|
def self.first_obj(value)
|
|
286
239
|
value = value.first if value.is_a?(Array)
|
|
287
240
|
value.is_a?(Hash) ? value : {}
|
|
288
241
|
end
|
|
289
242
|
private_class_method :first_obj
|
|
290
243
|
|
|
291
|
-
#
|
|
292
|
-
#
|
|
244
|
+
# Like {.first_obj} but preserves a bare string (e.g. +brand: "Apple"+)
|
|
245
|
+
# instead of replacing it with +{}+.
|
|
293
246
|
#
|
|
294
247
|
# @param value [Object] raw JSON-LD field value
|
|
295
248
|
# @return [String, Hash, nil]
|
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Pikuri
|
|
4
4
|
module Extractor
|
|
5
|
-
# The terminal plain-text arm of the registry: content that *is*
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# deal with, matching what +File.read+ with a UTF-8 encoding does).
|
|
9
|
-
# Markdown, source files, JSON, robots.txt all land here.
|
|
5
|
+
# The terminal plain-text arm of the registry: content that *is* already
|
|
6
|
+
# text passes through verbatim (forced to UTF-8, invalid bytes left in, as
|
|
7
|
+
# +File.read+ does). Markdown, source files, JSON, robots.txt land here.
|
|
10
8
|
#
|
|
11
|
-
# Matching
|
|
12
|
-
# content-type:
|
|
9
|
+
# Matching splits on whether the transport gave a content-type:
|
|
13
10
|
#
|
|
14
|
-
# * With
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# returned +nil+ for "unrecognised"): claim anything that passes
|
|
21
|
-
# the {FileType.binary?} heuristic on the sample. Opaque
|
|
22
|
-
# binaries stay unclaimed and surface as {Unsupported}.
|
|
11
|
+
# * With one (the web path): claim +text/*+ only — a non-text type no
|
|
12
|
+
# earlier extractor claimed is *not* second-guessed by sniffing
|
|
13
|
+
# (+application/octet-stream+ gets the {Unsupported} refusal).
|
|
14
|
+
# * Without one (the local-file path, {FileType.detect_mime} → +nil+): claim
|
|
15
|
+
# anything passing {FileType.binary?} on the sample; opaque binaries stay
|
|
16
|
+
# unclaimed and surface as {Unsupported}.
|
|
23
17
|
module Passthrough
|
|
24
18
|
# @return [Symbol] {Page#kind} tag.
|
|
25
19
|
def self.kind
|
|
@@ -37,25 +31,21 @@ module Pikuri
|
|
|
37
31
|
end
|
|
38
32
|
|
|
39
33
|
# @param io [IO, StringIO] IO over the text content.
|
|
40
|
-
# @return [String] the content, tagged UTF-8. Deliberately NOT
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# join of chomped lines would silently normalize away.
|
|
34
|
+
# @return [String] the content, tagged UTF-8. Deliberately NOT derived
|
|
35
|
+
# from {.extract_lines} — a passthrough stays verbatim (trailing
|
|
36
|
+
# newline, CRLF), which joining chomped lines would normalize away.
|
|
44
37
|
def self.extract(io)
|
|
45
38
|
io.read.force_encoding(Encoding::UTF_8)
|
|
46
39
|
end
|
|
47
40
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# this stream's tail for an exact +total_lines+ (see
|
|
53
|
-
# {Extractor.extract_paged}).
|
|
41
|
+
# Lazy line stream for {Extractor.extract_paged}: read line-by-line, so a
|
|
42
|
+
# window over the head of a gigabyte log never loads the rest. The whole
|
|
43
|
+
# stream is a cheap sequential read — which is why paging counts this
|
|
44
|
+
# tail for an exact +total_lines+.
|
|
54
45
|
#
|
|
55
|
-
# @param io [IO, StringIO] IO over the text content; must
|
|
56
|
-
#
|
|
57
|
-
# @return [Enumerator::Lazy<String>] chomped lines, tagged
|
|
58
|
-
# UTF-8.
|
|
46
|
+
# @param io [IO, StringIO] IO over the text content; must stay open while
|
|
47
|
+
# the enumerator is consumed.
|
|
48
|
+
# @return [Enumerator::Lazy<String>] chomped lines, tagged UTF-8.
|
|
59
49
|
def self.extract_lines(io)
|
|
60
50
|
io.each_line.lazy.map { |raw| raw.chomp.force_encoding(Encoding::UTF_8) }
|
|
61
51
|
end
|