Skip to content

Commit 9af1d72

Browse files
committed
squash: add webnovel sitesetting extension handler and kakuyomu sitesetting
1 parent d30f29a commit 9af1d72

File tree

3 files changed

+220
-39
lines changed

3 files changed

+220
-39
lines changed

lib/sitesetting.rb

+13-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
require "yaml"
88
require_relative "narou/api"
9+
require_relative "sitesettinghandler"
910

1011
class SiteSetting
1112
NOVEL_SITE_SETTING_DIR = "webnovel/"
@@ -101,14 +102,18 @@ def multi_match(source, *keys)
101102
keys.each do |key|
102103
setting_value = self[key] or next
103104
[*setting_value].each do |value|
104-
match_data = source.match(/#{value}/m)
105-
if match_data
106-
@match_values[key] = value # yamlのキーでもmatch_valuesに設定しておくが、
107-
update_match_values(match_data) # ←ここで同名のグループ名が定義されていたら上書きされるので注意
108-
# 例えば、title: <title>(?<title>.+?)</title> と定義されていた場合、
109-
# @match_values["title"] には (?<title>.+?) 部分の要素が反映される
110-
break
111-
end
105+
handle = SiteSettingHandler.handler(self, value)
106+
match_data = handle&.match(source) # ハンドルオブジェクトを得て、それにより処理する
107+
next unless match_data
108+
value = handle.value if handle.respond_to?(:value)
109+
# rubocop:disable Layout/CommentIndentation
110+
# 通常はこれまで通りだが、valueを変更することも可能にする
111+
@match_values[key] = value # yamlのキーでもmatch_valuesに設定しておくが、
112+
update_match_values(match_data) # ←ここで同名のグループ名が定義されていたら上書きされるので注意
113+
# 例えば、title: <title>(?<title>.+?)</title> と定義されていた場合、
114+
# @match_values["title"] には (?<title>.+?) 部分の要素が反映される
115+
# rubocop:enable Layout/CommentIndentation
116+
break
112117
end
113118
end
114119
match_data

lib/sitesettinghandler.rb

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# frozen_string_literal: true
2+
3+
require "weakref"
4+
require_relative "worker"
5+
6+
class SiteSettingHandler
7+
HANDLER_DIR = "handler"
8+
HANDLER_EXT = ".rb"
9+
10+
@@klasses = {}
11+
12+
class << self
13+
# @type : ハンドラのタイプ名
14+
attr_reader :type
15+
16+
#
17+
# ハンドラを登録する
18+
#
19+
# 継承したクラスで呼び出してハンドラを登録する
20+
# 通常、ファイル名からタイプ名を作るが、引数で直接指定も出来る
21+
# クラスを継承していない場合はクラスを指定して登録する
22+
#
23+
def add_handler(path_or_type: @@current_path, klass: self)
24+
@type = File.basename(path_or_type, HANDLER_EXT)
25+
@@klasses[@type] = klass
26+
end
27+
28+
#
29+
# ハンドラの呼び出し
30+
#
31+
# 引数から対応するハンドラを呼び出しインスタンスを作る
32+
# 引数がこのクラスの派生オブジェクトならそのまま返す
33+
# それ以外でもメソッドmatchを持つならそのまま返す
34+
# それ以外なら正規表現であるので正規表現ハンドラにして返す
35+
#
36+
def handler(parent, value)
37+
case value
38+
when Array
39+
# key:
40+
# type: value
41+
_make_handler(parent, *value)
42+
when Hash
43+
# key:
44+
# - type: value
45+
_make_handler(parent, *value.flatten)
46+
when String
47+
# key:
48+
# value
49+
/#{value}/m
50+
when self
51+
# 派生オブジェクトならそのまま返す
52+
value
53+
else
54+
if value.respond_to?(:match)
55+
# matchメソッドを持つオブジェクトならそのまま返す
56+
value
57+
else
58+
# その他であれば正規表現として生成
59+
/#{value}/m
60+
end
61+
end
62+
end
63+
64+
def _make_handler(parent, type, value)
65+
# typeが未登録の場合、例外が発生する
66+
klass = @@klasses.fetch(type)
67+
klass.make(value, WeakRef.new(parent))
68+
end
69+
70+
# makeを再定義すれば別のインスタンスを返すことも可能
71+
alias make new
72+
73+
end
74+
75+
def initialize(value, parent)
76+
@value = value
77+
@parent = parent
78+
end
79+
80+
# ハンドラタイプ名
81+
# class_attribute :type, instance_writer: false
82+
def type
83+
self.class.type
84+
end
85+
86+
# 呼び出し元のオブジェクト
87+
def parent
88+
@parent&.weakref_alive? ? @parent : nil
89+
end
90+
91+
# valueを定義すれば変更できる
92+
# def value()
93+
# self
94+
# end
95+
#
96+
# sourceから値が得られるかチェックし、得られるなら値をセットする
97+
# 継承したクラスにて実装する
98+
# def match(source)
99+
# PseudoMatchData.new("post_match", {"key" => "value"})
100+
# end
101+
102+
# matchが返すmatch_dataのためのクラス
103+
class PseudoMatchData < Hash
104+
attr_reader :post_match
105+
106+
def initialize(post_match, hash)
107+
super()
108+
@post_match = post_match
109+
replace(hash)
110+
end
111+
112+
def names
113+
keys.map(&:to_s)
114+
end
115+
end
116+
end
117+
118+
class EvalHandler < SiteSettingHandler
119+
def match(source)
120+
eval(@value, binding, parent&.path || "(nil)") # rubocop:disable Security/Eval
121+
end
122+
add_handler(path_or_type: "eval")
123+
end

webnovel/kakuyomu.jp.yaml

+84-31
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,87 @@ confirm_over18: no
1010
append_title_to_folder_name: yes
1111
title_strip_pattern: null
1212
sitename: *name
13-
version: 1.0
13+
version: 2.0
14+
15+
# ------------------------------------------------------------
16+
# 前処理設定
17+
# jsonを処理して正規表現で取得可能なデータへ変換し挿入する
18+
code: &code
19+
eval: |-
20+
magicword = "KakuyomuPreprocessEvalMagicWord"
21+
# magicwordがある場合、既にこの前処理が行われているので何もしない
22+
unless source.include?(magicword)
23+
require "json"
24+
source.match(%r|<script id="__NEXT_DATA__" type="application/json">(.*)</script>|) do |m|
25+
json = JSON.parse($1)
26+
# data: 各データが収められたハッシュ
27+
data = json["props"]["pageProps"]["__APOLLO_STATE__"]
28+
# work: この作品のデータ
29+
work = data["Work:#{json["query"]["workId"]}"]
30+
# まずTOCを処理する
31+
toc = work["tableOfContents"]
32+
# workのTOCはTableOfContentsChapterの参照の配列となっているので、それを解決する
33+
toc.map! {|v| data[v["__ref"]]}
34+
# TableOfContentsChapterにはChapterと各話の配列があるのでそれを取り出し
35+
toc.map! {|v| [v["chapter"], v["episodeUnions"]]}
36+
# Chapterと各話を一つの配列にフラットにし
37+
toc.flatten!
38+
# Chapterが使われていない場合にあるnilを排除し
39+
toc.compact!
40+
# Chapterと各話は参照なので、それを解決し
41+
toc.map! {|v| data[v["__ref"]]}
42+
# 必要な属性値を取り出して「;」でつないでまとめる
43+
# __typename: ChapterかEpisodeかの判別用
44+
# level: Chapterにあり1か2かでchapterかsubchapterか判別する
45+
# id: Episodeならindex値として必要。Chapterなら必要ないが処理の簡略化のため付加している
46+
# publishedAt: Episodeにありsubupdateとして利用する
47+
# title: Chapter、Episode共にタイトルとなる
48+
# 最後のtitle以外で「;」を含む事は無いはず
49+
toc_attr = %w(__typename level id publishedAt title)
50+
toc.map! {|v| v.slice(*toc_attr).values.join(";")}
51+
# 続いてworkの著者を処理する
52+
# 著者の参照から表示用の名前 activityName を取得する
53+
work["author"] = data[work["author"]["__ref"]]["activityName"]
54+
# alternateAuthorName がある場合は、それも使う。
55+
work["author"] = work["alternateAuthorName"] + "/" + work["author"] if work["alternateAuthorName"]
56+
# あらすじの改行を<br>に変換し、正規表現を単純化する。<br>はnarourb側で再変換される
57+
work["introduction"].gsub!("\n", "<br>")
58+
# workからTOC以外に利用する属性を列挙している
59+
work_attr = %w(author title serialStatus publicEpisodeCount publishedAt
60+
editedAt lastEpisodePublishedAt totalCharacterCount introduction)
61+
# HTMLコメントの開始、前処理済みか検知するためのmagicword、
62+
# TOC以外のworkの利用する属性値、TOC、HTMLコメントの終了を生成し
63+
str = ["<!---", magicword,
64+
work.slice(*work_attr).map {|v| v.join("::")},
65+
toc, "--->"].join("\n")
66+
# 元のページデータに挿入する。デバッグ時用に元々のデータはすべて残す。
67+
source.insert(m.begin(0), str)
68+
# 元々のデータは基本必要ないのでreplaceを使えば元々のデータはすべて消える。
69+
#source.replace(str)
70+
end
71+
end
72+
nil
1473
1574
# ------------------------------------------------------------
1675
# 書籍情報取得設定
17-
title: &title |-
18-
<h1 id="workTitle"><a href=".+?">(?<title>.+?)</a></h1>
19-
author: |-
20-
(?:<span class="activityName" itemprop="author">(?<author>.+?)</span>)|(?:<span class="screenName.*?">(?<author>.+?)</span>)
21-
story: &story |-
22-
<p id="introduction" class="ui-truncateTextButton js-work-introduction">(?<story>.+?)(?:[\n ]*?</span>|[\n ]*?</p>)
76+
title: &title
77+
- *code
78+
- ^title::(?<title>.+?)$
79+
author:
80+
- ^author::(?<author>.+?)$
81+
story: &story
82+
- ^introduction::(?<story>.+?)$
2383

2484
# ------------------------------------------------------------
2585
# 目次取得設定
2686
toc_url: \\k<top_url>/works/\\k<ncode>
27-
subtitles: |-2
28-
(?:<li class="widget-toc-chapter widget-toc-level1.*?">
29-
<span>(?<chapter>.+?)</span>
30-
</li>
31-
)?(?:<li class="widget-toc-chapter widget-toc-level2.*?">
32-
<span>(?<subchapter>.+?)</span>
33-
</li>
34-
)?<li class="widget-toc-episode">
35-
<a href="(?<href>/works/\d+/episodes/(?<index>\d+))".*?>
36-
<span class="widget-toc-episode-titleLabel.*?">(?<subtitle>.+?)</span>
37-
<time class="widget-toc-episode-datePublished" datetime="(?<subupdate>.+?)">.+?</time>
38-
</a>
39-
</li>
87+
subtitles:
88+
- *code
89+
- (?x)
90+
^(?:Chapter;1;\d+;(?<chapter>.+?)\n)?
91+
(?:Chapter;2;\d+;(?<subchapter>.+?)\n)?
92+
Episode;(?<index>\d+);(?<subupdate>.+?);(?<subtitle>.+?)$
93+
href: /works/\\k<ncode>/episodes/\\k<index>
4094

4195
# ------------------------------------------------------------
4296
# 本文取得設定
@@ -53,29 +107,28 @@ novel_info_url: \\k<toc_url>
53107
t: *title
54108

55109
# novel_type 小説種別
56-
nt: '<dt><span>執筆状況</span></dt>\n *?<dd>(?<novel_type>.+?)</dd>'
110+
nt: ^serialStatus::(?<novel_type>.+?)$
57111
novel_type_string:
58-
連載中: 1
59-
完結済: 3
112+
RUNNING: 1
113+
COMPLETED: 3
60114

61115
# general_all_no 掲載話数
62-
ga: '<dt><span>エピソード</span></dt>\n *?<dd>(?<general_all_no>\d+)話</dd>'
116+
ga: ^publicEpisodeCount::(?<general_all_no>.+?)$
63117

64118
# story あらすじ
65119
s: *story
66120

67121
# general_firstup 初回掲載日
68-
gf: '<dt><span>公開日</span></dt>\n *?<dd><time itemprop="datePublished" datetime="(?<general_firstup>.+?)">.+?</time>'
122+
gf: ^publishedAt::(?<general_firstup>.+?)$
69123

70-
# novelupdated_at 小説の更新時刻。最終掲載日で代用
71-
nu: '<dt><span>最終更新日</span></dt>\n *?<dd><time itemprop="dateModified" datetime="(?<novelupdated_at>.+?)">.+?</time>'
124+
# novelupdated_at 小説の更新時刻
125+
nu: ^editedAt::(?<novelupdated_at>.+?)$
72126

73127
# general_lastup 最新話掲載日
74-
gl: null
128+
gl: ^lastEpisodePublishedAt::(?<general_lastup>.+?)$
75129

76130
# writer 作者名
77-
w: |-
78-
(?:<span class="activityName" itemprop="author">(?<writer>.+?)</span>)|(?:<span class="screenName.*?">(?<writer>.+?)</span>)
131+
w: ^author::(?<writer>.+?)$
79132

80133
# length 文字数
81-
l: '<dt><span>総文字数</span></dt>\n *?<dd>(?<length>.+?)文字</dd>'
134+
l: ^totalCharacterCount::(?<length>.+?)$

0 commit comments

Comments
 (0)