cosensee 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -2
  3. data/README.md +25 -7
  4. data/Rakefile +1 -1
  5. data/assets/js/search.js +42 -0
  6. data/assets/styles/input.css +3 -0
  7. data/exe/cosensee +36 -0
  8. data/lib/cosensee/api/page_data.rb +43 -0
  9. data/lib/cosensee/bracket_parser.rb +21 -16
  10. data/lib/cosensee/bracket_serializer.rb +3 -1
  11. data/lib/cosensee/cli/initializer.rb +64 -0
  12. data/lib/cosensee/cli/option.rb +65 -0
  13. data/lib/cosensee/cli/parser.rb +83 -0
  14. data/lib/cosensee/delegatable.rb +33 -0
  15. data/lib/cosensee/html_builder.rb +26 -17
  16. data/lib/cosensee/line_parser.rb +29 -16
  17. data/lib/cosensee/node/blank_bracket.rb +10 -0
  18. data/lib/cosensee/node/code.rb +20 -0
  19. data/lib/cosensee/node/codeblock.rb +32 -0
  20. data/lib/cosensee/node/command_line.rb +20 -0
  21. data/lib/cosensee/node/decorate_bracket.rb +23 -0
  22. data/lib/cosensee/node/double_bracket.rb +22 -0
  23. data/lib/cosensee/node/empty_bracket.rb +10 -0
  24. data/lib/cosensee/node/external_link_bracket.rb +10 -0
  25. data/lib/cosensee/node/formula_bracket.rb +10 -0
  26. data/lib/cosensee/node/gyazo_image_bracket.rb +10 -0
  27. data/lib/cosensee/node/hash_tag.rb +21 -0
  28. data/lib/cosensee/node/icon_bracket.rb +10 -0
  29. data/lib/cosensee/node/image_bracket.rb +10 -0
  30. data/lib/cosensee/node/indent.rb +27 -0
  31. data/lib/cosensee/node/internal_link_bracket.rb +10 -0
  32. data/lib/cosensee/node/link.rb +20 -0
  33. data/lib/cosensee/node/quote.rb +26 -0
  34. data/lib/cosensee/node/spotify_playlist_bracket.rb +10 -0
  35. data/lib/cosensee/node/text_bracket.rb +12 -0
  36. data/lib/cosensee/node/youtube_bracket.rb +10 -0
  37. data/lib/cosensee/page.rb +46 -16
  38. data/lib/cosensee/page_store.rb +25 -2
  39. data/lib/cosensee/parsed_line.rb +24 -6
  40. data/lib/cosensee/project.rb +21 -6
  41. data/lib/cosensee/render_class_findable.rb +13 -0
  42. data/lib/cosensee/tailwind_command.rb +24 -0
  43. data/lib/cosensee/tailwind_renderer/blank_bracket.rb +1 -1
  44. data/lib/cosensee/tailwind_renderer/code.rb +1 -1
  45. data/lib/cosensee/tailwind_renderer/codeblock.rb +22 -0
  46. data/lib/cosensee/tailwind_renderer/command_line.rb +1 -1
  47. data/lib/cosensee/tailwind_renderer/decorate_bracket.rb +1 -1
  48. data/lib/cosensee/tailwind_renderer/double_bracket.rb +2 -2
  49. data/lib/cosensee/tailwind_renderer/empty_bracket.rb +1 -1
  50. data/lib/cosensee/tailwind_renderer/external_link_bracket.rb +1 -1
  51. data/lib/cosensee/tailwind_renderer/formula_bracket.rb +1 -1
  52. data/lib/cosensee/tailwind_renderer/gyazo_image_bracket.rb +15 -0
  53. data/lib/cosensee/tailwind_renderer/hash_tag.rb +1 -1
  54. data/lib/cosensee/tailwind_renderer/icon_bracket.rb +4 -3
  55. data/lib/cosensee/tailwind_renderer/image_bracket.rb +3 -3
  56. data/lib/cosensee/tailwind_renderer/inline_svg_text.rb +37 -0
  57. data/lib/cosensee/tailwind_renderer/internal_link_bracket.rb +1 -1
  58. data/lib/cosensee/tailwind_renderer/link.rb +1 -1
  59. data/lib/cosensee/tailwind_renderer/page.rb +3 -18
  60. data/lib/cosensee/tailwind_renderer/parsed_line.rb +4 -3
  61. data/lib/cosensee/tailwind_renderer/quote.rb +2 -2
  62. data/lib/cosensee/tailwind_renderer/spotify_playlist_bracket.rb +11 -0
  63. data/lib/cosensee/tailwind_renderer/text_bracket.rb +2 -2
  64. data/lib/cosensee/tailwind_renderer/youtube_bracket.rb +11 -0
  65. data/lib/cosensee/tailwind_renderer.rb +6 -11
  66. data/lib/cosensee/user.rb +7 -1
  67. data/lib/cosensee/version.rb +1 -1
  68. data/lib/cosensee/web_content_generator.rb +77 -0
  69. data/lib/cosensee/web_server/static_file_handler.rb +43 -0
  70. data/lib/cosensee/web_server.rb +38 -0
  71. data/lib/cosensee.rb +45 -20
  72. data/templates/index.html.erb +49 -27
  73. data/templates/page.html.erb +85 -29
  74. metadata +114 -23
  75. data/lib/cosensee/blank_bracket.rb +0 -8
  76. data/lib/cosensee/code.rb +0 -18
  77. data/lib/cosensee/codeblock.rb +0 -18
  78. data/lib/cosensee/command_line.rb +0 -18
  79. data/lib/cosensee/decorate_bracket.rb +0 -16
  80. data/lib/cosensee/double_bracket.rb +0 -20
  81. data/lib/cosensee/empty_bracket.rb +0 -8
  82. data/lib/cosensee/external_link_bracket.rb +0 -8
  83. data/lib/cosensee/formula_bracket.rb +0 -8
  84. data/lib/cosensee/hash_tag.rb +0 -19
  85. data/lib/cosensee/icon_bracket.rb +0 -8
  86. data/lib/cosensee/image_bracket.rb +0 -8
  87. data/lib/cosensee/indent.rb +0 -25
  88. data/lib/cosensee/internal_link_bracket.rb +0 -8
  89. data/lib/cosensee/line.rb +0 -47
  90. data/lib/cosensee/link.rb +0 -18
  91. data/lib/cosensee/quote.rb +0 -22
  92. data/lib/cosensee/tailwind_renderer/codeblock_builder.rb +0 -37
  93. data/lib/cosensee/text_bracket.rb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca076a9a5e18e0bd0f5c1825e35714536bcfad2921b8d3e997a3fe86140314b4
4
- data.tar.gz: e28ec42e8750b9426275e78ba6b238b358217d8c97c7d95c1134a5f54f6f341c
3
+ metadata.gz: 8d86134550a97a17819c4280b1805d789f96d53eecbfb27ca62ef84a893d0064
4
+ data.tar.gz: 1e3fb5ffcc426841dd1a68a9b9b23fd9d42dcc0f8232545c97dfcf01ffbc60e5
5
5
  SHA512:
6
- metadata.gz: 72a7eb0c65b77920c3b1784db4bf58f9d984c0f660dbda56cab55ac2ac91d83ecf3e47b08ffb413d08e1601180f1258adcb141470a1dcbe5b653bd8d77232d7b
7
- data.tar.gz: 1a8bef01b367b5d8f665236e0940fb4cf823134d4cbe8a3955eab932cf76e63d21c9548b813acadfd16c5da57ee17a40cf350ed093e1c23f3955ede40862954f
6
+ metadata.gz: 73a4994a814da445c620e34ab927c5f0da3e22219a1189d588ae3361f89802f90b2e07d90e2b24239a4946d36d13b677302e01dd8cf60563aa04d558d555fc28
7
+ data.tar.gz: 4a519da076c4fafe6e82c4224c7358c1c2653d5ae95a1e64cef666c3b46cdcdcaa8020a6ba8adad4f9f77c10d9990143ccc96a7cbe94205d1f4010901b1320e9
data/.rubocop.yml CHANGED
@@ -5,6 +5,7 @@ AllCops:
5
5
  NewCops: enable
6
6
 
7
7
  require:
8
+ - rubocop-factory_bot
8
9
  - rubocop-rake
9
10
  - rubocop-rspec
10
11
 
@@ -17,7 +18,7 @@ Layout/MultilineMethodCallIndentation:
17
18
  EnforcedStyle: indented_relative_to_receiver
18
19
 
19
20
  Metrics/AbcSize:
20
- Max: 60
21
+ Max: 80
21
22
 
22
23
  Metrics/ClassLength:
23
24
  Max: 300
@@ -32,11 +33,14 @@ Metrics/MethodLength:
32
33
  Max: 80
33
34
 
34
35
  Metrics/ParameterLists:
35
- Max: 8
36
+ Max: 10
36
37
 
37
38
  Metrics/PerceivedComplexity:
38
39
  Max: 20
39
40
 
41
+ Style/FetchEnvVar:
42
+ Enabled: false
43
+
40
44
  Style/MinMaxComparison:
41
45
  Enabled: false
42
46
 
@@ -52,5 +56,12 @@ RSpec/ExampleLength:
52
56
  RSpec/MultipleExpectations:
53
57
  Enabled: false
54
58
 
59
+ RSpec/MultipleMemoizedHelpers:
60
+ Max: 7
61
+
55
62
  Style/NumericPredicate:
56
63
  Enabled: false
64
+
65
+ Style/HashSyntax:
66
+ EnforcedStyle: ruby19_no_mixed_keys
67
+ EnforcedShorthandSyntax: always
data/README.md CHANGED
@@ -14,19 +14,37 @@ If you use bundler, add to the application's Gemfile by executing:
14
14
 
15
15
  ## Usage
16
16
 
17
- To output HTML, specify the JSON file from Cosense as an argument and run `bin/build` with Cosense JSON dump file.
17
+ First, create project directory and move to it.
18
18
 
19
- $ bin/build <json file>
19
+ $ cosensee --init <project dir>
20
+ $ cd <project dir>
20
21
 
21
- The HTML files will be output to the .out directory. You can view them locally by using the bin/server command.
22
+ To output HTML, specify the JSON file from Cosense as an argument and run `cosensee` with Cosense JSON page-data file.
22
23
 
23
- $ $bin/server
24
+ $ cosensee -f <json file>
24
25
 
25
- By adding a Cosense JSON file as an argument, the build process will be executed before starting the server.
26
+ If you want to output the HTML file to a directory other than `public`, use the `-d` option.
26
27
 
27
- $ $bin/server <json file>
28
+ The HTML files will be output to the `public` directory. You can view them locally by using the `-s` option.
28
29
 
29
- The default port used by bin/server is 1212.
30
+ $ cosensee -f <json file> -s
31
+
32
+ The default port used by the local server is `1212`. If you want to change the port, use the `-p` option.
33
+
34
+ $ cosensee -f <json file> -s -p 3000
35
+
36
+ You can also use the `-h` option to display the help message.
37
+
38
+ $ cosensee --help
39
+
40
+ ## Deploy
41
+
42
+ Cosensee outputs static HTML and CSS files, so they can be deployed as-is.
43
+ Use the tools and resources specific to the environment you want to deploy to.
44
+
45
+ For example, in the case of Cloudflare Pages, you can deploy using [Wrangler](https://developers.cloudflare.com/workers/wrangler/) as follows:
46
+
47
+ $ npx wrangler pages deploy dist --project-name <cloudflare project name>
30
48
 
31
49
  ## Development
32
50
 
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ require 'rubocop/rake_task'
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
  RuboCop::RakeTask.new
9
9
 
10
- task default: :spec
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,42 @@
1
+ import Fuse from 'https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.esm.js';
2
+
3
+ export function createSearch() {
4
+ return {
5
+ query: '',
6
+ results: [],
7
+ fuse: null,
8
+ lastResults: [],
9
+
10
+ async init() {
11
+ try {
12
+ const res = await fetch("/search.json");
13
+ if (!res.ok) {
14
+ throw new Error(`Error HTTP status: ${res.status}`);
15
+ }
16
+ const data = await res.json();
17
+ this.fuse = new Fuse(data, { keys: ["title", "summary"], threshold: 0.3 });
18
+ } catch (error) {
19
+ console.error("Error fetching data:", error);
20
+ }
21
+ },
22
+
23
+ search() {
24
+ if (!this.fuse || this.query.trim() === '') {
25
+ this.clearResults();
26
+ return;
27
+ }
28
+ this.results = this.fuse.search(this.query);
29
+ this.lastResults = this.results;
30
+ },
31
+
32
+ clearResults() {
33
+ this.results = [];
34
+ },
35
+
36
+ restoreResults() {
37
+ if (this.query.trim() !== '' && this.lastResults.length > 0) {
38
+ this.results = this.lastResults;
39
+ }
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
data/exe/cosensee ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'cosensee'
6
+
7
+ require 'console'
8
+ require 'dotenv'
9
+
10
+ Dotenv.load
11
+ logger = Console.logger
12
+
13
+ option = Cosensee::CLI::Parser.parse(ARGV)
14
+ exit 1 if option.failed?
15
+
16
+ if option.init?
17
+ Cosensee::CLI::Initializer.new(option:, logger:).run
18
+ exit 0
19
+ end
20
+
21
+ begin
22
+ content_generator = Cosensee::WebContentGenerator.new(option:, renderer_class: Cosensee::TailwindRenderer, logger:, sid: ENV['CONNECT_SID'])
23
+ content_generator.generate
24
+ rescue Cosensee::WebContentGenerator::Error => e
25
+ logger.error "Error: #{e.message}"
26
+ exit 1
27
+ end
28
+
29
+ exit 0 unless option.server?
30
+
31
+ web_server = Cosensee::WebServer.new(
32
+ dir: option.output_dir,
33
+ server_url: "http://localhost:#{option.port}",
34
+ logger:
35
+ )
36
+ web_server.start
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open-uri'
4
+
5
+ module Cosensee
6
+ module Api
7
+ # for page-data API
8
+ class PageData
9
+ def export(project_name:, sid:)
10
+ uri = "https://scrapbox.io/api/page-data/export/#{project_name}.json"
11
+ send_request(uri, sid)
12
+ end
13
+
14
+ def download(project_name:, sid:, filename:)
15
+ res = export(project_name:, sid:)
16
+
17
+ begin
18
+ File.binwrite(filename, res)
19
+ rescue SystemCallError => e
20
+ raise Cosensee::Error, "Failed to write to file '#{filename}': #{e.message}"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def send_request(uri, sid)
27
+ cookies = "connect.sid=#{sid}"
28
+ parsed_uri = URI.parse(uri)
29
+
30
+ begin
31
+ response = parsed_uri.open('Cookie' => cookies)
32
+ response.read
33
+ rescue OpenURI::HTTPError => e
34
+ raise Cosensee::Error, "HTTP error while accessing #{uri}: #{e.message}"
35
+ rescue SocketError => e
36
+ raise Cosensee::Error, "Network error: #{e.message}"
37
+ rescue URI::InvalidURIError => e
38
+ raise Cosensee::Error, "Invalid URI: #{e.message}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -16,31 +16,37 @@ module Cosensee
16
16
 
17
17
  case content
18
18
  in [/\A\z/]
19
- EmptyBracket.new(content:, raw: '[]')
19
+ Node::EmptyBracket.new(content:, raw: '[]')
20
20
  in [/\A([ \t]+)\z/]
21
- BlankBracket.new(content:, blank: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
21
+ Node::BlankBracket.new(content:, blank: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
22
22
  in [/\A\$ (.*)\z/]
23
- FormulaBracket.new(content:, formula: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
23
+ Node::FormulaBracket.new(content:, formula: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
24
24
  in [/\A(.*).icon\z/]
25
- IconBracket.new(content:, icon_name: Regexp.last_match(1))
25
+ Node::IconBracket.new(content:, icon_name: Regexp.last_match(1), raw: Regexp.last_match(0))
26
+ in [%r{\A(https://www\.youtube\.com/watch\?v=([^&]+)(&.*)?)\z}]
27
+ Node::YoutubeBracket.new(content:, video_id: Regexp.last_match(2), raw: "[#{Regexp.last_match(0)}]")
26
28
  in [%r{\A(https?://[^\s]+)\s+(https?://[^\s\]]*\.(png|jpe?g|gif|svg|webp)(\?[^\s\]]+)?)\z}]
27
- ImageBracket.new(content:, link: Regexp.last_match(1), src: Regexp.last_match(2), raw: "[#{Regexp.last_match(0)}]")
29
+ Node::ImageBracket.new(content:, link: Regexp.last_match(1), src: Regexp.last_match(2), raw: "[#{Regexp.last_match(0)}]")
28
30
  in [%r{\A(https?://[^\s\]]*\.(png|jpe?g|gif|svg|webp)(\?[^\s\]]+)?)\s+(https?://[^\s]+)\z}]
29
- ImageBracket.new(content:, link: Regexp.last_match(4), src: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
31
+ Node::ImageBracket.new(content:, link: Regexp.last_match(4), src: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
30
32
  in [%r{\A(https?://[^\s\]]*\.(png|jpe?g|gif|svg|webp))\z}]
31
- ImageBracket.new(content:, link: nil, src: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
33
+ Node::ImageBracket.new(content:, link: nil, src: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
34
+ in [%r{\A(https://gyazo.com/([0-9a-f]{32})(?:/raw)?)\z}]
35
+ Node::GyazoImageBracket.new(content:, link: nil, src: Regexp.last_match(1), image_id: Regexp.last_match(2), raw: "[#{Regexp.last_match(0)}]")
36
+ in [%r{\A(https://open.spotify.com/playlist/(\w+))\z}]
37
+ Node::SpotifyPlaylistBracket.new(content:, playlist_id: Regexp.last_match(2), src: Regexp.last_match(1), raw: "[#{Regexp.last_match(0)}]")
32
38
  in [%r{\A(https?://[^ \t]*)(\s+(.+))?\z}]
33
39
  # match_external_link_precede
34
40
  anchor = Regexp.last_match(3) || Regexp.last_match(1)
35
41
  link = Regexp.last_match(1)
36
42
  raw = "[#{Regexp.last_match(0)}]"
37
- ExternalLinkBracket.new(content:, link:, anchor:, raw:)
43
+ Node::ExternalLinkBracket.new(content:, link:, anchor:, raw:)
38
44
  in [%r{\A((.*\S)\s+)?(https?://[^\s]+)\z}]
39
45
  # match_external_link_succeed
40
46
  anchor = Regexp.last_match(2) || Regexp.last_match(3)
41
47
  link = Regexp.last_match(3)
42
48
  raw = "[#{Regexp.last_match(0)}]"
43
- ExternalLinkBracket.new(content:, link:, anchor:, raw:)
49
+ Node::ExternalLinkBracket.new(content:, link:, anchor:, raw:)
44
50
  in [%r{\A([_\*/\-"#%&'\(\)~\|\+<>{},\.]+) (.+)\z}]
45
51
  # match_decorate
46
52
  deco = Regexp.last_match(1)
@@ -49,7 +55,7 @@ module Cosensee
49
55
  underlined = deco.include?('_')
50
56
  deleted = deco.include?('-')
51
57
  slanted = deco.include?('/')
52
- DecorateBracket.new(
58
+ Node::DecorateBracket.new(
53
59
  content:,
54
60
  font_size:,
55
61
  underlined:,
@@ -60,17 +66,16 @@ module Cosensee
60
66
  )
61
67
  else
62
68
  line_parser = LineParser.new
63
- data = ParsedBracket.new(content:)
64
- parsed = data
65
- .then { |d| line_parser.parse_url(d) }
66
- .then { |d| line_parser.parse_hashtag(d) }
69
+ parsed = ParsedBracket.new(content:)
70
+ .then { line_parser.parse_url(it) }
67
71
  if parsed.single_text? && parsed.content == content
68
72
  anchor = parsed.first_content
69
73
  link = make_link(anchor)
70
74
  raw = raw_string
71
- InternalLinkBracket.new(content:, link:, anchor:, raw:)
75
+ Node::InternalLinkBracket.new(content:, link:, anchor:, raw:)
72
76
  else
73
- TextBracket.new(content: parsed.content, raw: raw_string)
77
+ parsed = parsed.then { line_parser.parse_hashtag(it) }
78
+ Node::TextBracket.new(content: parsed.content, raw: raw_string)
74
79
  end
75
80
  end
76
81
  end
@@ -5,7 +5,9 @@ require 'json'
5
5
  module Cosensee
6
6
  # serializer for brackets
7
7
  module BracketSerializer
8
- def to_s = raw
8
+ def to_s
9
+ content.map(&:to_s).join
10
+ end
9
11
 
10
12
  def to_obj
11
13
  unparsed = content.map do |elem|
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ # for CLI
5
+ class CLI
6
+ # for initializer
7
+ class Initializer
8
+ def initialize(logger:, option:)
9
+ @logger = logger
10
+ @option = option
11
+ end
12
+
13
+ attr_reader :logger, :option
14
+
15
+ def run
16
+ logger.info 'Initializing...'
17
+ create_project_dir if option.init?
18
+ create_directories
19
+ create_files
20
+ logger.info 'Done!'
21
+ end
22
+
23
+ private
24
+
25
+ def create_project_dir
26
+ return if File.exist?(project_path)
27
+
28
+ FileUtils.mkdir_p(project_path)
29
+ logger.info "Created project directory: #{project_path}"
30
+ end
31
+
32
+ def create_directories
33
+ FileUtils.mkdir_p(project_path(option.output_dir))
34
+ FileUtils.mkdir_p(project_path(option.output_dir, option.css_dir))
35
+ end
36
+
37
+ def create_files
38
+ create_tailwind_config
39
+ end
40
+
41
+ def create_tailwind_config
42
+ return if File.exist?(project_path(Cosensee::TAILWIND_CONFIG_FILE))
43
+
44
+ logger.info 'Creating TailwindCSS config file...'
45
+ File.write(project_path(Cosensee::TAILWIND_CONFIG_FILE), <<~TAILWIND_CONFIG)
46
+ /** @type {import('tailwindcss').Config} */
47
+ module.exports = {
48
+ content: [
49
+ "dist/**/*.html"
50
+ ],
51
+ theme: {
52
+ extend: {},
53
+ },
54
+ plugins: [],
55
+ }
56
+ TAILWIND_CONFIG
57
+ end
58
+
59
+ def project_path(*relative_paths)
60
+ File.join(option.project_dir, *relative_paths)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ class CLI
5
+ # Option class for Cosensee::CLI
6
+ class Option
7
+ attr_accessor :port, :output_dir, :css_dir
8
+ attr_writer :failed, :server, :remote, :filename, :skip_tailwind_execution, :init, :clean
9
+
10
+ def initialize(filename: nil, remote: nil, port: DEFAULT_PORT, output_dir: DEFAULT_OUTPUT_DIR, css_dir: DEFAULT_CSS_DIR, server: nil, skip_tailwind_execution: false, init: nil, clean: false, base_url: nil)
11
+ @remote = remote
12
+ @filename = filename
13
+ @port = port
14
+ @output_dir = output_dir
15
+ @css_dir = css_dir
16
+ @server = server
17
+ @skip_tailwind_execution = skip_tailwind_execution
18
+ @failed = false
19
+ @clean = clean
20
+ @init = init
21
+ @base_url = base_url
22
+ end
23
+
24
+ def filename
25
+ @filename || ENV['COSENSEE_FILENAME']
26
+ end
27
+
28
+ def project_name
29
+ @remote || ENV['COSENSEE_PROJECT_NAME']
30
+ end
31
+
32
+ def base_url
33
+ @base_url || ENV['COSENSEE_BASE_URL']
34
+ end
35
+
36
+ def project_dir
37
+ @init || '.'
38
+ end
39
+
40
+ def remote?
41
+ !!@remote
42
+ end
43
+
44
+ def failed?
45
+ @failed
46
+ end
47
+
48
+ def server?
49
+ @server
50
+ end
51
+
52
+ def skip_tailwind_execution?
53
+ @skip_tailwind_execution
54
+ end
55
+
56
+ def clean?
57
+ @clean
58
+ end
59
+
60
+ def init?
61
+ !!@init
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Cosensee
6
+ class CLI
7
+ # Parser class for Cosensee::CLI
8
+ class Parser
9
+ attr_reader :args, :option, :op
10
+
11
+ def self.parse(args)
12
+ new(args).parse
13
+ end
14
+
15
+ def initialize(args)
16
+ @args = args
17
+ @option = Option.new
18
+ @op = OptionParser.new
19
+ end
20
+
21
+ def parse
22
+ begin
23
+ op.banner = 'Usage: bin/build [-f <filename>] [-r <project_name>]'
24
+ op.version = Cosensee::VERSION
25
+
26
+ op.on('-f FILENAME', '--file FILENAME', 'Specify the file name') do |filename|
27
+ option.filename = filename
28
+ end
29
+ op.on('-r PROJECT_NAME', '--remote PROJECT_NAME', 'Retrieve the project pages file from the remote page-data API (with env var `CONNECT_SID`)') do |project_name|
30
+ option.remote = project_name
31
+ end
32
+ op.on('-p PORT', '--port PORT', "Specify port number of web server (default: #{DEFAULT_PORT})") do |port|
33
+ option.port = port
34
+ end
35
+ op.on('-d OUTPUT_DIR', '--dir OUTPUT_DIR', "Specify directory name of generated html files(default: #{DEFAULT_OUTPUT_DIR})") do |output_dir|
36
+ option.output_dir = output_dir
37
+ end
38
+ op.on('--css-dir CSS_DIR', "Specify directory name of generated html files(default: #{DEFAULT_CSS_DIR})") do |css_dir|
39
+ option.css_dir = css_dir
40
+ end
41
+ op.on('-s', '--server', 'Serves files by running a web server locally.') do
42
+ option.server = true
43
+ end
44
+
45
+ op.on('--skip-tailwind', 'Skip TailwindCSS execution') do
46
+ option.skip_tailwind_execution = true
47
+ end
48
+
49
+ op.on('--clean', 'Make dist directory clean') do
50
+ option.clean = true
51
+ end
52
+
53
+ op.on('--init PROJECT_DIR', 'Initialize the project with default files') do |project_dir|
54
+ option.init = project_dir
55
+ end
56
+
57
+ op.parse!(args)
58
+ rescue OptionParser::MissingArgument => e
59
+ return option_error("Error: option requires an argument: #{e.args.join(' ')}")
60
+ rescue OptionParser::InvalidOption => e
61
+ return option_error("Error: invalid option: #{e.args.join(' ')}")
62
+ end
63
+
64
+ if !option.filename && !option.server? && !option.init?
65
+ return option_error('Error: filename not found. You must specify -f, or use server with -s.')
66
+ elsif !option.filename && option.remote?
67
+ return option_error('Error: project_name not found. You must not specify project name -p without -f.')
68
+ end
69
+
70
+ option
71
+ end
72
+
73
+ private
74
+
75
+ def option_error(message)
76
+ puts message
77
+ puts op.help
78
+ option.failed = true
79
+ option
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosensee
4
+ # Add `delegate` method that delegate method calls to another object
5
+ #
6
+ # Usage:
7
+ #
8
+ # ```ruby
9
+ # class Foo
10
+ # extend Delegatable
11
+ #
12
+ # def initialize(bar)
13
+ # @bar = bar
14
+ # end
15
+ #
16
+ # attr_reader :bar
17
+ #
18
+ # delegate :buz, :baz, to: :bar
19
+ # end
20
+ # ```
21
+ #
22
+ # This will define `buz` and `baz` method that call `bar.buz` and `bar.baz` respectively.
23
+ module Delegatable
24
+ def delegate(*methods, to:)
25
+ methods.each do |method|
26
+ define_method(method) do |*args, **kargs, &block|
27
+ target = send(to)
28
+ target.public_send(method, *args, **kargs, &block)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -8,16 +8,20 @@ require 'fileutils'
8
8
  module Cosensee
9
9
  # generate HTML files
10
10
  class HtmlBuilder
11
- def initialize(project, root_dir: nil)
11
+ def initialize(project, output_dir: nil, css_dir: nil, base_url: nil)
12
12
  @project = project
13
13
  @templates_dir = File.join(__dir__, '../../templates')
14
- @root_dir = root_dir || File.join(Dir.pwd, '.out')
15
- FileUtils.mkdir_p(@root_dir)
14
+ @output_dir = output_dir || File.join(Dir.pwd, Cosensee::DEFAULT_OUTPUT_DIR)
15
+ @css_dir = css_dir || Cosensee::DEFAULT_CSS_DIR
16
+ @base_url = base_url
17
+ FileUtils.mkdir_p(@output_dir)
16
18
  end
17
19
 
18
- attr_reader :project, :root_dir, :templates_dir
20
+ attr_reader :project, :output_dir, :css_dir, :templates_dir, :base_url
21
+
22
+ def build_all(clean: true)
23
+ purge_files if clean
19
24
 
20
- def build_all
21
25
  build_index(project)
22
26
 
23
27
  # build all pages
@@ -26,34 +30,39 @@ module Cosensee
26
30
  end
27
31
 
28
32
  # build all orphan (title only) pages
29
- project.page_store.orphan_page_titles.each do |title|
33
+ project.orphan_page_titles.each do |title|
30
34
  build_page_only_title(title)
31
35
  end
32
36
  end
33
37
 
34
38
  def build_index(project)
35
- template = Tilt::ErubiTemplate.new(File.join(templates_dir, 'index.html.erb'))
36
- output = template.render(nil, project:)
37
- File.write(File.join(root_dir, 'index.html'), output)
39
+ render_html(src: 'index.html.erb', to: 'index.html', project:, css_dir:, base_url:)
38
40
  end
39
41
 
40
42
  def build_page(page)
41
- template = Tilt::ErubiTemplate.new(File.join(templates_dir, 'page.html.erb'))
42
- output = template.render(nil, project:, page:, title: page.title)
43
- File.write(File.join(root_dir, page.link_path), output)
43
+ render_html(src: 'page.html.erb', to: page.link_path, project:, css_dir:, page:, title: page.title, base_url:)
44
44
  end
45
45
 
46
46
  def build_page_only_title(title)
47
- path = File.join(root_dir, "#{title.gsub(/ /, '_').gsub('/', '%2F')}.html")
47
+ path = File.join(output_dir, "#{title.gsub(/ /, '_').gsub('/', '%2F')}.html")
48
48
  return if File.exist?(path)
49
49
 
50
- template = Tilt::ErubiTemplate.new(File.join(templates_dir, 'page.html.erb'))
51
- output = template.render(nil, project:, page: nil, title:)
50
+ # make a dummy page
51
+ page = Page.new(title:, id: 0, created: Time.now, updated: Time.now, views: 0, lines: [])
52
+
53
+ template = Tilt::ErubiTemplate.new(File.join(templates_dir, 'page.html.erb'), escape_html: true)
54
+ output = template.render(nil, project:, page:, title:, css_dir:, base_url:)
52
55
  File.write(path, output)
53
56
  end
54
57
 
55
- def page_title(page)
56
- "#{page.title} | #{project.title}"
58
+ def purge_files
59
+ FileUtils.rm_rf(Dir.glob("#{output_dir}/*.html"))
60
+ end
61
+
62
+ def render_html(src:, to:, **args)
63
+ template = Tilt::ErubiTemplate.new(File.join(templates_dir, src), escape_html: true)
64
+ output = template.render(nil, **args)
65
+ File.write(File.join(output_dir, to), output)
57
66
  end
58
67
  end
59
68
  end