Skip to content

Commit 9b76326

Browse files
authored
Merge pull request #40 from CyberAgentGameEntertainment/feature/kim/web-support
Upgrade dependencies, enhance web layout, and add CI for deployment
2 parents 407ae51 + 20a6e25 commit 9b76326

21 files changed

Lines changed: 1949 additions & 1903 deletions

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!Dockerfile

.github/workflows/deploy-web.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Deploy Web to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: true
16+
17+
env:
18+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
19+
20+
jobs:
21+
build:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Ruby
28+
uses: ruby/setup-ruby@v1
29+
with:
30+
ruby-version: '3.3'
31+
bundler-cache: true
32+
33+
- name: Setup Node.js
34+
uses: actions/setup-node@v4
35+
with:
36+
node-version: '24'
37+
cache: 'npm'
38+
39+
- name: Install npm dependencies
40+
run: npm ci --ignore-scripts
41+
42+
- name: Build web pages (JP + EN)
43+
run: npm run web
44+
45+
- name: Setup Pages
46+
uses: actions/configure-pages@v4
47+
48+
- name: Upload artifact
49+
uses: actions/upload-pages-artifact@v3
50+
with:
51+
path: articles/webroot
52+
53+
deploy:
54+
needs: build
55+
runs-on: ubuntu-latest
56+
environment:
57+
name: github-pages
58+
url: ${{ steps.deployment.outputs.page_url }}
59+
steps:
60+
- name: Deploy to GitHub Pages
61+
id: deployment
62+
uses: actions/deploy-pages@v4

.github/workflows/on_push.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ name: Build Re:VIEW to make distribution file
22
# The workflow is triggered on pushes to the repository.
33
on: [push]
44

5+
env:
6+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
7+
58
jobs:
69
build:
7-
name:
10+
name:
811
runs-on: ubuntu-latest
912
steps:
1013
# uses v2 Stable version

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ articles/*.html
88
# articles/*.md
99
articles/*.xml
1010
articles/*.txt
11+
articles/webroot/
1112

1213
.DS_Store

.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.3.10

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM ruby:3.3-bookworm
2+
3+
# TeX Live + Japanese support for Re:VIEW PDF builds
4+
RUN apt-get update && apt-get install -y --no-install-recommends \
5+
texlive-luatex \
6+
texlive-lang-japanese \
7+
texlive-lang-cjk \
8+
texlive-fonts-recommended \
9+
texlive-fonts-extra \
10+
texlive-latex-recommended \
11+
texlive-latex-extra \
12+
texlive-plain-generic \
13+
texlive-extra-utils \
14+
lmodern \
15+
pandoc \
16+
nodejs \
17+
npm \
18+
ghostscript \
19+
&& rm -rf /var/lib/apt/lists/*
20+
21+
RUN gem install bundler:4.0.3
22+
23+
WORKDIR /book

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# A sample Gemfile
22
source "https://rubygems.org"
33

4-
gem 'review', '5.3.0'
4+
gem 'review', '5.11.0'
55
gem 'pandoc2review'
66
gem 'rake'
77
# gem 'review-peg', '0.2.2'

Gruntfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const reviewTextMaker = `${reviewPrefix}rake text ${reviewPostfix}`;
1717
const reviewIDGXMLMaker = `${reviewPrefix}rake idgxml ${reviewPostfix}`;
1818
const reviewVivliostyle = `${reviewPrefix}rake vivliostyle ${reviewPostfix}`;
1919

20-
const bookConfig = yaml.safeLoad(fs.readFileSync(`${articles}/${reviewConfig}`, "utf8"));
20+
const bookConfig = yaml.load(fs.readFileSync(`${articles}/${reviewConfig}`, "utf8"));
2121

2222
module.exports = grunt => {
2323
grunt.initConfig({
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE html>
3+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="<%=h @language %>">
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<% if @javascripts.present? %>
8+
<% @javascripts.each do |js| %>
9+
<%= js %>
10+
11+
<% end %>
12+
<% end %>
13+
<% if @stylesheets.present? %>
14+
<% @stylesheets.each do |style| %>
15+
<link rel="stylesheet" type="text/css" href="<%=h style %>" />
16+
<% end %>
17+
<% end%>
18+
<% if @next.present? %><link rel="next" title="<%= h(@next_title)%>" href="<%= h(@next.id.to_s+"."+@book.config['htmlext']) %>" /><% end %>
19+
<% if @prev.present? %><link rel="prev" title="<%= h(@prev_title)%>" href="<%= h(@prev.id.to_s+"."+@book.config['htmlext']) %>" /><% end %>
20+
<meta name="generator" content="Re:VIEW" />
21+
<title><%= h(@title) %> | <%=h @book.config.name_of("booktitle")%></title>
22+
</head>
23+
<body<%= @body_ext %>>
24+
<div class="book">
25+
<nav class="side-content">
26+
<div class="side-header">
27+
<% if @book.config["coverimage"].present? %>
28+
<a href="./" ><img class="side-cover" src="<%=h @book.config["imagedir"] %>/<%=h @book.config["coverimage"] %>" alt="<%=h @book.config.name_of("booktitle") %>" /></a>
29+
<% end %>
30+
<h1 class="side-title"><%=h @book.config.name_of("booktitle") %></h1>
31+
</div>
32+
<div class="lang-toggle" id="lang-toggle">
33+
<a href="#" class="lang-btn" data-lang="ja">日本語</a>
34+
<a href="#" class="lang-btn" data-lang="en">English</a>
35+
</div>
36+
<div class="search-box">
37+
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" aria-label="Search" />
38+
<div id="search-results" class="search-results"></div>
39+
</div>
40+
<%= @toc %>
41+
<p class="review-signature">powered by <a href="http://reviewml.org/">Re:VIEW</a></p>
42+
</nav>
43+
<div class="book-body">
44+
<header>
45+
</header>
46+
<div class="book-page">
47+
<%= @body %>
48+
</div>
49+
<nav class="book-navi book-prev">
50+
<% if @prev.present? %>
51+
<a href="<%= h(@prev.id.to_s+"."+@book.config['htmlext']) %>">
52+
<div class="book-cursor"><span class="cursor-prev">&#9664;</span></div>
53+
</a>
54+
<% end %>
55+
</nav>
56+
<nav class="book-navi book-next">
57+
<% if @next.present? %>
58+
<a href="<%= h(@next.id.to_s+"."+@book.config['htmlext']) %>">
59+
<div class="book-cursor"><span class="cursor-next">&#9654;</span></div>
60+
</a>
61+
<% end %>
62+
</nav>
63+
</div>
64+
</div>
65+
<footer>
66+
<% if @book.config["copyright"].present? %>
67+
<p class="copyright"><%=h @book.config["copyright"] %></p>
68+
<% end %>
69+
</footer>
70+
<script>
71+
(function() {
72+
function normalizePage(value) {
73+
var page = (value || '').split('#')[0].split('?')[0];
74+
page = page.replace(/\.html$/, '');
75+
if (page === '' || page === '.' || page === './' || page.charAt(page.length - 1) === '/') {
76+
return 'index';
77+
}
78+
var last = page.split('/').pop();
79+
return (last === '' || last === '.' || last === 'index') ? 'index' : last;
80+
}
81+
82+
var currentPage = normalizePage(location.pathname);
83+
var tocItems = document.querySelectorAll('.book-toc > li');
84+
var headings = document.querySelectorAll('.book-page h2');
85+
86+
function matchPage(href) {
87+
return normalizePage(href) === currentPage;
88+
}
89+
90+
// Fix TOP link to use relative directory path instead of index.html
91+
tocItems.forEach(function(li) {
92+
var link = li.querySelector('a');
93+
if (link && link.getAttribute('href') === 'index.html') {
94+
link.setAttribute('href', './');
95+
}
96+
});
97+
98+
tocItems.forEach(function(li) {
99+
var link = li.querySelector('a');
100+
if (!link) return;
101+
var href = link.getAttribute('href');
102+
103+
// Highlight current page
104+
if (matchPage(href)) {
105+
li.classList.add('toc-active');
106+
}
107+
108+
// Add sub-sections for current page
109+
if (matchPage(href) && headings.length > 0) {
110+
var subUl = document.createElement('ul');
111+
subUl.className = 'toc-sub';
112+
headings.forEach(function(h2) {
113+
var id = h2.getAttribute('id');
114+
if (!id) return;
115+
var text = h2.textContent.replace(/^\s+/, '');
116+
var subLi = document.createElement('li');
117+
var subA = document.createElement('a');
118+
subA.href = '#' + id;
119+
subA.textContent = text;
120+
subLi.appendChild(subA);
121+
subUl.appendChild(subLi);
122+
});
123+
li.appendChild(subUl);
124+
}
125+
});
126+
127+
// Scroll-based highlight for sub-sections
128+
if (headings.length > 0) {
129+
var subLinks = document.querySelectorAll('.toc-sub a');
130+
function updateActive() {
131+
var scrollY = window.scrollY || document.documentElement.scrollTop;
132+
var active = null;
133+
headings.forEach(function(h2, i) {
134+
if (h2.getBoundingClientRect().top + window.scrollY - 80 <= scrollY) {
135+
active = i;
136+
}
137+
});
138+
subLinks.forEach(function(a, i) {
139+
a.parentElement.classList.toggle('toc-sub-active', i === active);
140+
});
141+
}
142+
window.addEventListener('scroll', updateActive);
143+
updateActive();
144+
}
145+
})();
146+
147+
// --- Search ---
148+
(function() {
149+
var input = document.getElementById('search-input');
150+
var resultsDiv = document.getElementById('search-results');
151+
var searchIndex = null;
152+
153+
function runSearch() {
154+
var q = input.value.trim().toLowerCase();
155+
if (!q || !searchIndex) {
156+
resultsDiv.innerHTML = '';
157+
resultsDiv.style.display = 'none';
158+
return;
159+
}
160+
var keywords = q.split(/\s+/);
161+
var matches = searchIndex.filter(function(entry) {
162+
var haystack = (entry.title + ' ' + entry.text).toLowerCase();
163+
return keywords.every(function(kw) { return haystack.indexOf(kw) !== -1; });
164+
}).slice(0, 15);
165+
166+
if (matches.length === 0) {
167+
resultsDiv.innerHTML = '<div class="search-empty">No results</div>';
168+
} else {
169+
resultsDiv.innerHTML = matches.map(function(m) {
170+
var href = m.id ? m.file + '#' + m.id : m.file;
171+
var snippet = highlightSnippet(m.text, keywords);
172+
return '<a class="search-result" href="' + href + '">'
173+
+ '<div class="search-result-title">' + escapeHtml(m.title) + '</div>'
174+
+ '<div class="search-result-snippet">' + snippet + '</div>'
175+
+ '</a>';
176+
}).join('');
177+
}
178+
resultsDiv.style.display = 'block';
179+
}
180+
181+
input.addEventListener('focus', function() {
182+
if (searchIndex) return;
183+
fetch('search-index.json')
184+
.then(function(r) { return r.json(); })
185+
.then(function(data) { searchIndex = data; runSearch(); })
186+
.catch(function() { searchIndex = []; });
187+
});
188+
189+
input.addEventListener('input', runSearch);
190+
191+
// Close results on outside click
192+
document.addEventListener('click', function(e) {
193+
if (!e.target.closest('.search-box')) {
194+
resultsDiv.style.display = 'none';
195+
}
196+
});
197+
198+
// Close on Escape
199+
input.addEventListener('keydown', function(e) {
200+
if (e.key === 'Escape') {
201+
resultsDiv.style.display = 'none';
202+
input.blur();
203+
}
204+
});
205+
206+
function escapeHtml(s) {
207+
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
208+
}
209+
210+
function highlightSnippet(text, keywords) {
211+
// Find the first keyword match position and show context around it
212+
var lower = text.toLowerCase();
213+
var pos = -1;
214+
for (var i = 0; i < keywords.length; i++) {
215+
var p = lower.indexOf(keywords[i]);
216+
if (p !== -1 && (pos === -1 || p < pos)) pos = p;
217+
}
218+
var start = Math.max(0, pos - 30);
219+
var end = Math.min(text.length, start + 120);
220+
var snippet = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '');
221+
snippet = escapeHtml(snippet);
222+
keywords.forEach(function(kw) {
223+
var re = new RegExp('(' + kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
224+
snippet = snippet.replace(re, '<mark>$1</mark>');
225+
});
226+
return snippet;
227+
}
228+
})();
229+
230+
// --- Language Toggle ---
231+
(function() {
232+
var path = location.pathname;
233+
var langMatch = path.match(/\/(ja|en)\//);
234+
var currentLang = langMatch ? langMatch[1] : 'ja';
235+
236+
// Highlight active language
237+
document.querySelectorAll('.lang-btn').forEach(function(btn) {
238+
var lang = btn.getAttribute('data-lang');
239+
if (lang === currentLang) {
240+
btn.classList.add('lang-active');
241+
}
242+
btn.addEventListener('click', function(e) {
243+
e.preventDefault();
244+
if (lang === currentLang) return;
245+
var newPath = path.replace('/' + currentLang + '/', '/' + lang + '/');
246+
location.href = newPath + location.hash;
247+
});
248+
});
249+
})();
250+
</script>
251+
</body>
252+
</html>

0 commit comments

Comments
 (0)