From b61027be4b53a45c5454ebb758d5395824c934a0 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sat, 7 Jun 2025 18:54:10 +0900 Subject: [PATCH] #19 --- backend/.gitignore | 2 + backend/Gemfile | 2 + backend/Gemfile.lock | 84 +++++++++++++++++++ .../app/controllers/wiki_proxy_controller.rb | 31 +++++++ backend/config/routes.rb | 1 + frontend/package-lock.json | 65 +++++++++++++- frontend/package.json | 2 + frontend/src/App.tsx | 2 + frontend/src/pages/WikiEditPage.tsx | 40 +++++++++ 9 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 backend/app/controllers/wiki_proxy_controller.rb create mode 100644 frontend/src/pages/WikiEditPage.tsx diff --git a/backend/.gitignore b/backend/.gitignore index 8bcd372..5b11ceb 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -36,3 +36,5 @@ /config/database.yml /config/credentials/production.key + +/wiki diff --git a/backend/Gemfile b/backend/Gemfile index 17d7302..af3b648 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -55,3 +55,5 @@ gem "mysql2", "~> 0.5.6" gem "image_processing", "~> 1.14" gem "nokogiri", "~> 1.18" + +gem 'gollum' diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 7b9a1ae..cd35aa7 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -100,8 +100,41 @@ GEM ffi (1.17.2-x86_64-darwin) ffi (1.17.2-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-musl) + gemojione (4.3.3) + json + github-markup (4.0.2) globalid (1.2.1) activesupport (>= 6.1) + gollum (6.1.0) + gemojione (~> 4.1) + gollum-lib (~> 6.0) + i18n (~> 1.8) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.1.0) + mustache-sinatra (~> 2.0) + octicons (~> 19.0) + rack (>= 3.0) + rackup (~> 2.1) + rdoc (~> 6) + rss (~> 0.3) + sinatra (~> 4.0) + sinatra-contrib (~> 4.0) + sprockets (~> 4.1) + sprockets-helpers (~> 1.2) + therubyrhino (~> 2.1.0) + useragent (~> 0.16.2) + webrick (~> 1.7) + gollum-lib (6.0) + gemojione (~> 4.1) + github-markup (~> 4.0) + gollum-rugged_adapter (~> 3.0) + loofah (~> 2.3) + nokogiri (~> 1.8) + rouge (~> 3.1) + twitter-text (= 1.14.7) + gollum-rugged_adapter (3.0) + mime-types (~> 3.4) + rugged (~> 1.5) i18n (1.14.7) concurrent-ruby (~> 1.0) image_processing (1.14.0) @@ -126,6 +159,10 @@ GEM sshkit (>= 1.23.0, < 2.0) thor (~> 1.3) zeitwerk (>= 2.6.18, < 3.0) + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) @@ -138,12 +175,22 @@ GEM net-pop net-smtp marcel (1.0.4) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0603) mini_magick (5.2.0) benchmark logger mini_mime (1.1.5) minitest (5.25.5) msgpack (1.8.0) + multi_json (1.15.0) + mustache (1.1.1) + mustache-sinatra (2.0.0) + mustache (~> 1.0) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) mysql2 (0.5.6) net-imap (0.5.8) date @@ -176,6 +223,7 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-linux-musl) racc (~> 1.4) + octicons (19.15.2) ostruct (0.6.1) parallel (1.27.0) parser (3.3.8.0) @@ -194,6 +242,10 @@ GEM rack (3.1.14) rack-cors (2.0.2) rack (>= 2.0.0) + rack-protection (4.1.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) @@ -237,6 +289,10 @@ GEM regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) + rexml (3.4.1) + rouge (3.30.0) + rss (0.3.1) + rexml rubocop (1.75.6) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -269,11 +325,28 @@ GEM ruby-vips (2.2.3) ffi (~> 1.12) logger + ruby2_keywords (0.0.5) + rugged (1.9.0) securerandom (0.4.1) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + sinatra-contrib (4.1.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.1.1) + sinatra (= 4.1.1) + tilt (~> 2.0) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger rack (>= 2.2.4, < 4) + sprockets-helpers (1.4.0) + sprockets (>= 2.2) sprockets-rails (3.5.2) actionpack (>= 6.1) activesupport (>= 6.1) @@ -294,20 +367,30 @@ GEM net-ssh (>= 2.8.0) ostruct stringio (3.1.7) + therubyrhino (2.1.2) + therubyrhino_jar (>= 1.7.4, < 1.7.9) + therubyrhino_jar (1.7.8) thor (1.3.2) thruster (0.1.13) thruster (0.1.13-aarch64-linux) thruster (0.1.13-arm64-darwin) thruster (0.1.13-x86_64-darwin) thruster (0.1.13-x86_64-linux) + tilt (2.6.0) timeout (0.4.3) + twitter-text (1.14.7) + unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.9.1) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uri (1.0.3) useragent (0.16.11) + webrick (1.9.1) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) @@ -329,6 +412,7 @@ PLATFORMS DEPENDENCIES bootsnap brakeman + gollum image_processing (~> 1.14) jwt kamal diff --git a/backend/app/controllers/wiki_proxy_controller.rb b/backend/app/controllers/wiki_proxy_controller.rb new file mode 100644 index 0000000..c839679 --- /dev/null +++ b/backend/app/controllers/wiki_proxy_controller.rb @@ -0,0 +1,31 @@ +require 'net/http' +require 'uri' + + +class WikiProxyController < ApplicationController + def edit + tag = params[:tag] + uri = "http://localhost:4567/gollum/edit/#{ URI.encode_www_form_component(tag) }" + begin + res = fetch_with_redirect(uri) + render html: res.body.html_safe, content_type: res.content_type + rescue => e + render plain: "Wiki システムとの通信に失敗しました:#{ e.message }", status: 502 + end + end + + private + + def fetch_with_redirect uri_str, limit = 5 + raise 'Too many redirects' if limit == 0 + + uri = URI.parse(uri_str) + res = Net::HTTP.get_response(uri) + if res.is_a? Net::HTTPRedirection + location = res['location'] + fetch_with_redirect(location, limit - 1) + else + res + end + end +end diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 3bfc4da..35e0132 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -54,6 +54,7 @@ Rails.application.routes.draw do get "ip_addresses/destroy" get 'preview/title', to: 'preview#title' get 'preview/thumbnail', to: 'preview#thumbnail' + get '/wiki/edit/:tag', to: 'wiki_proxy#edit' root 'home#index' resources :posts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 461b912..0898647 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,8 +17,10 @@ "clsx": "^2.1.1", "humps": "^2.0.1", "lucide-react": "^0.511.0", + "marked": "^15.0.12", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-markdown-editor-lite": "^1.3.4", "react-router-dom": "^6.30.0", "tailwind-merge": "^3.3.0" }, @@ -292,6 +294,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1957,7 +1968,7 @@ "version": "19.1.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1967,7 +1978,7 @@ "version": "19.1.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -2614,6 +2625,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2711,7 +2728,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3090,6 +3107,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3800,6 +3823,18 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4361,6 +4396,21 @@ "react": "^19.1.0" } }, + "node_modules/react-markdown-editor-lite": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/react-markdown-editor-lite/-/react-markdown-editor-lite-1.3.4.tgz", + "integrity": "sha512-PhS4HzLzSgCsr8O9CfJX75nAYmZ0NwpfviLxARlT0Tau+APOerDSHSw3u9hub5wd0EqmonWibw0vhXXNu4ldRA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.2", + "classnames": "^2.2.6", + "eventemitter3": "^4.0.0", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5125,6 +5175,15 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 41651ef..6f799bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,8 +18,10 @@ "clsx": "^2.1.1", "humps": "^2.0.1", "lucide-react": "^0.511.0", + "marked": "^15.0.12", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-markdown-editor-lite": "^1.3.4", "react-router-dom": "^6.30.0", "tailwind-merge": "^3.3.0" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63be0ce..b8b0c05 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import TagSidebar from './components/TagSidebar' import PostPage from './pages/PostPage' import PostNewPage from './pages/PostNewPage' import PostDetailPage from './pages/PostDetailPage' +import WikiEditPage from './pages/WikiEditPage' import { API_BASE_URL } from './config' import axios from 'axios' import { Toaster } from '@/components/ui/toaster' @@ -76,6 +77,7 @@ const App = () => { } /> } /> } /> + } /> diff --git a/frontend/src/pages/WikiEditPage.tsx b/frontend/src/pages/WikiEditPage.tsx new file mode 100644 index 0000000..6113d4a --- /dev/null +++ b/frontend/src/pages/WikiEditPage.tsx @@ -0,0 +1,40 @@ +import { useParams } from 'react-router-dom' +import { useEffect, useState } from 'react' +import axios from 'axios' + + +const WikiEditPage = () => { + const { name } = useParams<{ name: string }> () + + const [html, setHtml] = useState('') + const [loading, setLoading] = useState(true) + + useEffect(() => { + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = '/wiki/custom.css' // Gollum の静的 CSS にアクセスできるようにしとく + document.head.appendChild(link) + return () => { + document.head.removeChild(link) + } + }, []) + + useEffect(() => { + setLoading(true) + axios + .get(`/api/wiki/edit/${encodeURIComponent(name)}`, { + headers: { 'X-Transfer-Code': localStorage.getItem('user_code') || '' } + }) + .then((res) => setHtml(res.data)) + .finally(() => setLoading(false)) + }, [name]) + + return loading ? ( +

読み込み中...

+ ) : ( +
+ ) +} + + +export default WikiEditPage