From 0d75eee9466cdbefa1a2500a806d7cbc6deaaa83 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Thu, 23 Oct 2025 19:31:44 +0900 Subject: [PATCH 01/24] =?UTF-8?q?CircleCI=20=E3=82=92=20GitHub=20Actions?= =?UTF-8?q?=20=E3=81=AB=E7=A7=BB=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .circleci/config.yml | 49 ------------------------------- .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 49 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 474f50fa..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: 2 -jobs: - build: - working_directory: ~/circleci-narou - docker: - - image: circleci/ruby:3.0 - environment: - BUNDLE_JOBS: 1 - BUNDLE_RETRY: 3 - BUNDLE_PATH: vendor/bundle - steps: - - checkout - - - run: - name: Install bundler 2.1.4 - command: gem install bundler:2.1.4 - - # Restore bundle cache - - restore_cache: - keys: - - narou-bundle-{{ checksum "Gemfile.lock" }} - - narou-bundle- - - - run: - name: Bundle Install - command: bundle check || bundle install - - # Store bundle cache - - save_cache: - key: narou-bundle-{{ checksum "Gemfile.lock" }} - paths: - - vendor/bundle - - - run: - name: Run rspec - command: | - bundle exec rspec \ - --format RspecJunitFormatter \ - --out test_results/rspec/results.xml \ - --format progress \ - $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - - # Save test results for timing analysis - - store_test_results: - path: test_results - - - store_artifacts: - path: ./coverage - destination: artifact-file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9d53680a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + push: + branches: [ "**" ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + env: + BUNDLE_JOBS: "1" + BUNDLE_RETRY: "3" + BUNDLE_PATH: vendor/bundle + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Ruby 3.0 + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.0" + + - name: Install bundler 2.1.4 + run: gem install bundler:2.1.4 + + - name: Cache bundle + uses: actions/cache@v4 + with: + path: vendor/bundle + key: ${{ runner.os }}-narou-bundle-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-narou-bundle- + + - name: Bundle install + run: | + bundle _2.1.4_ config set path "${BUNDLE_PATH}" + bundle _2.1.4_ check || bundle _2.1.4_ install + + - name: Run RSpec + run: | + mkdir -p test_results/rspec + # CircleCI のテスト分割は GHA ではそのまま使えないため、単純実行に置き換え + bundle _2.1.4_ exec rspec \ + --format RspecJunitFormatter \ + --out test_results/rspec/results.xml \ + --format progress + + - name: Upload test results (JUnit XML) + if: always() + uses: actions/upload-artifact@v4 + with: + name: rspec-junit + path: test_results + + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage From 0dac470a1f278fba4191d34d0ae64f6350a2415c Mon Sep 17 00:00:00 2001 From: ponponusa Date: Fri, 24 Oct 2025 00:33:47 +0900 Subject: [PATCH 02/24] =?UTF-8?q?HTTP=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=82=92weblick=E3=81=8B=E3=82=89puma=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20docker=E7=92=B0=E5=A2=83=E3=81=AB=E3=81=8A=E3=81=84?= =?UTF-8?q?=E3=81=A6=E3=83=87=E3=83=90=E3=82=A4=E3=82=B9=E8=AA=8D=E8=AD=98?= =?UTF-8?q?=E3=82=92=E7=84=A1=E5=8A=B9=E3=81=AB=20=E5=90=84=E7=A8=AEgem?= =?UTF-8?q?=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E3=82=92=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 14 +-- Gemfile.lock | 216 ++++++++++++++++++++++------------- lib/device.rb | 3 + lib/device/library/docker.rb | 17 +++ lib/helper.rb | 7 ++ lib/web/appserver.rb | 7 +- narou.gemspec | 47 ++++---- 7 files changed, 201 insertions(+), 110 deletions(-) create mode 100644 lib/device/library/docker.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d53680a..4b1ec6a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up Ruby 3.0 + - name: Set up Ruby 3.4 uses: ruby/setup-ruby@v1 with: - ruby-version: "3.0" + ruby-version: "3.4" - - name: Install bundler 2.1.4 - run: gem install bundler:2.1.4 + - name: Install bundler 2.7.2 + run: gem install bundler:2.7.2 - name: Cache bundle uses: actions/cache@v4 @@ -36,14 +36,14 @@ jobs: - name: Bundle install run: | - bundle _2.1.4_ config set path "${BUNDLE_PATH}" - bundle _2.1.4_ check || bundle _2.1.4_ install + bundle _2.7.2_ config set path "${BUNDLE_PATH}" + bundle _2.7.2_ check || bundle _2.7.2_ install - name: Run RSpec run: | mkdir -p test_results/rspec # CircleCI のテスト分割は GHA ではそのまま使えないため、単純実行に置き換え - bundle _2.1.4_ exec rspec \ + bundle _2.7.2_ exec rspec \ --format RspecJunitFormatter \ --out test_results/rspec/results.xml \ --format progress diff --git a/Gemfile.lock b/Gemfile.lock index 0c0d6410..6ffe68ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,146 +1,204 @@ PATH remote: . specs: - narou (3.8.0) - activesupport (>= 6.1, < 8.0) - diff-lcs (~> 1.2, >= 1.2.5) - erubis (~> 2.7) - ffi (~> 1.4, >= 1.4.2) - haml (>= 5.1.2, < 6) - mail (~> 2.6.0, >= 2.6.6) - memoist (~> 0.11.0) + narou (3.9.1.20251023a) + activesupport (~> 8.0, >= 8.1.0) + csv (~> 3.3) + diff-lcs (~> 1.6, >= 1.6.2) + erubi (~> 1.13.1) + ffi (~> 1.17, >= 1.17.2) + haml (>= 5.2.2, < 6) + mail (~> 2.9, >= 2.9.0) + memoist (~> 0.16.2) + nkf (~> 0.2.0) open_uri_redirections (~> 0.2, >= 0.2.1) - pony (~> 1, >= 1.11) - psych (~> 4.0) - rubyzip (~> 2.0, >= 2.0.0) + pony (~> 1, >= 1.13) + psych (~> 5.2) + puma (~> 6.4) + rackup (~> 2.1) + rexml (~> 3.4) + rubyzip (~> 3.2, >= 3.2.0) sassc (~> 2.4) - sinatra (~> 2.0, >= 2.0.8.1) - sinatra-contrib (~> 2.0, >= 2.0.8.1) + sinatra (~> 4.2, >= 4.2.0) + sinatra-contrib (~> 4.2, >= 4.2.0) systemu (~> 2.6, >= 2.6.5) termcolorlight (~> 1.0, >= 1.1.1) - tilt (~> 2.0, >= 2.0.10) - unicode-display_width (~> 1.4) - webrick (~> 1.7) + tilt (~> 2.6, >= 2.6.1) + unicode-display_width (~> 3.2) GEM remote: https://rubygems.org/ specs: - activesupport (6.1.4) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (8.1.0) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - awesome_print (1.8.0) - byebug (11.1.3) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + awesome_print (1.9.2) + base64 (0.3.0) + bigdecimal (3.3.1) + bigdecimal (3.3.1-java) + byebug (12.0.0) coderay (1.1.3) - concurrent-ruby (1.1.9) - diff-lcs (1.4.4) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + csv (3.3.5) + date (3.4.1) + date (3.4.1-java) + diff-lcs (1.6.2) docile (1.3.5) - erubis (2.7.0) - ffi (1.15.3) - ffi (1.15.3-java) + drb (2.2.3) + erubi (1.13.1) + ffi (1.17.2) + ffi (1.17.2-java) haml (5.2.2) temple (>= 0.8.0) tilt i18n (1.8.10) concurrent-ruby (~> 1.0) jar-dependencies (0.4.1) - mail (2.6.6) - mime-types (>= 1.16, < 4) - memoist (0.11.0) + json (2.15.1) + json (2.15.1-java) + logger (1.7.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + memoist (0.16.2) method_source (1.0.0) - mime-types (3.3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2021.0704) + mini_mime (1.1.5) minitest (5.14.4) multi_json (1.15.0) - mustermann (1.1.1) + mustermann (3.0.4) ruby2_keywords (~> 0.0.1) + net-imap (0.5.12) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.4) + nio4r (2.7.4-java) + nkf (0.2.0) + nkf (0.2.0-java) open_uri_redirections (0.2.1) pony (1.13.1) mail (>= 2.0) - pry (0.13.1) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.13.1-java) + pry (0.15.2-java) coderay (~> 1.1) method_source (~> 1.0) spoon (~> 0.0) - pry-byebug (3.9.0) - byebug (~> 11.0) - pry (~> 0.13.0) - psych (4.0.1) - psych (4.0.1-java) + pry-byebug (3.11.0) + byebug (~> 12.0) + pry (>= 0.13, < 0.16) + psych (5.2.6) + date + stringio + psych (5.2.6-java) + date jar-dependencies (>= 0.1.7) - rack (2.2.3.1) - rack-protection (2.2.0) - rack - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + puma (6.6.1) + nio4r (~> 2.0) + puma (6.6.1-java) + nio4r (~> 2.0) + rack (3.2.3) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.2.1) + rack (>= 3) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) + rspec-support (~> 3.13.0) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.10.1) - rspec_junit_formatter (0.4.1) + rspec-support (3.13.6) + rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (3.2.0) sassc (2.4.0) ffi (~> 1.9) - simplecov (0.21.2) + securerandom (0.4.1) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.2) - sinatra (2.2.0) - mustermann (~> 1.0) - rack (~> 2.2) - rack-protection (= 2.2.0) + sinatra (4.2.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.2.1) + rack-session (>= 2.0.0, < 3) tilt (~> 2.0) - sinatra-contrib (2.2.0) - multi_json - mustermann (~> 1.0) - rack-protection (= 2.2.0) - sinatra (= 2.2.0) + sinatra-contrib (4.2.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.2.1) + sinatra (= 4.2.1) tilt (~> 2.0) spoon (0.0.6) ffi + stringio (3.1.7) systemu (2.6.5) temple (0.8.2) termcolorlight (1.1.1) - tilt (2.0.10) + tilt (2.6.1) timecop (0.9.2) - tzinfo (2.0.4) + timeout (0.4.3) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (1.7.0) - webrick (1.7.0) - zeitwerk (2.4.2) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + uri (1.0.4) PLATFORMS java ruby DEPENDENCIES - awesome_print (~> 1.8) + awesome_print (~> 1.9) narou! - pry (~> 0.12) - pry-byebug (~> 3.8) - rspec (~> 3.10) + pry (~> 0.15) + pry-byebug (~> 3.11) + rspec (~> 3.13) rspec-retry (~> 0.6) - rspec_junit_formatter (~> 0.4) - simplecov (~> 0.20) + rspec_junit_formatter (~> 0.6) + simplecov (~> 0.22) timecop (~> 0.9) BUNDLED WITH - 2.2.15 + 2.7.2 diff --git a/lib/device.rb b/lib/device.rb index 6c761173..f885ad78 100644 --- a/lib/device.rb +++ b/lib/device.rb @@ -33,6 +33,9 @@ def get_storage_path when :mac require_relative "device/library/mac" extend Device::Library::Mac + when :docker + require_relative "device/library/docker" + extend Device::Library::Docker else require_relative "device/library/linux" extend Device::Library::Linux diff --git a/lib/device/library/docker.rb b/lib/device/library/docker.rb new file mode 100644 index 00000000..e7f5d2ef --- /dev/null +++ b/lib/device/library/docker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# +# Copyright 2013 whiteleaf. All rights reserved. +# + +module Device::Library + module Docker + def get_device_root_dir(volume_name) + nil + end + + def eject(volume_name) + raise Device::CantEject + end + end +end diff --git a/lib/helper.rb b/lib/helper.rb index a2562fca..26693cc5 100644 --- a/lib/helper.rb +++ b/lib/helper.rb @@ -30,6 +30,11 @@ def os_cygwin? @@os_is_cygwin ||= HOST_OS =~ /cygwin/i end + def running_in_docker? + return false unless File.exist?('/proc/1/cgroup') + File.readlines('/proc/1/cgroup').any? { |line| line.include?('/docker/') || line.include?('/lxc/') } + end + def determine_os case when os_windows? @@ -38,6 +43,8 @@ def determine_os :mac when os_cygwin? :cygwin + when running_in_docker? + :docker else :other end diff --git a/lib/web/appserver.rb b/lib/web/appserver.rb index 4f018ae1..bfcd95bc 100755 --- a/lib/web/appserver.rb +++ b/lib/web/appserver.rb @@ -11,6 +11,9 @@ require "sinatra/base" require "sinatra/json" require "sinatra/reloader" if $development +require "securerandom" +require "rack/session" +require "rack/protection" # require "better_errors" if $debug require "tilt/erubi" require "tilt/haml" @@ -33,6 +36,7 @@ class Narou::AppServer < Sinatra::Base configure do set :app_file, __FILE__ set :erb, trim: "-" + set :quiet, true enable :protection enable :sessions @@ -41,7 +45,8 @@ class Narou::AppServer < Sinatra::Base end set :environment, :production unless $development - set :server, :webrick + set :server, :puma + set :server_settings, { Silent: true } if $debug use BetterErrors::Middleware diff --git a/narou.gemspec b/narou.gemspec index 0c3a5b6c..0ebc5adb 100644 --- a/narou.gemspec +++ b/narou.gemspec @@ -41,41 +41,42 @@ Gem::Specification.new do |gem| EOS gem.post_install_message = install_message.gsub("\t", " ") - gem.required_ruby_version = ">=2.3.0" + gem.required_ruby_version = ">=3.4.0" gem.files = `git ls-files`.split("\n").reject { |fn| fn =~ %r!^spec/|^"spec! } << Narou.create_git_commit_version gem.executables = gem.files.grep(%r!^bin/!).map { |f| File.basename(f) } gem.add_runtime_dependency 'termcolorlight', '~> 1.0', '>= 1.1.1' - gem.add_runtime_dependency 'rubyzip', '~> 2.3', '>= 2.3.2' - gem.add_runtime_dependency 'mail', '~> 2.6.0', '>= 2.6.6' - gem.add_runtime_dependency 'pony', '~> 1', '>= 1.11' - gem.add_runtime_dependency 'diff-lcs', '~> 1.2', '>= 1.2.5' - gem.add_runtime_dependency 'sinatra', '~> 2.0', '>= 2.0.8.1' - gem.add_runtime_dependency 'sinatra-contrib', '~> 2.0', '>= 2.0.8.1' - gem.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.10' + gem.add_runtime_dependency 'rubyzip', '~> 3.2', '>= 3.2.0' + gem.add_runtime_dependency 'mail', '~> 2.9', '>= 2.9.0' + gem.add_runtime_dependency 'pony', '~> 1', '>= 1.13' + gem.add_runtime_dependency 'diff-lcs', '~> 1.6', '>= 1.6.2' + gem.add_runtime_dependency 'sinatra', '~> 4.2', '>= 4.2.0' + gem.add_runtime_dependency 'sinatra-contrib', '~> 4.2', '>= 4.2.0' + gem.add_runtime_dependency 'rackup', '~> 2.1' + gem.add_runtime_dependency 'puma', '~> 6.4' + gem.add_runtime_dependency 'tilt', '~> 2.6', '>= 2.6.1' gem.add_runtime_dependency 'sassc', '~> 2.4' - gem.add_runtime_dependency 'ffi', '~> 1.4', '>= 1.4.2' - gem.add_runtime_dependency 'haml', '>= 5.1.2', '< 6' - gem.add_runtime_dependency 'memoist', '~> 0.11.0' + gem.add_runtime_dependency 'ffi', '~> 1.17', '>= 1.17.2' + gem.add_runtime_dependency 'haml', '>= 5.2.2', '< 6' + gem.add_runtime_dependency 'memoist', '~> 0.16.2' gem.add_runtime_dependency 'systemu', '~> 2.6', '>= 2.6.5' - gem.add_runtime_dependency 'erubi', '~> 1.13' + gem.add_runtime_dependency 'erubi', '~> 1.13.1' gem.add_runtime_dependency 'open_uri_redirections', '~> 0.2', '>= 0.2.1' - gem.add_runtime_dependency 'activesupport', '>= 6.1', '< 8.0' - gem.add_runtime_dependency 'unicode-display_width', '~> 1.4' - gem.add_runtime_dependency 'webrick', '~> 1.7' - gem.add_runtime_dependency 'psych', '~> 4.0' + gem.add_runtime_dependency 'activesupport', '~> 8.0', '>= 8.1.0' + gem.add_runtime_dependency 'unicode-display_width', '~> 3.2' + gem.add_runtime_dependency 'psych', '~> 5.2' gem.add_runtime_dependency 'nkf', '~> 0.2.0' gem.add_runtime_dependency 'csv', '~> 3.3' - gem.add_runtime_dependency 'rexml', '~> 3.2' + gem.add_runtime_dependency 'rexml', '~> 3.4' - gem.add_development_dependency 'rspec', '~> 3.10' + gem.add_development_dependency 'rspec', '~> 3.13' gem.add_development_dependency 'rspec-retry', '~> 0.6' - gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4' + gem.add_development_dependency 'rspec_junit_formatter', '~> 0.6' gem.add_development_dependency 'timecop', '~> 0.9' - gem.add_development_dependency 'pry', '~> 0.12' - gem.add_development_dependency 'pry-byebug', '~> 3.8' - gem.add_development_dependency 'awesome_print', '~> 1.8' - gem.add_development_dependency 'simplecov', '~> 0.20' + gem.add_development_dependency 'pry', '~> 0.15' + gem.add_development_dependency 'pry-byebug', '~> 3.11' + gem.add_development_dependency 'awesome_print', '~> 1.9' + gem.add_development_dependency 'simplecov', '~> 0.22' end From f01f7f605e6045dbd9c57adea287c6ad4682135a Mon Sep 17 00:00:00 2001 From: ponponusa Date: Fri, 24 Oct 2025 01:07:04 +0900 Subject: [PATCH 03/24] Update last_commit_year > 2025 --- lib/narou.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/narou.rb b/lib/narou.rb index 9aa8dc64..884450ae 100644 --- a/lib/narou.rb +++ b/lib/narou.rb @@ -44,7 +44,7 @@ class << self @@is_web = false def last_commit_year - 2024 + 2025 end def root_dir From 5d2ca76b5f344d8b21c6e924dd1f36a55a7396b8 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Fri, 24 Oct 2025 23:28:58 +0900 Subject: [PATCH 04/24] =?UTF-8?q?Update=20jQuery=203.7=20Update=20datatabl?= =?UTF-8?q?e=202.3=20=E5=90=84js=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9?= =?UTF-8?q?=E3=83=AA=E3=82=92=E3=81=A7=E3=81=8D=E3=82=8B=E9=99=90=E3=82=8A?= =?UTF-8?q?=E6=9C=80=E6=96=B0=E3=81=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/web/public/resources/bootbox.js | 985 ++++++++++++++++++++++ lib/web/public/resources/bootbox.min.js | 6 - lib/web/public/resources/narou.library.js | 4 +- lib/web/public/resources/narou.ui.js | 109 ++- lib/web/views/layout.haml | 39 +- 5 files changed, 1092 insertions(+), 51 deletions(-) create mode 100644 lib/web/public/resources/bootbox.js delete mode 100644 lib/web/public/resources/bootbox.min.js diff --git a/lib/web/public/resources/bootbox.js b/lib/web/public/resources/bootbox.js new file mode 100644 index 00000000..d92e2856 --- /dev/null +++ b/lib/web/public/resources/bootbox.js @@ -0,0 +1,985 @@ +/** + * bootbox.js [v4.4.0] + * + * http://bootboxjs.com/license.txt + */ + +// @see https://github.com/makeusabrew/bootbox/issues/180 +// @see https://github.com/makeusabrew/bootbox/issues/186 +(function (root, factory) { + + "use strict"; + if ((typeof define === "function") && define.amd) { + // AMD. Register as an anonymous module. + define(["jquery"], factory); + } else if (typeof exports === "object") { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + // Browser globals (root is window) + root.bootbox = factory(root.jQuery); + } + +}(this, function init($, undefined) { + + "use strict"; + + // the base DOM structure needed to create a modal + var templates = { + dialog: + "", + header: + "", + footer: + "", + closeButton: + "", + form: + "
", + inputs: { + text: + "", + textarea: + "", + email: + "", + select: + "", + checkbox: + "
", + date: + "", + time: + "", + number: + "", + password: + "" + } + }; + + var defaults = { + // default language + locale: "en", + // show backdrop or not. Default to static so user has to interact with dialog + backdrop: "static", + // animate the modal in/out + animate: true, + // additional class string applied to the top level dialog + className: null, + // whether or not to include a close button + closeButton: true, + // show the dialog immediately by default + show: true, + // dialog container + container: "body" + }; + + // our public object; augmented after our private API + var exports = {}; + + /** + * @private + */ + function _t(key) { + var locale = locales[defaults.locale]; + return locale ? locale[key] : locales.en[key]; + } + + function processCallback(e, dialog, callback) { + e.stopPropagation(); + e.preventDefault(); + + // by default we assume a callback will get rid of the dialog, + // although it is given the opportunity to override this + + // so, if the callback can be invoked and it *explicitly returns false* + // then we'll set a flag to keep the dialog active... + var preserveDialog = (typeof callback === "function") && callback.call(dialog, e) === false; + + // ... otherwise we'll bin it + if (!preserveDialog) { + dialog.modal("hide"); + } + } + + function getKeyLength(obj) { + // @TODO defer to Object.keys(x).length if available? + var k, t = 0; + for (k in obj) { + t++; + } + return t; + } + + function each(collection, iterator) { + var index = 0; + $.each(collection, function (key, value) { + iterator(key, value, index++); + }); + } + + function sanitize(options) { + var buttons; + var total; + + if (typeof options !== "object") { + throw new Error("Please supply an object of options"); + } + + if (!options.message) { + throw new Error("Please specify a message"); + } + + // make sure any supplied options take precedence over defaults + options = $.extend({}, defaults, options); + + if (!options.buttons) { + options.buttons = {}; + } + + buttons = options.buttons; + + total = getKeyLength(buttons); + + each(buttons, function (key, button, index) { + + if (typeof button === "function") { + // short form, assume value is our callback. Since button + // isn't an object it isn't a reference either so re-assign it + button = buttons[key] = { + callback: button + }; + } + + // before any further checks make sure by now button is the correct type + if (typeof button !== "object") { + throw new Error("button with key " + key + " must be an object"); + } + + if (!button.label) { + // the lack of an explicit label means we'll assume the key is good enough + button.label = key; + } + + if (!button.className) { + if (total <= 2 && index === total - 1) { + // always add a primary to the main option in a two-button dialog + button.className = "btn-primary"; + } else { + button.className = "btn-default"; + } + } + }); + + return options; + } + + /** + * map a flexible set of arguments into a single returned object + * if args.length is already one just return it, otherwise + * use the properties argument to map the unnamed args to + * object properties + * so in the latter case: + * mapArguments(["foo", $.noop], ["message", "callback"]) + * -> { message: "foo", callback: $.noop } + */ + function mapArguments(args, properties) { + var argn = args.length; + var options = {}; + + if (argn < 1 || argn > 2) { + throw new Error("Invalid argument length"); + } + + if (argn === 2 || typeof args[0] === "string") { + options[properties[0]] = args[0]; + options[properties[1]] = args[1]; + } else { + options = args[0]; + } + + return options; + } + + /** + * merge a set of default dialog options with user supplied arguments + */ + function mergeArguments(defaults, args, properties) { + return $.extend( + // deep merge + true, + // ensure the target is an empty, unreferenced object + {}, + // the base options object for this type of dialog (often just buttons) + defaults, + // args could be an object or array; if it's an array properties will + // map it to a proper options object + mapArguments( + args, + properties + ) + ); + } + + /** + * this entry-level method makes heavy use of composition to take a simple + * range of inputs and return valid options suitable for passing to bootbox.dialog + */ + function mergeDialogOptions(className, labels, properties, args) { + // build up a base set of dialog properties + var baseOptions = { + className: "bootbox-" + className, + buttons: createLabels.apply(null, labels) + }; + + // ensure the buttons properties generated, *after* merging + // with user args are still valid against the supplied labels + return validateButtons( + // merge the generated base properties with user supplied arguments + mergeArguments( + baseOptions, + args, + // if args.length > 1, properties specify how each arg maps to an object key + properties + ), + labels + ); + } + + /** + * from a given list of arguments return a suitable object of button labels + * all this does is normalise the given labels and translate them where possible + * e.g. "ok", "confirm" -> { ok: "OK, cancel: "Annuleren" } + */ + function createLabels() { + var buttons = {}; + + for (var i = 0, j = arguments.length; i < j; i++) { + var argument = arguments[i]; + var key = argument.toLowerCase(); + var value = argument.toUpperCase(); + + buttons[key] = { + label: _t(value) + }; + } + + return buttons; + } + + function validateButtons(options, buttons) { + var allowedButtons = {}; + each(buttons, function (key, value) { + allowedButtons[value] = true; + }); + + each(options.buttons, function (key) { + if (allowedButtons[key] === undefined) { + throw new Error("button key " + key + " is not allowed (options are " + buttons.join("\n") + ")"); + } + }); + + return options; + } + + exports.alert = function () { + var options; + + options = mergeDialogOptions("alert", ["ok"], ["message", "callback"], arguments); + + if (options.callback && !(typeof options.callback === "function")) { + throw new Error("alert requires callback property to be a function when provided"); + } + + /** + * overrides + */ + options.buttons.ok.callback = options.onEscape = function () { + if (typeof options.callback === "function") { + return options.callback.call(this); + } + return true; + }; + + return exports.dialog(options); + }; + + exports.confirm = function () { + var options; + + options = mergeDialogOptions("confirm", ["cancel", "confirm"], ["message", "callback"], arguments); + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, false); + }; + + options.buttons.confirm.callback = function () { + return options.callback.call(this, true); + }; + + // confirm specific validation + if (!(typeof options.callback === "function")) { + throw new Error("confirm requires a callback"); + } + + return exports.dialog(options); + }; + + exports.prompt = function () { + var options; + var defaults; + var dialog; + var form; + var input; + var shouldShow; + var inputOptions; + + // we have to create our form first otherwise + // its value is undefined when gearing up our options + // @TODO this could be solved by allowing message to + // be a function instead... + form = $(templates.form); + + // prompt defaults are more complex than others in that + // users can override more defaults + // @TODO I don't like that prompt has to do a lot of heavy + // lifting which mergeDialogOptions can *almost* support already + // just because of 'value' and 'inputType' - can we refactor? + defaults = { + className: "bootbox-prompt", + buttons: createLabels("cancel", "confirm"), + value: "", + inputType: "text" + }; + + options = validateButtons( + mergeArguments(defaults, arguments, ["title", "callback"]), + ["cancel", "confirm"] + ); + + // capture the user's show value; we always set this to false before + // spawning the dialog to give us a chance to attach some handlers to + // it, but we need to make sure we respect a preference not to show it + shouldShow = (options.show === undefined) ? true : options.show; + + /** + * overrides; undo anything the user tried to set they shouldn't have + */ + options.message = form; + + options.buttons.cancel.callback = options.onEscape = function () { + return options.callback.call(this, null); + }; + + options.buttons.confirm.callback = function () { + var value; + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "select": + case "date": + case "time": + case "number": + case "password": + value = input.val(); + break; + + case "checkbox": + var checkedItems = input.find("input:checked"); + + // we assume that checkboxes are always multiple, + // hence we default to an empty array + value = []; + + each(checkedItems, function (_, item) { + value.push($(item).val()); + }); + break; + } + + return options.callback.call(this, value); + }; + + options.show = false; + + // prompt specific validation + if (!options.title) { + throw new Error("prompt requires a title"); + } + + if (!(typeof options.callback === "function")) { + throw new Error("prompt requires a callback"); + } + + if (!templates.inputs[options.inputType]) { + throw new Error("invalid prompt type"); + } + + // create the input based on the supplied type + input = $(templates.inputs[options.inputType]); + + switch (options.inputType) { + case "text": + case "textarea": + case "email": + case "date": + case "time": + case "number": + case "password": + input.val(options.value); + break; + + case "select": + var groups = {}; + inputOptions = options.inputOptions || []; + + if (!$.isArray(inputOptions)) { + throw new Error("Please pass an array of input options"); + } + + if (!inputOptions.length) { + throw new Error("prompt with select requires options"); + } + + each(inputOptions, function (_, option) { + + // assume the element to attach to is the input... + var elem = input; + + if (option.value === undefined || option.text === undefined) { + throw new Error("given options in wrong format"); + } + + // ... but override that element if this option sits in a group + + if (option.group) { + // initialise group if necessary + if (!groups[option.group]) { + groups[option.group] = $("").attr("label", option.group); + } + + elem = groups[option.group]; + } + + elem.append(""); + }); + + each(groups, function (_, group) { + input.append(group); + }); + + // safe to set a select's value as per a normal input + input.val(options.value); + break; + + case "checkbox": + var values = $.isArray(options.value) ? options.value : [options.value]; + inputOptions = options.inputOptions || []; + + if (!inputOptions.length) { + throw new Error("prompt with checkbox requires options"); + } + + if (!inputOptions[0].value || !inputOptions[0].text) { + throw new Error("given options in wrong format"); + } + + // checkboxes have to nest within a containing element, so + // they break the rules a bit and we end up re-assigning + // our 'input' element to this container instead + input = $("
"); + + each(inputOptions, function (_, option) { + var checkbox = $(templates.inputs[options.inputType]); + + checkbox.find("input").attr("value", option.value); + checkbox.find("label").append(option.text); + + // we've ensured values is an array so we can always iterate over it + each(values, function (_, value) { + if (value === option.value) { + checkbox.find("input").prop("checked", true); + } + }); + + input.append(checkbox); + }); + break; + } + + // @TODO provide an attributes option instead + // and simply map that as keys: vals + if (options.placeholder) { + input.attr("placeholder", options.placeholder); + } + + if (options.pattern) { + input.attr("pattern", options.pattern); + } + + if (options.maxlength) { + input.attr("maxlength", options.maxlength); + } + + // now place it in our form + form.append(input); + + form.on("submit", function (e) { + e.preventDefault(); + // Fix for SammyJS (or similar JS routing library) hijacking the form post. + e.stopPropagation(); + // @TODO can we actually click *the* button object instead? + // e.g. buttons.confirm.click() or similar + dialog.find(".btn-primary").click(); + }); + + dialog = exports.dialog(options); + + // clear the existing handler focusing the submit button... + dialog.off("shown.bs.modal"); + + // ...and replace it with one focusing our input, if possible + dialog.on("shown.bs.modal", function () { + // need the closure here since input isn't + // an object otherwise + input.on("focus"); + }); + + if (shouldShow === true) { + dialog.modal("show"); + } + + return dialog; + }; + + exports.dialog = function (options) { + options = sanitize(options); + + var dialog = $(templates.dialog); + var innerDialog = dialog.find(".modal-dialog"); + var body = dialog.find(".modal-body"); + var buttons = options.buttons; + var buttonStr = ""; + var callbacks = { + onEscape: options.onEscape + }; + + if ($.fn.modal === undefined) { + throw new Error( + "$.fn.modal is not defined; please double check you have included " + + "the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ " + + "for more details." + ); + } + + each(buttons, function (key, button) { + + // @TODO I don't like this string appending to itself; bit dirty. Needs reworking + // can we just build up button elements instead? slower but neater. Then button + // can just become a template too + buttonStr += ""; + callbacks[key] = button.callback; + }); + + body.find(".bootbox-body").html(options.message); + + if (options.animate === true) { + dialog.addClass("fade"); + } + + if (options.className) { + dialog.addClass(options.className); + } + + if (options.size === "large") { + innerDialog.addClass("modal-lg"); + } else if (options.size === "small") { + innerDialog.addClass("modal-sm"); + } + + if (options.title) { + body.before(templates.header); + } + + if (options.closeButton) { + var closeButton = $(templates.closeButton); + + if (options.title) { + dialog.find(".modal-header").prepend(closeButton); + } else { + closeButton.css("margin-top", "-10px").prependTo(body); + } + } + + if (options.title) { + dialog.find(".modal-title").html(options.title); + } + + if (buttonStr.length) { + body.after(templates.footer); + dialog.find(".modal-footer").html(buttonStr); + } + + + /** + * Bootstrap event listeners; used handle extra + * setup & teardown required after the underlying + * modal has performed certain actions + */ + + dialog.on("hidden.bs.modal", function (e) { + // ensure we don't accidentally intercept hidden events triggered + // by children of the current dialog. We shouldn't anymore now BS + // namespaces its events; but still worth doing + if (e.target === this) { + dialog.remove(); + } + }); + + /* + dialog.on("show.bs.modal", function() { + // sadly this doesn't work; show is called *just* before + // the backdrop is added so we'd need a setTimeout hack or + // otherwise... leaving in as would be nice + if (options.backdrop) { + dialog.next(".modal-backdrop").addClass("bootbox-backdrop"); + } + }); + */ + + dialog.on("shown.bs.modal", function () { + dialog.find(".btn-primary:first").on("focus"); + }); + + /** + * Bootbox event listeners; experimental and may not last + * just an attempt to decouple some behaviours from their + * respective triggers + */ + + if (options.backdrop !== "static") { + // A boolean true/false according to the Bootstrap docs + // should show a dialog the user can dismiss by clicking on + // the background. + // We always only ever pass static/false to the actual + // $.modal function because with `true` we can't trap + // this event (the .modal-backdrop swallows it) + // However, we still want to sort of respect true + // and invoke the escape mechanism instead + dialog.on("click.dismiss.bs.modal", function (e) { + // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop + // moved *inside* the outer dialog rather than *alongside* it + if (dialog.children(".modal-backdrop").length) { + e.currentTarget = dialog.children(".modal-backdrop").get(0); + } + + if (e.target !== e.currentTarget) { + return; + } + + dialog.trigger("escape.close.bb"); + }); + } + + dialog.on("escape.close.bb", function (e) { + if (callbacks.onEscape) { + processCallback(e, dialog, callbacks.onEscape); + } + }); + + /** + * Standard jQuery event listeners; used to handle user + * interaction with our dialog + */ + + dialog.on("click", ".modal-footer button", function (e) { + var callbackKey = $(this).data("bb-handler"); + + processCallback(e, dialog, callbacks[callbackKey]); + }); + + dialog.on("click", ".bootbox-close-button", function (e) { + // onEscape might be falsy but that's fine; the fact is + // if the user has managed to click the close button we + // have to close the dialog, callback or not + processCallback(e, dialog, callbacks.onEscape); + }); + + dialog.on("keyup", function (e) { + if (e.which === 27) { + dialog.trigger("escape.close.bb"); + } + }); + + // the remainder of this method simply deals with adding our + // dialogent to the DOM, augmenting it with Bootstrap's modal + // functionality and then giving the resulting object back + // to our caller + + $(options.container).append(dialog); + + dialog.modal({ + backdrop: options.backdrop ? "static" : false, + keyboard: false, + show: false + }); + + if (options.show) { + dialog.modal("show"); + } + + // @TODO should we return the raw element here or should + // we wrap it in an object on which we can expose some neater + // methods, e.g. var d = bootbox.alert(); d.hide(); instead + // of d.modal("hide"); + + /* + function BBDialog(elem) { + this.elem = elem; + } + + BBDialog.prototype = { + hide: function() { + return this.elem.modal("hide"); + }, + show: function() { + return this.elem.modal("show"); + } + }; + */ + + return dialog; + + }; + + exports.setDefaults = function () { + var values = {}; + + if (arguments.length === 2) { + // allow passing of single key/value... + values[arguments[0]] = arguments[1]; + } else { + // ... and as an object too + values = arguments[0]; + } + + $.extend(defaults, values); + }; + + exports.hideAll = function () { + $(".bootbox").modal("hide"); + + return exports; + }; + + + /** + * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are + * unlikely to be required. If this gets too large it can be split out into separate JS files. + */ + var locales = { + bg_BG: { + OK: "Ок", + CANCEL: "Отказ", + CONFIRM: "Потвърждавам" + }, + br: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Sim" + }, + cs: { + OK: "OK", + CANCEL: "Zrušit", + CONFIRM: "Potvrdit" + }, + da: { + OK: "OK", + CANCEL: "Annuller", + CONFIRM: "Accepter" + }, + de: { + OK: "OK", + CANCEL: "Abbrechen", + CONFIRM: "Akzeptieren" + }, + el: { + OK: "Εντάξει", + CANCEL: "Ακύρωση", + CONFIRM: "Επιβεβαίωση" + }, + en: { + OK: "OK", + CANCEL: "Cancel", + CONFIRM: "OK" + }, + es: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Aceptar" + }, + et: { + OK: "OK", + CANCEL: "Katkesta", + CONFIRM: "OK" + }, + fa: { + OK: "قبول", + CANCEL: "لغو", + CONFIRM: "تایید" + }, + fi: { + OK: "OK", + CANCEL: "Peruuta", + CONFIRM: "OK" + }, + fr: { + OK: "OK", + CANCEL: "Annuler", + CONFIRM: "D'accord" + }, + he: { + OK: "אישור", + CANCEL: "ביטול", + CONFIRM: "אישור" + }, + hu: { + OK: "OK", + CANCEL: "Mégsem", + CONFIRM: "Megerősít" + }, + hr: { + OK: "OK", + CANCEL: "Odustani", + CONFIRM: "Potvrdi" + }, + id: { + OK: "OK", + CANCEL: "Batal", + CONFIRM: "OK" + }, + it: { + OK: "OK", + CANCEL: "Annulla", + CONFIRM: "Conferma" + }, + ja: { + OK: "OK", + CANCEL: "キャンセル", + CONFIRM: "確認" + }, + lt: { + OK: "Gerai", + CANCEL: "Atšaukti", + CONFIRM: "Patvirtinti" + }, + lv: { + OK: "Labi", + CANCEL: "Atcelt", + CONFIRM: "Apstiprināt" + }, + nl: { + OK: "OK", + CANCEL: "Annuleren", + CONFIRM: "Accepteren" + }, + no: { + OK: "OK", + CANCEL: "Avbryt", + CONFIRM: "OK" + }, + pl: { + OK: "OK", + CANCEL: "Anuluj", + CONFIRM: "Potwierdź" + }, + pt: { + OK: "OK", + CANCEL: "Cancelar", + CONFIRM: "Confirmar" + }, + ru: { + OK: "OK", + CANCEL: "Отмена", + CONFIRM: "Применить" + }, + sq: { + OK: "OK", + CANCEL: "Anulo", + CONFIRM: "Prano" + }, + sv: { + OK: "OK", + CANCEL: "Avbryt", + CONFIRM: "OK" + }, + th: { + OK: "ตกลง", + CANCEL: "ยกเลิก", + CONFIRM: "ยืนยัน" + }, + tr: { + OK: "Tamam", + CANCEL: "İptal", + CONFIRM: "Onayla" + }, + zh_CN: { + OK: "OK", + CANCEL: "取消", + CONFIRM: "确认" + }, + zh_TW: { + OK: "OK", + CANCEL: "取消", + CONFIRM: "確認" + } + }; + + exports.addLocale = function (name, values) { + $.each(["OK", "CANCEL", "CONFIRM"], function (_, v) { + if (!values[v]) { + throw new Error("Please supply a translation for '" + v + "'"); + } + }); + + locales[name] = { + OK: values.OK, + CANCEL: values.CANCEL, + CONFIRM: values.CONFIRM + }; + + return exports; + }; + + exports.removeLocale = function (name) { + delete locales[name]; + + return exports; + }; + + exports.setLocale = function (name) { + return exports.setDefaults("locale", name); + }; + + exports.init = function (_$) { + return init(_$ || $); + }; + + return exports; +})); \ No newline at end of file diff --git a/lib/web/public/resources/bootbox.min.js b/lib/web/public/resources/bootbox.min.js deleted file mode 100644 index 134660a2..00000000 --- a/lib/web/public/resources/bootbox.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * bootbox.js v4.3.0 - * - * http://bootboxjs.com/license.txt - */ -!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d(a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:!0,animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback():!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback(!1)},a.buttons.confirm.callback=function(){return a.callback(!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback(null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback(c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var c=b(n.dialog),d=c.find(".modal-dialog"),f=c.find(".modal-body"),i=a.buttons,j="",k={onEscape:a.onEscape};if(g(i,function(a,b){j+="",k[a]=b.callback}),f.find(".bootbox-body").html(a.message),a.animate===!0&&c.addClass("fade"),a.className&&c.addClass(a.className),"large"===a.size&&d.addClass("modal-lg"),"small"===a.size&&d.addClass("modal-sm"),a.title&&f.before(n.header),a.closeButton){var l=b(n.closeButton);a.title?c.find(".modal-header").prepend(l):l.css("margin-top","-10px").prependTo(f)}return a.title&&c.find(".modal-title").html(a.title),j.length&&(f.after(n.footer),c.find(".modal-footer").html(j)),c.on("hidden.bs.modal",function(a){a.target===this&&c.remove()}),c.on("shown.bs.modal",function(){c.find(".btn-primary:first").focus()}),c.on("escape.close.bb",function(a){k.onEscape&&e(a,c,k.onEscape)}),c.on("click",".modal-footer button",function(a){var d=b(this).data("bb-handler");e(a,c,k[d])}),c.on("click",".bootbox-close-button",function(a){e(a,c,k.onEscape)}),c.on("keyup",function(a){27===a.which&&c.trigger("escape.close.bb")}),b(a.container).append(c),c.modal({backdrop:a.backdrop,keyboard:!1,show:!1}),a.show&&c.modal("show"),c},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"OK"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.init=function(c){return a(c||b)},p}); diff --git a/lib/web/public/resources/narou.library.js b/lib/web/public/resources/narou.library.js index e94aaf29..e7a70e3e 100644 --- a/lib/web/public/resources/narou.library.js +++ b/lib/web/public/resources/narou.library.js @@ -15,7 +15,7 @@ var Narou = (function () { }); // jQuery はデフォルトだと dataTransfer オブジェクトをコピーしないので - $.event.props.push("dataTransfer"); + $.event.fixHooks['drop'] = { props: [ 'dataTransfer' ] }; /************************************************************************* * ローカルストレージ @@ -917,7 +917,7 @@ var Narou = (function () { } }); download_modal.one("shown.bs.modal", function () { - $("#download-input").focus(); + $("#download-input").on("focus"); }); }); } diff --git a/lib/web/public/resources/narou.ui.js b/lib/web/public/resources/narou.ui.js index b4a046cc..1ee8cdd0 100644 --- a/lib/web/public/resources/narou.ui.js +++ b/lib/web/public/resources/narou.ui.js @@ -1448,7 +1448,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // 解除 $("#rect-select-menu-clear").on("click", function(e) { @@ -1470,7 +1470,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // 反転 $("#rect-select-menu-reverse").on("click", function(e) { @@ -1497,7 +1497,7 @@ $(function() { table.fireChangeSelect(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); // キャンセル $("#rect-select-menu-cancel").on("click", function(e) { @@ -1505,7 +1505,7 @@ $(function() { e.stopPropagation(); menu.hide(); selected_rect_element.hide(); - $(this).children("a").blur(); + $(this).children("a").on("blur"); }); }; rect_select_menu_initialize(); @@ -1566,7 +1566,7 @@ $(function() { table.fireChangeSelect(); $("#rect-select-menu").hide(); selected_rect_element.hide(); - $(".n-popover").blur(); + $(".n-popover").on("blur"); }; if (not_clicked_yet) { // 範囲選択開始 @@ -1651,34 +1651,95 @@ $(function() { }); /* - * メニュー - * 表示>表示する項目を設定 - */ - var colvis = new $.fn.dataTable.ColVis(table, { - restore: "元に戻す", - showAll: "全ての項目を表示", - showNone: "全て隠す", - bCssPosition: true, - overlayFade: 300, - exclude: [ "title", "frozen", "new_arrivals_date" ], + * メニュー + * 表示>表示する項目を設定 + */ + // DataTables APIインスタンスを取得 + var dt = (table instanceof $.fn.dataTable.Api) ? table : $(table).DataTable(); + + // Buttonsインスタンスを構築 + var buttons = new $.fn.dataTable.Buttons(dt, { + buttons: [ + { + extend: 'colvis', + text: '表示する項目を設定', + + // ColVisの exclude 相当: + // このセレクタに含まれる列だけ列表示/非表示の対象にする + // => 除外したい列には列定義側で className: 'title' 等を付けておく + columns: ':not(.title):not(.frozen):not(.new_arrivals_date)', + + // メニュー内で各列にどういう名前を出すか + columnText: function (dtApi, columnIdx, columnTitle) { + return columnTitle || ('列 ' + (columnIdx + 1)); + }, + + // "全て表示 / 全て隠す / 元に戻す" をメニュー先頭に追加 + prefixButtons: [ + { + text: '全ての項目を表示', + action: function (e, dtApi) { + dtApi.columns().visible(true); + } + }, + { + text: '全て隠す', + action: function (e, dtApi) { + dtApi.columns().visible(false); + } + }, + { + text: '元に戻す', + extend: 'colvisRestore' + } + ] + } + ] + }); + + // ButtonsのDOMコンテナを一旦bodyに追加しておく + // (存在しないと trigger('click') で動かないことがあるため) + var $btnContainer = $(buttons.container()); + $btnContainer.css({ + position: 'absolute', + left: '-9999px', + top: '-9999px' }); + $('body').append($btnContainer); + + // colvisボタンDOMを取得 + var $colvisButton = $btnContainer.find('button.buttons-colvis'); + + // 既存のクリックハンドラを書き換え $("#action-view-setting").on(click_event_name, function(e) { e.preventDefault(); + var pos = {}; if (touchable_device) { var target = $(this); pos.x = target.offset().left; pos.y = target.offset().top + target.outerHeight(); + } else { + pos = Narou.get_event_position(e); // 既存ヘルパーで {x, y} が返る想定 } - else { - pos = Narou.get_event_position(e); - } - $(colvis.dom.collection).css({ - position: "absolute", - left: pos.x, top: pos.y + + slideNavbar.slide(); // 既存動作 + + // colvisメニューを開かせる + $colvisButton.trigger('click'); + + // DataTables Buttons が生成したドロップダウン(コレクション)を取得 + var $collection = $('.dt-button-collection').last(); + + // 旧ColVisの colvis._fnCollectionShow() 相当: + // メニューをクリック位置にフローティング表示 + $collection.css({ + position: 'absolute', + left: pos.x, + top: pos.y, + display: 'block', + zIndex: 2000 // 必要に応じて調整(Bootstrapの.navbarやモーダルとの重なり用) }); - slideNavbar.slide(); - colvis._fnCollectionShow(); }); /* @@ -2022,7 +2083,7 @@ Mac スタイル:
メニューが画面外にはみ出そうとしたら、 * ボタンを一時的に非アクティブ化 */ function disableButtonMoment($button) { - $button.blur(); + $button.on("blur"); $button.tooltip("hide"); $button.prop("disabled", true); // 少したったらアクティブに戻す diff --git a/lib/web/views/layout.haml b/lib/web/views/layout.haml index 845d82e2..9295dd2d 100644 --- a/lib/web/views/layout.haml +++ b/lib/web/views/layout.haml @@ -13,19 +13,16 @@ %link{:href => "/theme/#{@bootstrap_theme}/bootstrap.min.css", :rel => "stylesheet"}/ %link{:href => "/theme/#{@bootstrap_theme}/style.css", :rel => "stylesheet"}/ - else - %link{:href => "//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css", :rel => "stylesheet"}/ %link{:href => "/resources/default-style.css?_=#{Narou::VERSION}", :rel => "stylesheet"}/ - %link{:href => "//cdn.datatables.net/plug-ins/725b2a2115b/integration/bootstrap/3/dataTables.bootstrap.css", :rel => "stylesheet"}/ - %link{:href => "//cdn.datatables.net/colreorder/1.1.2/css/dataTables.colReorder.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/2.3.4/css/dataTables.bootstrap.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/colreorder/2.1.2/css/colReorder.bootstrap.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.datatables.net/buttons/3.2.5/css/buttons.bootstrap.min.css", :rel => "stylesheet"} %link{:href => "//cdn.datatables.net/colvis/1.1.1/css/dataTables.colVis.css", :rel => "stylesheet"}/ - %link{:href => "//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.6.5/css/bootstrap-select.min.css", :rel => "stylesheet"}/ - %link{:href => "/resources/perfect-scrollbar.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/dist/css/bootstrap-select.min.css", :rel => "stylesheet"}/ + %link{:href => "//cdn.jsdelivr.net/npm/perfect-scrollbar@1.5.6/css/perfect-scrollbar.min.css", :rel => "stylesheet"}/ %link{:href => "/resources/toggle-switch.css", :rel => "stylesheet"}/ %link{:href => "/style.css?_=#{Narou::VERSION}", :rel => "stylesheet"}/ - / HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries - /[if lt IE 9] - - :javascript var local_initialize_functions = []; var slideNavbar; @@ -40,18 +37,22 @@ &= message != yield = partial :move_to_top - %script{:src => "//code.jquery.com/jquery-1.11.1.min.js"} - %script{:src => "//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"} - %script{:src => "//cdn.datatables.net/1.10.10/js/jquery.dataTables.min.js"} - %script{:src => "//cdn.datatables.net/plug-ins/725b2a2115b/integration/bootstrap/3/dataTables.bootstrap.js"} - %script{:src => "//cdn.datatables.net/colreorder/1.3.0/js/dataTables.colReorder.min.js"} - %script{:src => "//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.6.5/js/bootstrap-select.min.js"} - %script{:src => "//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/jquery-migrate@3.5.2/dist/jquery-migrate.js"} + %script{:src => "//cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/js/bootstrap.min.js"} + %script{:src => "//cdn.datatables.net/2.3.4/js/dataTables.min.js"} + %script{:src => "//cdn.datatables.net/2.3.4/js/dataTables.bootstrap.min.js"} + %script{:src => "//cdn.datatables.net/colreorder/2.1.2/js/dataTables.colReorder.min.js"} + %script{:src => "//cdn.datatables.net/colreorder/2.1.2/js/colReorder.bootstrap.min.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/dataTables.buttons.min.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/buttons.bootstrap.min.js"} + %script{:src => "//cdn.datatables.net/buttons/3.2.5/js/buttons.colVis.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/bootstrap-select@1.13.18/js/bootstrap-select.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"} + %script{:src => "//cdn.jsdelivr.net/npm/perfect-scrollbar@0.8.1/dist/js/perfect-scrollbar.jquery.min.js"} + %script{:src => "/resources/bootbox.js"} %script{:src => "/resources/sprintf.js"} - %script{:src => "/resources/dataTables.colVis.js"} - %script{:src => "/resources/bootbox.min.js"} %script{:src => "/resources/shortcut.js"} - %script{:src => "/resources/perfect-scrollbar.min.js"} %script{:src => "/resources/jquery.outerclick.js"} %script{:src => "/resources/jquery.slidenavbar.js"} %script{:src => "/resources/jquery.moveto.js"} From 99006221b9c1207c11ca11fdbc3fa2693fed8fff Mon Sep 17 00:00:00 2001 From: ponponusa Date: Sat, 25 Oct 2025 00:40:21 +0900 Subject: [PATCH 05/24] =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B?= =?UTF-8?q?=E9=A0=85=E7=9B=AE=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=8C=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3?= =?UTF-8?q?=E3=81=9F=E3=81=AE=E3=81=A7=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/web/public/resources/narou.ui.js | 69 +++++++++++----------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/lib/web/public/resources/narou.ui.js b/lib/web/public/resources/narou.ui.js index 1ee8cdd0..cc461b80 100644 --- a/lib/web/public/resources/narou.ui.js +++ b/lib/web/public/resources/narou.ui.js @@ -1654,65 +1654,58 @@ $(function() { * メニュー * 表示>表示する項目を設定 */ - // DataTables APIインスタンスを取得 + // DataTableのAPIインスタンスを生成 var dt = (table instanceof $.fn.dataTable.Api) ? table : $(table).DataTable(); - // Buttonsインスタンスを構築 - var buttons = new $.fn.dataTable.Buttons(dt, { + // Buttonsのcolvisグループを定義 + new $.fn.dataTable.Buttons(dt, { + name: 'colvisGroup', buttons: [ { extend: 'colvis', text: '表示する項目を設定', - - // ColVisの exclude 相当: - // このセレクタに含まれる列だけ列表示/非表示の対象にする - // => 除外したい列には列定義側で className: 'title' 等を付けておく columns: ':not(.title):not(.frozen):not(.new_arrivals_date)', - - // メニュー内で各列にどういう名前を出すか columnText: function (dtApi, columnIdx, columnTitle) { return columnTitle || ('列 ' + (columnIdx + 1)); }, - - // "全て表示 / 全て隠す / 元に戻す" をメニュー先頭に追加 prefixButtons: [ { text: '全ての項目を表示', - action: function (e, dtApi) { - dtApi.columns().visible(true); + action: function (e, api) { + api.columns().visible(true); } }, { text: '全て隠す', - action: function (e, dtApi) { - dtApi.columns().visible(false); + action: function (e, api) { + api.columns().visible(false); } }, { - text: '元に戻す', - extend: 'colvisRestore' + extend: 'colvisRestore', + text: '元に戻す' } ] } ] }); - // ButtonsのDOMコンテナを一旦bodyに追加しておく - // (存在しないと trigger('click') で動かないことがあるため) - var $btnContainer = $(buttons.container()); + // ButtonsコンテナをDOMに追加 + var $btnContainer = dt.buttons('colvisGroup', null).container(); + $('body').append($btnContainer); $btnContainer.css({ - position: 'absolute', - left: '-9999px', - top: '-9999px' + visibility: 'hidden', + pointerEvents: 'none', + width: 0, + height: 0, + overflow: 'hidden' }); - $('body').append($btnContainer); - // colvisボタンDOMを取得 - var $colvisButton = $btnContainer.find('button.buttons-colvis'); - - // 既存のクリックハンドラを書き換え - $("#action-view-setting").on(click_event_name, function(e) { + // 表示メニューからcolvisメニューを開く + $('#action-view-setting').on(click_event_name, function(e) { e.preventDefault(); + slideNavbar.slide(); + dt.buttons('.buttons-colvis').trigger(); var pos = {}; if (touchable_device) { @@ -1720,25 +1713,19 @@ $(function() { pos.x = target.offset().left; pos.y = target.offset().top + target.outerHeight(); } else { - pos = Narou.get_event_position(e); // 既存ヘルパーで {x, y} が返る想定 + pos = Narou.get_event_position(e); // { x:..., y:... } } - slideNavbar.slide(); // 既存動作 - - // colvisメニューを開かせる - $colvisButton.trigger('click'); - - // DataTables Buttons が生成したドロップダウン(コレクション)を取得 - var $collection = $('.dt-button-collection').last(); - - // 旧ColVisの colvis._fnCollectionShow() 相当: - // メニューをクリック位置にフローティング表示 + // メニュー(.dt-button-collection)の配置と表示 + var $collection = $btnContainer.find('.dt-button-collection').last(); $collection.css({ position: 'absolute', left: pos.x, top: pos.y, display: 'block', - zIndex: 2000 // 必要に応じて調整(Bootstrapの.navbarやモーダルとの重なり用) + visibility: 'visible', + pointerEvents: 'auto', + zIndex: 2000 }); }); From e4f5b6d1be237bd5175c30b4d363374e24d87376 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Sat, 25 Oct 2025 00:45:40 +0900 Subject: [PATCH 06/24] =?UTF-8?q?docker=E4=B8=8A=E3=81=A7=E3=81=AE?= =?UTF-8?q?=E5=8B=95=E4=BD=9C=E5=88=A4=E5=AE=9A=E3=82=92OS=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=81=AE=E5=85=88=E9=A0=AD=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/device.rb | 6 +++--- lib/helper.rb | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/device.rb b/lib/device.rb index f885ad78..fbaf091d 100644 --- a/lib/device.rb +++ b/lib/device.rb @@ -24,6 +24,9 @@ def get_storage_path end case Helper.determine_os + when :docker + require_relative "device/library/docker" + extend Device::Library::Docker when :windows require_relative "device/library/windows" extend Device::Library::Windows @@ -33,9 +36,6 @@ def get_storage_path when :mac require_relative "device/library/mac" extend Device::Library::Mac - when :docker - require_relative "device/library/docker" - extend Device::Library::Docker else require_relative "device/library/linux" extend Device::Library::Linux diff --git a/lib/helper.rb b/lib/helper.rb index 26693cc5..8c4c7edc 100644 --- a/lib/helper.rb +++ b/lib/helper.rb @@ -18,6 +18,11 @@ module Helper FILENAME_LENGTH_LIMIT = 50 FOLDER_LENGTH_LIMIT = 50 + def in_docker? + return false unless File.exist?('/proc/1/cgroup') + File.readlines('/proc/1/cgroup').any? { |line| line.include?('/docker/') || line.include?('/lxc/') } + end + def os_windows? @@os_is_windows ||= HOST_OS =~ /mswin(?!ce)|mingw|bccwin/i end @@ -30,21 +35,16 @@ def os_cygwin? @@os_is_cygwin ||= HOST_OS =~ /cygwin/i end - def running_in_docker? - return false unless File.exist?('/proc/1/cgroup') - File.readlines('/proc/1/cgroup').any? { |line| line.include?('/docker/') || line.include?('/lxc/') } - end - def determine_os case + when in_docker? + :docker when os_windows? :windows when os_mac? :mac when os_cygwin? :cygwin - when running_in_docker? - :docker else :other end From 528bd01320bb484206d524462febe12587df09a0 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Sat, 25 Oct 2025 17:33:45 +0900 Subject: [PATCH 07/24] Securty fix - Use of `Kernel.open`, `IO.read` or similar sinks with user-controlled input - Incomplete multi-character sanitization - Incomplete string escaping or encoding --- Gemfile.lock | 15 ++++++++++++++- lib/command/list/novel_decorator.rb | 2 +- lib/device/ibunko.rb | 6 +++++- lib/device/library/linux.rb | 2 +- lib/downloader.rb | 5 +++-- lib/extensions/jruby.rb | 4 ++-- lib/html.rb | 7 ++++++- lib/illustration.rb | 2 +- lib/ini.rb | 2 +- lib/inspector.rb | 2 +- lib/novelconverter.rb | 2 +- narou.gemspec | 1 + 12 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6ffe68ed..2d8ec1b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - narou (3.9.1.20251023a) + narou (3.9.1.20251024d) activesupport (~> 8.0, >= 8.1.0) csv (~> 3.3) diff-lcs (~> 1.6, >= 1.6.2) @@ -18,6 +18,7 @@ PATH rackup (~> 2.1) rexml (~> 3.4) rubyzip (~> 3.2, >= 3.2.0) + sanitize (~> 7.0.0) sassc (~> 2.4) sinatra (~> 4.2, >= 4.2.0) sinatra-contrib (~> 4.2, >= 4.2.0) @@ -50,6 +51,7 @@ GEM coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.4) + crass (1.0.6) csv (3.3.5) date (3.4.1) date (3.4.1-java) @@ -77,6 +79,7 @@ GEM memoist (0.16.2) method_source (1.0.0) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (5.14.4) multi_json (1.15.0) mustermann (3.0.4) @@ -94,6 +97,11 @@ GEM nio4r (2.7.4-java) nkf (0.2.0) nkf (0.2.0-java) + nokogiri (1.18.10) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.18.10-java) + racc (~> 1.4) open_uri_redirections (0.2.1) pony (1.13.1) mail (>= 2.0) @@ -117,6 +125,8 @@ GEM nio4r (~> 2.0) puma (6.6.1-java) nio4r (~> 2.0) + racc (1.8.1) + racc (1.8.1-java) rack (3.2.3) rack-protection (4.2.1) base64 (>= 0.1.0) @@ -147,6 +157,9 @@ GEM rspec-core (>= 2, < 4, != 2.12.0) ruby2_keywords (0.0.5) rubyzip (3.2.0) + sanitize (7.0.0) + crass (~> 1.0.2) + nokogiri (>= 1.16.8) sassc (2.4.0) ffi (~> 1.9) securerandom (0.4.1) diff --git a/lib/command/list/novel_decorator.rb b/lib/command/list/novel_decorator.rb index 057e96a9..44a60982 100644 --- a/lib/command/list/novel_decorator.rb +++ b/lib/command/list/novel_decorator.rb @@ -21,7 +21,7 @@ def initialize(novel, parent) def decorate_id disp_id = ((frozen ? "*" : "") + id.to_s).rjust(4) if frozen - disp_id.sub("*", "*") + disp_id.gsub("*", "*") else disp_id end diff --git a/lib/device/ibunko.rb b/lib/device/ibunko.rb index fa0a9410..2c51ba6e 100644 --- a/lib/device/ibunko.rb +++ b/lib/device/ibunko.rb @@ -33,7 +33,11 @@ def create_pure_aozora_zip data = File.read(@converted_txt_path, encoding: Encoding::UTF_8) # EPUB最適化のために混入しうるHTML/タグ類を汎用的に除去 # 先に汎用HTMLを除去してから、青空注記→i文庫カスタムタグへの変換を行う - data.gsub!(%r{]+>}, "") + previous = nil + while data != previous + previous = data + data = data.gsub(%r{]+>}, "") + end # HTMLエンティティは実体に復号 data = Helper.restore_entity(data) # 青空注記 → i文庫HDカスタムタグへ変換 diff --git a/lib/device/library/linux.rb b/lib/device/library/linux.rb index 0017be65..446d7519 100644 --- a/lib/device/library/linux.rb +++ b/lib/device/library/linux.rb @@ -32,7 +32,7 @@ def device_mount_info(volume_name) raise Device::CantEject, "端末が接続されていません" unless device_root pattern = %r!^(/dev/[^ ]+) .* #{device_root} .*\Wuhelper=(\w+)! - open("|mount") do |io| + File.open("|mount") do |io| while line = io.gets if line =~ pattern return [$1, $2] diff --git a/lib/downloader.rb b/lib/downloader.rb index 9d9fc0a2..8de15006 100644 --- a/lib/downloader.rb +++ b/lib/downloader.rb @@ -7,6 +7,7 @@ require "yaml" require "fileutils" require "ostruct" +require "sanitize" require_relative "narou" require_relative "helper" require_relative "sitesetting" @@ -419,7 +420,7 @@ def run_download auto_add_tags = Inventory.load("local_setting")["auto-add-tags"] if @setting["tag"] && auto_add_tags - clean_tag = @setting["tag"].gsub(/<[^>]*>/, '').gsub(/キーワードが設定されていません/, '').gsub(/キーワード/, '').gsub(/\"?\(\?\.\+\?\)\"?/, '').gsub(/\(\?\ 0 new_tags = clean_tag.split(/[  ]+| /).uniq old_tags = (record && record["tags"]) ? record["tags"] : [] @@ -644,7 +645,7 @@ def update_database(suspend: false) } auto_add_tags = Inventory.load("local_setting")["auto-add-tags"] if @setting["tag"] && auto_add_tags - clean_tag = @setting["tag"].gsub(/<[^>]*>/, '').gsub(/キーワード/, '').gsub(/\"?\(\?\.\+\?\)\"?/, '').gsub(/\(\?\ 0 tags = clean_tag.split(/[  ]+| /) if record && record["tags"] diff --git a/lib/extensions/jruby.rb b/lib/extensions/jruby.rb index 29ba08f4..bc70c0a1 100644 --- a/lib/extensions/jruby.rb +++ b/lib/extensions/jruby.rb @@ -10,7 +10,7 @@ module FileUtils # マルチバイト文字を含むパスを認識出来ないため def self.cp(src, dst, opt = nil) - open(src, "rb") do |fp| + File.open(src, "rb") do |fp| File.binwrite(dst, fp.read) end end @@ -19,7 +19,7 @@ def self.cp(src, dst, opt = nil) class File # 何故かエンコーディングエラーが出るため def self.binwrite(path, data) - open(path, "wb") do |fp| + File.open(path, "wb") do |fp| fp.write(data) end end diff --git a/lib/html.rb b/lib/html.rb index 3ed428eb..9bd78091 100644 --- a/lib/html.rb +++ b/lib/html.rb @@ -59,7 +59,12 @@ def to_aozora(pre_html: false) end def delete_tag(text = @string) - text.gsub(/<.+?>/, "") + previous = nil + while text != previous + previous = text + text = text.gsub(/<.+?>/, "") + end + text end def br_to_aozora(text = @string) diff --git a/lib/illustration.rb b/lib/illustration.rb index cc0e450d..74347eb0 100644 --- a/lib/illustration.rb +++ b/lib/illustration.rb @@ -59,7 +59,7 @@ def download_image(url, basename = nil) content_type = fp.meta["content-type"] ext = MIME[content_type] or raise UnknownMIMEType, content_type illust_abs_path = create_illust_path(basename) + "." + ext - open(illust_abs_path, "wb") do |write_fp| + File.open(illust_abs_path, "wb") do |write_fp| write_fp.write(fp.read) end @inspector.info("挿絵「#{File.basename(illust_abs_path)}」を保存しました。") diff --git a/lib/ini.rb b/lib/ini.rb index 15833f34..2ed096cb 100644 --- a/lib/ini.rb +++ b/lib/ini.rb @@ -103,7 +103,7 @@ def save(filename = nil) unless filename raise NoFilenameError end - open(@filename, "w") do |fp| + File.open(@filename, "w") do |fp| @data.each do |section, values| if section != GLOBAL_SECTION fp.puts("[#{section}]") diff --git a/lib/inspector.rb b/lib/inspector.rb index 4b12d61f..78d952ad 100644 --- a/lib/inspector.rb +++ b/lib/inspector.rb @@ -82,7 +82,7 @@ def display(klass = ALL, target = $stdout) def save(path = nil) path = File.join(@setting.archive_path, INSPECT_LOG_NAME) if path.nil? - open(path, "w") do |fp| + File.open(path, "w") do |fp| fp.puts "--- ログ出力 #{Time.now} ---" display(ALL, fp) end diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index e9cd97d4..f5625733 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -89,7 +89,7 @@ def self.convert_file(filename, options = {}) setting.author = "" setting.title = File.basename(filename) novel_converter = new(setting, output_filename, options[:display_inspector]) - text = open(filename, "r:BOM|UTF-8") { |fp| fp.read }.gsub("\r", "") + text = File.open(filename, "r:BOM|UTF-8") { |fp| fp.read }.gsub("\r", "") if options[:encoding] text.force_encoding(options[:encoding]).encode!(Encoding::UTF_8) end diff --git a/narou.gemspec b/narou.gemspec index 0ebc5adb..9f1799aa 100644 --- a/narou.gemspec +++ b/narou.gemspec @@ -69,6 +69,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'nkf', '~> 0.2.0' gem.add_runtime_dependency 'csv', '~> 3.3' gem.add_runtime_dependency 'rexml', '~> 3.4' + gem.add_runtime_dependency 'sanitize', '~> 7.0.0' gem.add_development_dependency 'rspec', '~> 3.13' gem.add_development_dependency 'rspec-retry', '~> 0.6' From 4b3e38c756ac804e31c7cedb30b7ce070476233a Mon Sep 17 00:00:00 2001 From: ponponusa Date: Sat, 25 Oct 2025 20:28:43 +0900 Subject: [PATCH 08/24] =?UTF-8?q?WebUI=E3=81=AE=E8=AA=8D=E8=A8=BC=E3=82=92?= =?UTF-8?q?Digest=E3=81=8B=E3=82=89Basic=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/command/setting.rb | 19 ++++++------------- lib/web/appserver.rb | 12 +++++------- lib/web/settingmessages.rb | 4 +--- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/command/setting.rb b/lib/command/setting.rb index dd8c3743..e8d98ffe 100644 --- a/lib/command/setting.rb +++ b/lib/command/setting.rb @@ -642,25 +642,18 @@ def self.get_setting_tab_info invisible: true, tab: :global }, - "server-digest-auth.enable" => { - type: :boolean, help: "WEBサーバでDigest認証を使用するかどうか", + "server-basic-auth.enable" => { + type: :boolean, help: "WEBサーバでBasic認証を使用するかどうか", invisible: true, tab: :global }, - "server-digest-auth.user" => { - type: :string, help: "WEBサーバでDigest認証をするユーザ名", + "server-basic-auth.user" => { + type: :string, help: "WEBサーバでBasic認証をするユーザ名", invisible: true, tab: :global }, - "server-digest-auth.password" => { - type: :string, help: "WEBサーバのDigest認証のパスワード。hashed-passwordも設定した場合はそちらが優先される", - invisible: true, - tab: :global - }, - "server-digest-auth.hashed-password" => { - type: :string, - help: "WEBサーバのDigest認証のパスワードを、Realmを\"narou.rb\"としてハッシュにしたもの。下記のようなコマンドで生成できる\n" \ - "$ ruby -r 'digest/md5' -e 'puts Digest::MD5.hexdigest \"\#{$*[0]}:narou.rb:\#{$*[1]}\"' user password", + "server-basic-auth.password" => { + type: :string, help: "WEBサーバのBasic認証のパスワード", invisible: true, tab: :global }, diff --git a/lib/web/appserver.rb b/lib/web/appserver.rb index bfcd95bc..39fdb3c7 100755 --- a/lib/web/appserver.rb +++ b/lib/web/appserver.rb @@ -167,19 +167,17 @@ def fill_general_all_no_in_database end # サーバーの認証の設定 - # とりあえずDigest認証のみ + # - Digest認証がRackの機能からオミットされたので、Basic認証に変更 def setup_server_authentication - auth = Inventory.load("global_setting", :global).group("server-digest-auth") + auth = Inventory.load("global_setting", :global).group("server-basic-auth") user = auth.user - hashed = auth.hashed_password - passwd = hashed || auth.password + passwd = auth.password # ハッシュは使わない - # enableかつユーザー名とパスワードが設定されている時のみ認証を有効にする return unless auth.enable && user && passwd self.class.class_exec do - use Rack::Auth::Digest::MD5, { realm: "narou.rb", opaque: "", passwords_hashed: hashed } do |username| - passwd if username == user + use Rack::Auth::Basic, "narou.rb" do |username, password| + username == user && password == passwd end end end diff --git a/lib/web/settingmessages.rb b/lib/web/settingmessages.rb index beb66515..c4282248 100644 --- a/lib/web/settingmessages.rb +++ b/lib/web/settingmessages.rb @@ -21,9 +21,7 @@ module Narou "no-color" => "コンソールのカラー表示を無効にする\n※要サーバ再起動", "economy" => "容量節約に関する設定", "send.without-freeze" => "一括送信時に凍結された小説は対象外にする。(個別送信時は凍結済みでも送信可能)", - "server-digest-auth.enable" => "%%ORIG%%\n※digest-auth関連の設定を変更した場合サーバの再起動が必要", - "server-digest-auth.hashed-password" => "サーバのDigest認証のパスワードを、Realmを\"narou.rb\"としてハッシュにしたもの。\n" \ - "https://tgws.plus/app/digest/ などで生成できる", + "server-basic-auth.enable" => "%%ORIG%%\n※basic-auth関連の設定を変更した場合サーバの再起動が必要", "concurrency" => "%%ORIG%% ※要サーバ再起動", "logging" => "%%ORIG%%\n※要サーバ再起動", "logging.format-filename" => "%%ORIG%%\n※要サーバ再起動", From 2a369e8cac19aa1331f63dca347c5b411d454fbd Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 01:09:10 +0900 Subject: [PATCH 09/24] =?UTF-8?q?bootsnap=E3=82=92=E5=B0=8E=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- narou.gemspec | 1 + narou.rb | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/narou.gemspec b/narou.gemspec index 9f1799aa..7d7a1ac8 100644 --- a/narou.gemspec +++ b/narou.gemspec @@ -70,6 +70,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'csv', '~> 3.3' gem.add_runtime_dependency 'rexml', '~> 3.4' gem.add_runtime_dependency 'sanitize', '~> 7.0.0' + gem.add_runtime_dependency 'bootsnap', '~> 1.18', '>= 1.18.6' gem.add_development_dependency 'rspec', '~> 3.13' gem.add_development_dependency 'rspec-retry', '~> 0.6' diff --git a/narou.rb b/narou.rb index 87da4c13..c73bfbc8 100644 --- a/narou.rb +++ b/narou.rb @@ -7,6 +7,17 @@ # Copyright 2013 whiteleaf. All rights reserved. # +require 'bootsnap' +Bootsnap.setup( + cache_dir: 'tmp/cache', # Path to your cache + ignore_directories: [], # Directory names to skip. + development_mode: false, # Current working environment, e.g. RACK_ENV, RAILS_ENV, etc + load_path_cache: true, # Optimize the LOAD_PATH with a cache + compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting. + compile_cache_yaml: true, # Compile YAML into a cache + readonly: true, # Use the caches but don't update them on miss or stale entries. +) + require_relative "lib/extension" require_relative "lib/extensions/monkey_patches" require_relative "lib/backtracer" From aa56398de9623b9c4cec3544c18bccc3164d628c Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 01:10:45 +0900 Subject: [PATCH 10/24] =?UTF-8?q?Yaml=E3=83=AD=E3=83=BC=E3=83=89/I/O?= =?UTF-8?q?=E3=83=BBERB=E3=83=BB=E3=83=AD=E3=82=B0=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E5=96=84=20-=20Yaml=E3=83=AD=E3=83=BC=E3=83=89=E3=82=92?= =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=99=E3=82=8B?= =?UTF-8?q?=20-=20=E9=80=B2=E6=8D=97=E8=A1=A8=E7=A4=BAI/O=E3=82=92?= =?UTF-8?q?=E9=96=93=E5=BC=95=E3=81=8F=20-=20Template=20(ERB)=20=E3=81=AE?= =?UTF-8?q?=E5=86=8D=E3=82=B3=E3=83=B3=E3=83=91=E3=82=A4=E3=83=AB=E3=82=92?= =?UTF-8?q?=E9=81=BF=E3=81=91=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/converterbase.rb | 2 +- lib/novelconverter.rb | 46 +++++++++++++++++++++++++--- lib/template.rb | 71 ++++++++++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 23 deletions(-) diff --git a/lib/converterbase.rb b/lib/converterbase.rb index 7e9ec882..eef415d3 100644 --- a/lib/converterbase.rb +++ b/lib/converterbase.rb @@ -1368,7 +1368,7 @@ def convert_main(io) @write_fp.write(data) else @read_fp.each_with_index do |line, i| - progressbar.output(i) if progressbar + progressbar.output(i) if progressbar && (i % 50).zero? # 50行ごとに制限 @request_skip_output_line = false zenkaku_rstrip(line) if @request_insert_blank_next_line diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index f5625733..f5c37c2d 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -575,9 +575,12 @@ def initialize_event on(:"convert_main.init") do |subtitles| progressbar = ProgressBar.new(subtitles.size, io: stream_io) end + on(:"convert_main.loop") do |i| - progressbar.output(i) if progressbar + # 毎回ではな10件ごとに絞る + progressbar.output(i) if progressbar && (i % 10).zero? end + on(:"convert_main.finish") do progressbar.clear if progressbar end @@ -611,13 +614,25 @@ def create_novel_text_by_template(sections, toc, is_hotentry = false, index = ni cover_chuki = create_cover_chuki device = Narou.get_device setting = @setting - toc["title"] = setting.novel_title unless setting.novel_title.empty? + + toc["title"] = setting.novel_title unless setting.novel_title.empty? toc["author"] = setting.novel_author unless setting.novel_author.empty? + processing_title = toc["title"] processing_title += "_#{index}" if index processed_title = decorate_title(processing_title) template_name = (device && device.ibunko? ? NOVEL_TEXT_TEMPLATE_NAME_FOR_IBUNKO : NOVEL_TEXT_TEMPLATE_NAME) - Template.get(template_name, binding, 1.1) + + # テンプレートをキャッシュする + # コンパイル済みERB(またはProc)をキャッシュして binding だけ都度差し込む + @__template_cache ||= {} + compiled = @__template_cache[template_name] + unless compiled + compiled = Template.compile(template_name, 1.1) + @__template_cache[template_name] = compiled + end + + Template.render(compiled, binding) end # @@ -845,6 +860,9 @@ def cut_subtitles(subtitles) # subtitle info から変換処理をする # def subtitles_to_sections(subtitles, html) + # 章データキャッシュ + @__section_cache ||= {} + sections = [] section_save_dir = Downloader.get_novel_section_save_dir(@setting.archive_path) @@ -853,16 +871,32 @@ def subtitles_to_sections(subtitles, html) subtitles.each_with_index do |subinfo, i| trigger(:"convert_main.loop", i) @converter.current_index = i - section = load_novel_section(subinfo, section_save_dir) - if section["chapter"].length > 0 + + # Yamlロードをキャッシュ + key = subinfo["index"] + section = @__section_cache[key] + unless section + # load_novel_section はハッシュを返す + section = load_novel_section(subinfo, section_save_dir) + @__section_cache[key] = section + end + + # dupしてから編集する事でキャッシュ汚染を防ぐ + section = section.dup + section["element"] = section["element"].dup + + # Converter呼び出しを一箇所に集約 + if section["chapter"] && !section["chapter"].empty? section["chapter"] = @converter.convert(section["chapter"], "chapter") end @inspector.subtitle = section["subtitle"] section["subtitle"] = @converter.convert(section["subtitle"], "subtitle") + element = section["element"] data_type = element.delete("data_type") || "text" @converter.data_type = data_type + element.each do |text_type, elm_text| if data_type != "text" html.string = elm_text @@ -870,8 +904,10 @@ def subtitles_to_sections(subtitles, html) end element[text_type] = @converter.convert(elm_text, text_type) end + sections << section end + @use_dakuten_font = @converter.use_dakuten_font sections ensure diff --git a/lib/template.rb b/lib/template.rb index a19161ed..4cfedd81 100644 --- a/lib/template.rb +++ b/lib/template.rb @@ -13,6 +13,57 @@ class Template class LoadError < StandardError; end + # コンパイル済みテンプレートをキャッシュする + # { "novel.txt" => { erb: , binary_version: 1.1, src_filename: "novel.txt" } } + # キャッシュする関係でスレッドセーフにはなっていないので、並列での変換処理を行う場合は対応が必要 + @__compiled_cache = {} + + # + # テンプレートを元にデータを作成 + # + # テンプレートファイルの検索順位 + # 1. root_dir/template + # 2. script_dir/template + # + def self.compile(src_filename, binary_version) + # すでにキャッシュ済みならそのまま返す + cached = @__compiled_cache[src_filename] + return cached if cached + + # ファイル探索(getと同じロジック) + [Narou.root_dir, Narou.script_dir].each do |dir| + path = dir.join(TEMPLATE_DIR, src_filename + ".erb") + next unless path.exist? + + src = Helper::CacheLoader.load(path) + erb = ERB.new(src, trim_mode: "-") + + compiled = { + erb: erb, + binary_version: binary_version, + src_filename: src_filename + } + + @__compiled_cache[src_filename] = compiled + return compiled + end + + raise LoadError, "テンプレートファイルが見つかりません。(#{src_filename}.erb)" + end + + def self.render(compiled, _binding) + # compiled は compile が返した Hash + @@binary_version = compiled[:binary_version] + @@src_filename = compiled[:src_filename] + + # target_binary_version から参照される @@src_version は + # テンプレート内で <%= Template.target_binary_version 1.1 %> みたいに呼ばれる想定 + # なので、ここでは設定しない。テンプレの中から呼ばれた時点で + # @@src_version が更新され、invalid_templace_version? が機能する + + compiled[:erb].result(_binding) + end + # # テンプレートを元にファイルを作成 # @@ -36,24 +87,10 @@ def self.write(src_filename, dest_filepath, _binding, binary_version, overwrite end end - # - # テンプレートを元にデータを作成 - # - # テンプレートファイルの検索順位 - # 1. root_dir/template - # 2. script_dir/template - # + # 既存コード向けのwrap関数 def self.get(src_filename, _binding, binary_version) - @@binary_version = binary_version - @@src_filename = src_filename - [Narou.root_dir, Narou.script_dir].each do |dir| - path = dir.join(TEMPLATE_DIR, src_filename + ".erb") - next unless path.exist? - src = Helper::CacheLoader.load(path) - result = ERB.new(src, trim_mode: "-").result(_binding) - return result - end - raise LoadError, "テンプレートファイルが見つかりません。(#{src_filename}.erb)" + compiled = compile(src_filename, binary_version) + render(compiled, _binding) end def self.invalid_templace_version? From de79ce8a1365c164d6d8934d2a41f2e544c99164 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 02:05:22 +0900 Subject: [PATCH 11/24] =?UTF-8?q?add=5Fdc=5Fsubject=5Fto=5Fepub=20?= =?UTF-8?q?=E3=82=92=E3=82=AA=E3=83=B3=E3=83=A1=E3=83=A2=E3=83=AA=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/novelconverter.rb | 75 ++++++++++++------------------------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index f5c37c2d..0643a596 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -259,97 +259,64 @@ def self.txt_to_epub(filename, dst_dir: nil, device: nil, verbose: false, yokoga # def self.add_dc_subject_to_epub(epub_path, subjects, stream_io: $stdout2) return :success if subjects.nil? || subjects.empty? - - # 必要なgemが利用できない場合は警告を出して処理をスキップ if defined?(ZIP_UNAVAILABLE) stream_io.error "dc:subject埋め込み機能を使用するにはrubyzip gemが必要です" return :error end - if defined?(REXML_UNAVAILABLE) - stream_io.error "dc:subject埋め込み機能を使用するにはrexml gemが必要です" - return :error - end - - temp_dir = nil + entries = {} begin - # 一時ディレクトリ作成 - temp_dir = Dir.mktmpdir - - # EPUBファイルを展開 + # EPUBをメモリ上に展開 Zip::File.open(epub_path) do |zip_file| zip_file.each do |entry| - entry_path = File.join(temp_dir, entry.name) - FileUtils.mkdir_p(File.dirname(entry_path)) - entry.extract(entry_path) + entries[entry.name] = entry.get_input_stream.read end end - # standard.opfファイルを探す - opf_path = Dir.glob(File.join(temp_dir, "**", "standard.opf")).first - unless opf_path + # standard.opf 書き換え + opf_name, opf_body = entries.find { |name, _| name.end_with?("standard.opf") } + unless opf_name stream_io.error "standard.opfファイルが見つかりませんでした" return :error end - # 文字列置換でdc:subjectを追加する方式に変更 - # (REXMLの整形では元のフォーマットが崩れるため) - content = File.read(opf_path) - - # 既存のdc:subjectを削除 + content = opf_body.dup content.gsub!(/.*?<\/dc:subject>\s*\n?\s*/m, "") - - # 新しいdc:subjectを生成(XMLエスケープも行う) - dc_subject_lines = subjects.map do |subject| - next if subject.strip.empty? - escaped_subject = subject.strip.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub("\"", """).gsub("'", "'") - " #{escaped_subject}" - end.compact - + dc_subject_lines = subjects.map(&:strip).reject(&:empty?).map { |s| + esc = s.gsub("&","&").gsub("<","<").gsub(">",">").gsub("\"",""").gsub("'","'") + " #{esc}" + } if dc_subject_lines.any? - # の直前にdc:subjectを挿入 dc_subjects_xml = dc_subject_lines.join("\n") + "\n" content.sub!(/(\s*)<\/metadata>/, "\n#{dc_subjects_xml}\\1") end + entries[opf_name] = content - # XMLを書き戻し - File.write(opf_path, content) - - # EPUBファイルを再作成(OutputStreamを使用しmimetypeを無圧縮・先頭に配置) + # 再Zip化 (mimetypeは無圧縮で先頭) File.delete(epub_path) - Zip::OutputStream.open(epub_path) do |zos| - # mimetypeファイルを最初に無圧縮で追加 - mimetype_path = File.join(temp_dir, "mimetype") - unless File.exist?(mimetype_path) + # mimetype必須 + if !entries["mimetype"] stream_io.error "mimetypeファイルが見つかりません" return :error end # 第1引数に名前、第4引数にZip::Entry::STORED を渡す - zos.put_next_entry('mimetype', nil, nil, Zip::Entry::STORED) + zos.put_next_entry("mimetype", nil, nil, Zip::Entry::STORED) + zos.write entries["mimetype"] - zos.write File.read(mimetype_path, mode: "rb") - - # 他のファイルを追加(mimetypeを除く) - Dir.glob(File.join(temp_dir, "**", "*"), File::FNM_DOTMATCH).sort.each do |file_path| - next if File.directory?(file_path) - relative_path = file_path.sub(temp_dir + "/", "") - next if relative_path == "mimetype" # mimetypeは既に追加済み - zos.put_next_entry(relative_path) - zos.write File.read(file_path, mode: "rb") + entries.each do |name, body| + next if name == "mimetype" + zos.put_next_entry(name) + zos.write body end end stream_io.puts "dc:subjectを追加しました: #{subjects.join(', ')}" :success - rescue => e stream_io.error "dc:subject追加中にエラーが発生しました: #{e.message}" :error - ensure - # 一時ディレクトリを削除 - FileUtils.rm_rf(temp_dir) if temp_dir end end From 3fd26ae761cecbd599ebd76c1205d7fb559767ca Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 02:21:43 +0900 Subject: [PATCH 12/24] =?UTF-8?q?=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88?= =?UTF-8?q?=E5=A4=89=E6=8F=9B=E5=87=A6=E7=90=86=E3=82=92=E3=81=BE=E3=81=A8?= =?UTF-8?q?=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/converterbase.rb | 13 ++++++++- lib/novelconverter.rb | 63 ++++++++++++++++++++++++++++++------------- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/lib/converterbase.rb b/lib/converterbase.rb index eef415d3..9395be67 100644 --- a/lib/converterbase.rb +++ b/lib/converterbase.rb @@ -45,6 +45,7 @@ def initialize(setting, inspector, illustration) @subtitles = nil @data_type = "text" @current_index = 0 + @device = Narou.get_device reset_member_values end @@ -65,7 +66,6 @@ def reset_member_values @num_and_comma_list = {} @force_indent_special_chapter_list = {} @in_author_comment_block = nil - @device = Narou.get_device end def outputs(data = "", force = false) @@ -1332,6 +1332,17 @@ def convert(text, text_type) return data end + # 複数のテキストをまとめて変換する + # pairs: { key1 => [text, text_type], key2 => [text, text_type], ... } + # 戻り値: { key1 => converted_text1, key2 => converted_text2, ... } + def convert_multi(pairs) + results = {} + pairs.each do |key, (text, text_type)| + results[key] = convert(text, text_type) + end + results + end + # # 変換処理本体 # diff --git a/lib/novelconverter.rb b/lib/novelconverter.rb index 0643a596..618fcb0c 100644 --- a/lib/novelconverter.rb +++ b/lib/novelconverter.rb @@ -827,7 +827,7 @@ def cut_subtitles(subtitles) # subtitle info から変換処理をする # def subtitles_to_sections(subtitles, html) - # 章データキャッシュ + # 章データをキャッシュ @__section_cache ||= {} sections = [] @@ -839,37 +839,61 @@ def subtitles_to_sections(subtitles, html) trigger(:"convert_main.loop", i) @converter.current_index = i - # Yamlロードをキャッシュ + # YAMLロードをキャッシュ key = subinfo["index"] - section = @__section_cache[key] - unless section - # load_novel_section はハッシュを返す - section = load_novel_section(subinfo, section_save_dir) - @__section_cache[key] = section + original_section = @__section_cache[key] + unless original_section + original_section = load_novel_section(subinfo, section_save_dir) + @__section_cache[key] = original_section end - # dupしてから編集する事でキャッシュ汚染を防ぐ - section = section.dup - section["element"] = section["element"].dup - - # Converter呼び出しを一箇所に集約 - if section["chapter"] && !section["chapter"].empty? - section["chapter"] = @converter.convert(section["chapter"], "chapter") - end - - @inspector.subtitle = section["subtitle"] - section["subtitle"] = @converter.convert(section["subtitle"], "subtitle") + # キャッシュを壊さないようディープ寄りにdup + # (chapter/subtitle/elementなど後で書き換えるので) + section = original_section.dup + section["element"] = original_section["element"].dup + # data_type 判定 element = section["element"] data_type = element.delete("data_type") || "text" @converter.data_type = data_type + # HTML→青空変換が必要なやつを先にプレーンテキスト化 + preprocessed_element_texts = {} element.each do |text_type, elm_text| if data_type != "text" html.string = elm_text elm_text = html.to_aozora(pre_html: data_type == "pre_html") end - element[text_type] = @converter.convert(elm_text, text_type) + preprocessed_element_texts[text_type] = elm_text + end + + # まとめてコンバータに渡すためのバッチ入力を作る + batch_inputs = {} + + # chapter + if section["chapter"] && !section["chapter"].empty? + batch_inputs[:chapter] = [section["chapter"], "chapter"] + end + + # subtitle + @inspector.subtitle = section["subtitle"] + batch_inputs[:subtitle] = [section["subtitle"], "subtitle"] + + # element 各種 + preprocessed_element_texts.each do |text_type, body_text| + batch_inputs[[:element, text_type]] = [body_text, text_type] + end + + # 一括変換 + converted = @converter.convert_multi(batch_inputs) + if batch_inputs[:chapter] + section["chapter"] = converted[:chapter] + end + + section["subtitle"] = converted[:subtitle] + + element.keys.each do |text_type| + section["element"][text_type] = converted[[:element, text_type]] end sections << section @@ -881,6 +905,7 @@ def subtitles_to_sections(subtitles, html) trigger(:"convert_main.finish") end + # # テキストデータ先頭二行からタイトルと作者名を取得 # From 9123fd58e0e9f746dc90b353fef403782b89fc54 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 02:43:18 +0900 Subject: [PATCH 13/24] update Gemfile.lock --- Gemfile.lock | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2d8ec1b1..e5b097a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,9 @@ PATH remote: . specs: - narou (3.9.1.20251024d) + narou (3.9.1) activesupport (~> 8.0, >= 8.1.0) + bootsnap (~> 1.18, >= 1.18.6) csv (~> 3.3) diff-lcs (~> 1.6, >= 1.6.2) erubi (~> 1.13.1) @@ -47,6 +48,8 @@ GEM base64 (0.3.0) bigdecimal (3.3.1) bigdecimal (3.3.1-java) + bootsnap (1.18.6) + msgpack (~> 1.2) byebug (12.0.0) coderay (1.1.3) concurrent-ruby (1.3.5) @@ -81,6 +84,8 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.14.4) + msgpack (1.8.0) + msgpack (1.8.0-java) multi_json (1.15.0) mustermann (3.0.4) ruby2_keywords (~> 0.0.1) From 035c9ce5a4bddcfcf3fd3017a379b112ca28eabb Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 03:10:15 +0900 Subject: [PATCH 14/24] =?UTF-8?q?YJIT=20=E3=82=92=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/narou | 2 +- narou.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/narou b/bin/narou index da47ca1e..34c007a3 100755 --- a/bin/narou +++ b/bin/narou @@ -1,4 +1,4 @@ -#! /usr/bin/env ruby +#! /usr/bin/env ruby --yjit # -*- mode: ruby -*- # -*- coding: utf-8 -*- # diff --git a/narou.rb b/narou.rb index c73bfbc8..db5b7342 100644 --- a/narou.rb +++ b/narou.rb @@ -1,4 +1,4 @@ -#! /usr/bin/env ruby +#! /usr/bin/env ruby --yjit # frozen_string_literal: true # From 9156edf26d07800bce646d3e11aa06a866c92524 Mon Sep 17 00:00:00 2001 From: ponponusa Date: Tue, 28 Oct 2025 12:06:41 +0900 Subject: [PATCH 15/24] =?UTF-8?q?resources=E3=81=AE=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E3=82=92=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/{ => css}/default-style.css | 0 .../resources/{ => css}/toggle-switch.css | 0 lib/web/public/resources/dataTables.colVis.js | 1113 ----------------- .../resources/{ => images}/sort_asc.png | Bin .../resources/{ => images}/sort_desc.png | Bin lib/web/public/resources/{ => js}/bootbox.js | 0 .../public/resources/{ => js}/common.ui.js | 0 .../resources/{ => js}/jquery.moveto.js | 0 .../resources/{ => js}/jquery.outerclick.js | 0 .../resources/{ => js}/jquery.slidenavbar.js | 0 .../resources/{ => js}/narou.library.js | 0 .../public/resources/{ => js}/narou.queue.js | 0 lib/web/public/resources/{ => js}/narou.ui.js | 0 lib/web/public/resources/{ => js}/shortcut.js | 0 lib/web/public/resources/{ => js}/sprintf.js | 0 .../resources/perfect-scrollbar.min.css | 5 - .../public/resources/perfect-scrollbar.min.js | 4 - lib/web/views/layout.haml | 22 +- lib/web/views/style.scss | 4 +- 19 files changed, 13 insertions(+), 1135 deletions(-) rename lib/web/public/resources/{ => css}/default-style.css (100%) rename lib/web/public/resources/{ => css}/toggle-switch.css (100%) delete mode 100644 lib/web/public/resources/dataTables.colVis.js rename lib/web/public/resources/{ => images}/sort_asc.png (100%) rename lib/web/public/resources/{ => images}/sort_desc.png (100%) rename lib/web/public/resources/{ => js}/bootbox.js (100%) rename lib/web/public/resources/{ => js}/common.ui.js (100%) rename lib/web/public/resources/{ => js}/jquery.moveto.js (100%) rename lib/web/public/resources/{ => js}/jquery.outerclick.js (100%) rename lib/web/public/resources/{ => js}/jquery.slidenavbar.js (100%) rename lib/web/public/resources/{ => js}/narou.library.js (100%) rename lib/web/public/resources/{ => js}/narou.queue.js (100%) rename lib/web/public/resources/{ => js}/narou.ui.js (100%) rename lib/web/public/resources/{ => js}/shortcut.js (100%) rename lib/web/public/resources/{ => js}/sprintf.js (100%) delete mode 100644 lib/web/public/resources/perfect-scrollbar.min.css delete mode 100644 lib/web/public/resources/perfect-scrollbar.min.js diff --git a/lib/web/public/resources/default-style.css b/lib/web/public/resources/css/default-style.css similarity index 100% rename from lib/web/public/resources/default-style.css rename to lib/web/public/resources/css/default-style.css diff --git a/lib/web/public/resources/toggle-switch.css b/lib/web/public/resources/css/toggle-switch.css similarity index 100% rename from lib/web/public/resources/toggle-switch.css rename to lib/web/public/resources/css/toggle-switch.css diff --git a/lib/web/public/resources/dataTables.colVis.js b/lib/web/public/resources/dataTables.colVis.js deleted file mode 100644 index 03f09f40..00000000 --- a/lib/web/public/resources/dataTables.colVis.js +++ /dev/null @@ -1,1113 +0,0 @@ -/*! ColVis 1.1.2-dev - * ©2010-2014 SpryMedia Ltd - datatables.net/license - */ - -/** - * @summary ColVis - * @description Controls for column visibility in DataTables - * @version 1.1.2-dev - * @file dataTables.colReorder.js - * @author SpryMedia Ltd (www.sprymedia.co.uk) - * @contact www.sprymedia.co.uk/contact - * @copyright Copyright 2010-2014 SpryMedia Ltd. - * - * This source file is free software, available under the following license: - * MIT license - http://datatables.net/license/mit - * - * This source file is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. - * - * For details please refer to: http://www.datatables.net - */ - -(function(window, document, undefined) { - - -var factory = function( $, DataTable ) { -"use strict"; - -/** - * ColVis provides column visibility control for DataTables - * - * @class ColVis - * @constructor - * @param {object} DataTables settings object. With DataTables 1.10 this can - * also be and API instance, table node, jQuery collection or jQuery selector. - * @param {object} ColVis configuration options - */ -var ColVis = function( oDTSettings, oInit ) -{ - /* Santiy check that we are a new instance */ - if ( !this.CLASS || this.CLASS != "ColVis" ) - { - alert( "Warning: ColVis must be initialised with the keyword 'new'" ); - } - - if ( typeof oInit == 'undefined' ) - { - oInit = {}; - } - - var camelToHungarian = $.fn.dataTable.camelToHungarian; - if ( camelToHungarian ) { - camelToHungarian( ColVis.defaults, ColVis.defaults, true ); - camelToHungarian( ColVis.defaults, oInit ); - } - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Public class variables - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * @namespace Settings object which contains customisable information for - * ColVis instance. Augmented by ColVis.defaults - */ - this.s = { - /** - * DataTables settings object - * @property dt - * @type Object - * @default null - */ - "dt": null, - - /** - * Customisation object - * @property oInit - * @type Object - * @default passed in - */ - "oInit": oInit, - - /** - * Flag to say if the collection is hidden - * @property hidden - * @type boolean - * @default true - */ - "hidden": true, - - /** - * Store the original visibility settings so they could be restored - * @property abOriginal - * @type Array - * @default [] - */ - "abOriginal": [] - }; - - - /** - * @namespace Common and useful DOM elements for the class instance - */ - this.dom = { - /** - * Wrapper for the button - given back to DataTables as the node to insert - * @property wrapper - * @type Node - * @default null - */ - "wrapper": null, - - /** - * Activation button - * @property button - * @type Node - * @default null - */ - "button": null, - - /** - * Collection list node - * @property collection - * @type Node - * @default null - */ - "collection": null, - - /** - * Background node used for shading the display and event capturing - * @property background - * @type Node - * @default null - */ - "background": null, - - /** - * Element to position over the activation button to catch mouse events when using mouseover - * @property catcher - * @type Node - * @default null - */ - "catcher": null, - - /** - * List of button elements - * @property buttons - * @type Array - * @default [] - */ - "buttons": [], - - /** - * List of group button elements - * @property groupButtons - * @type Array - * @default [] - */ - "groupButtons": [], - - /** - * Restore button - * @property restore - * @type Node - * @default null - */ - "restore": null - }; - - /* Store global reference */ - ColVis.aInstances.push( this ); - - /* Constructor logic */ - this.s.dt = $.fn.dataTable.Api ? - new $.fn.dataTable.Api( oDTSettings ).settings()[0] : - oDTSettings; - - this._fnConstruct( oInit ); - return this; -}; - - - -ColVis.prototype = { - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Public methods - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Get the ColVis instance's control button so it can be injected into the - * DOM - * @method button - * @returns {node} ColVis button - */ - button: function () - { - return this.dom.wrapper; - }, - - /** - * Alias of `rebuild` for backwards compatibility - * @method fnRebuild - */ - "fnRebuild": function () - { - this.rebuild(); - }, - - /** - * Rebuild the list of buttons for this instance (i.e. if there is a column - * header update) - * @method fnRebuild - */ - rebuild: function () - { - /* Remove the old buttons */ - for ( var i=this.dom.buttons.length-1 ; i>=0 ; i-- ) { - this.dom.collection.removeChild( this.dom.buttons[i] ); - } - this.dom.buttons.splice( 0, this.dom.buttons.length ); - - if ( this.dom.restore ) { - this.dom.restore.parentNode( this.dom.restore ); - } - - /* Re-add them (this is not the optimal way of doing this, it is fast and effective) */ - this._fnAddGroups(); - this._fnAddButtons(); - - /* Update the checkboxes */ - this._fnDrawCallback(); - }, - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Private methods (they are of course public in JS, but recommended as private) - * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - - /** - * Constructor logic - * @method _fnConstruct - * @returns void - * @private - */ - "_fnConstruct": function ( init ) - { - this._fnApplyCustomisation( init ); - - var that = this; - var i, iLen; - this.dom.wrapper = document.createElement('div'); - this.dom.wrapper.className = "ColVis"; - - this.dom.button = $( '