Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!Dockerfile
62 changes: 62 additions & 0 deletions .github/workflows/deploy-web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Deploy Web to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'

- name: Install npm dependencies
run: npm ci --ignore-scripts

- name: Build web pages (JP + EN)
run: npm run web

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: articles/webroot

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
5 changes: 4 additions & 1 deletion .github/workflows/on_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ name: Build Re:VIEW to make distribution file
# The workflow is triggered on pushes to the repository.
on: [push]

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
name:
name:
runs-on: ubuntu-latest
steps:
# uses v2 Stable version
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ articles/*.html
# articles/*.md
articles/*.xml
articles/*.txt
articles/webroot/

.DS_Store
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.3.10
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM ruby:3.3-bookworm

# TeX Live + Japanese support for Re:VIEW PDF builds
RUN apt-get update && apt-get install -y --no-install-recommends \
texlive-luatex \
texlive-lang-japanese \
texlive-lang-cjk \
texlive-fonts-recommended \
texlive-fonts-extra \
texlive-latex-recommended \
texlive-latex-extra \
texlive-plain-generic \
texlive-extra-utils \
lmodern \
pandoc \
nodejs \
npm \
ghostscript \
&& rm -rf /var/lib/apt/lists/*

RUN gem install bundler:4.0.3

WORKDIR /book
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A sample Gemfile
source "https://rubygems.org"

gem 'review', '5.3.0'
gem 'review', '5.11.0'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このバージョンアップでPDF側は問題ない感じでしたっけ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一応チェックしましたが大丈夫そうでした

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(そもそもpdf側がdocker経由で5.1固定?な気がしてて

gem 'pandoc2review'
gem 'rake'
Comment thread
piti6 marked this conversation as resolved.
# gem 'review-peg', '0.2.2'
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const reviewTextMaker = `${reviewPrefix}rake text ${reviewPostfix}`;
const reviewIDGXMLMaker = `${reviewPrefix}rake idgxml ${reviewPostfix}`;
const reviewVivliostyle = `${reviewPrefix}rake vivliostyle ${reviewPostfix}`;

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

module.exports = grunt => {
grunt.initConfig({
Expand Down
252 changes: 252 additions & 0 deletions articles/layouts/layout-web.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="<%=h @language %>">
<head>
<meta charset="UTF-8" />
Comment thread
piti6 marked this conversation as resolved.
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<% if @javascripts.present? %>
<% @javascripts.each do |js| %>
<%= js %>

<% end %>
Comment thread
piti6 marked this conversation as resolved.
<% end %>
<% if @stylesheets.present? %>
<% @stylesheets.each do |style| %>
<link rel="stylesheet" type="text/css" href="<%=h style %>" />
<% end %>
<% end%>
<% if @next.present? %><link rel="next" title="<%= h(@next_title)%>" href="<%= h(@next.id.to_s+"."+@book.config['htmlext']) %>" /><% end %>
<% if @prev.present? %><link rel="prev" title="<%= h(@prev_title)%>" href="<%= h(@prev.id.to_s+"."+@book.config['htmlext']) %>" /><% end %>
<meta name="generator" content="Re:VIEW" />
<title><%= h(@title) %> | <%=h @book.config.name_of("booktitle")%></title>
</head>
<body<%= @body_ext %>>
<div class="book">
<nav class="side-content">
<div class="side-header">
<% if @book.config["coverimage"].present? %>
<a href="./" ><img class="side-cover" src="<%=h @book.config["imagedir"] %>/<%=h @book.config["coverimage"] %>" alt="<%=h @book.config.name_of("booktitle") %>" /></a>
<% end %>
<h1 class="side-title"><%=h @book.config.name_of("booktitle") %></h1>
</div>
<div class="lang-toggle" id="lang-toggle">
<a href="#" class="lang-btn" data-lang="ja">日本語</a>
<a href="#" class="lang-btn" data-lang="en">English</a>
</div>
<div class="search-box">
<input type="text" id="search-input" placeholder="Search..." autocomplete="off" aria-label="Search" />
<div id="search-results" class="search-results"></div>
</div>
<%= @toc %>
<p class="review-signature">powered by <a href="http://reviewml.org/">Re:VIEW</a></p>
</nav>
<div class="book-body">
<header>
</header>
<div class="book-page">
<%= @body %>
</div>
<nav class="book-navi book-prev">
<% if @prev.present? %>
<a href="<%= h(@prev.id.to_s+"."+@book.config['htmlext']) %>">
<div class="book-cursor"><span class="cursor-prev">&#9664;</span></div>
</a>
<% end %>
</nav>
<nav class="book-navi book-next">
<% if @next.present? %>
<a href="<%= h(@next.id.to_s+"."+@book.config['htmlext']) %>">
<div class="book-cursor"><span class="cursor-next">&#9654;</span></div>
</a>
<% end %>
</nav>
</div>
</div>
<footer>
<% if @book.config["copyright"].present? %>
<p class="copyright"><%=h @book.config["copyright"] %></p>
<% end %>
</footer>
<script>
(function() {
function normalizePage(value) {
var page = (value || '').split('#')[0].split('?')[0];
page = page.replace(/\.html$/, '');
if (page === '' || page === '.' || page === './' || page.charAt(page.length - 1) === '/') {
return 'index';
}
var last = page.split('/').pop();
return (last === '' || last === '.' || last === 'index') ? 'index' : last;
}

var currentPage = normalizePage(location.pathname);
var tocItems = document.querySelectorAll('.book-toc > li');
var headings = document.querySelectorAll('.book-page h2');

function matchPage(href) {
return normalizePage(href) === currentPage;
}

// Fix TOP link to use relative directory path instead of index.html
tocItems.forEach(function(li) {
var link = li.querySelector('a');
if (link && link.getAttribute('href') === 'index.html') {
link.setAttribute('href', './');
Comment thread
piti6 marked this conversation as resolved.
}
});

tocItems.forEach(function(li) {
var link = li.querySelector('a');
if (!link) return;
var href = link.getAttribute('href');

// Highlight current page
if (matchPage(href)) {
li.classList.add('toc-active');
}

// Add sub-sections for current page
if (matchPage(href) && headings.length > 0) {
var subUl = document.createElement('ul');
subUl.className = 'toc-sub';
headings.forEach(function(h2) {
var id = h2.getAttribute('id');
if (!id) return;
var text = h2.textContent.replace(/^\s+/, '');
var subLi = document.createElement('li');
var subA = document.createElement('a');
subA.href = '#' + id;
subA.textContent = text;
Comment thread
piti6 marked this conversation as resolved.
subLi.appendChild(subA);
subUl.appendChild(subLi);
});
li.appendChild(subUl);
}
});

// Scroll-based highlight for sub-sections
if (headings.length > 0) {
var subLinks = document.querySelectorAll('.toc-sub a');
function updateActive() {
var scrollY = window.scrollY || document.documentElement.scrollTop;
var active = null;
headings.forEach(function(h2, i) {
if (h2.getBoundingClientRect().top + window.scrollY - 80 <= scrollY) {
active = i;
}
});
subLinks.forEach(function(a, i) {
a.parentElement.classList.toggle('toc-sub-active', i === active);
});
}
window.addEventListener('scroll', updateActive);
updateActive();
}
})();

// --- Search ---
(function() {
var input = document.getElementById('search-input');
var resultsDiv = document.getElementById('search-results');
var searchIndex = null;

function runSearch() {
var q = input.value.trim().toLowerCase();
if (!q || !searchIndex) {
resultsDiv.innerHTML = '';
resultsDiv.style.display = 'none';
return;
}
var keywords = q.split(/\s+/);
var matches = searchIndex.filter(function(entry) {
var haystack = (entry.title + ' ' + entry.text).toLowerCase();
return keywords.every(function(kw) { return haystack.indexOf(kw) !== -1; });
}).slice(0, 15);

if (matches.length === 0) {
resultsDiv.innerHTML = '<div class="search-empty">No results</div>';
} else {
resultsDiv.innerHTML = matches.map(function(m) {
var href = m.id ? m.file + '#' + m.id : m.file;
var snippet = highlightSnippet(m.text, keywords);
return '<a class="search-result" href="' + href + '">'
+ '<div class="search-result-title">' + escapeHtml(m.title) + '</div>'
+ '<div class="search-result-snippet">' + snippet + '</div>'
+ '</a>';
}).join('');
}
resultsDiv.style.display = 'block';
}

input.addEventListener('focus', function() {
if (searchIndex) return;
fetch('search-index.json')
.then(function(r) { return r.json(); })
.then(function(data) { searchIndex = data; runSearch(); })
.catch(function() { searchIndex = []; });
});

input.addEventListener('input', runSearch);

// Close results on outside click
document.addEventListener('click', function(e) {
if (!e.target.closest('.search-box')) {
resultsDiv.style.display = 'none';
}
});

// Close on Escape
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
resultsDiv.style.display = 'none';
input.blur();
}
});

function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function highlightSnippet(text, keywords) {
// Find the first keyword match position and show context around it
var lower = text.toLowerCase();
var pos = -1;
for (var i = 0; i < keywords.length; i++) {
var p = lower.indexOf(keywords[i]);
if (p !== -1 && (pos === -1 || p < pos)) pos = p;
}
var start = Math.max(0, pos - 30);
var end = Math.min(text.length, start + 120);
var snippet = (start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : '');
snippet = escapeHtml(snippet);
keywords.forEach(function(kw) {
var re = new RegExp('(' + kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
snippet = snippet.replace(re, '<mark>$1</mark>');
});
return snippet;
}
})();

// --- Language Toggle ---
(function() {
var path = location.pathname;
var langMatch = path.match(/\/(ja|en)\//);
var currentLang = langMatch ? langMatch[1] : 'ja';

// Highlight active language
document.querySelectorAll('.lang-btn').forEach(function(btn) {
var lang = btn.getAttribute('data-lang');
if (lang === currentLang) {
btn.classList.add('lang-active');
}
btn.addEventListener('click', function(e) {
e.preventDefault();
if (lang === currentLang) return;
var newPath = path.replace('/' + currentLang + '/', '/' + lang + '/');
location.href = newPath + location.hash;
});
});
})();
</script>
</body>
</html>
Loading
Loading