cosensee 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -2
- data/README.md +25 -7
- data/Rakefile +1 -1
- data/assets/js/search.js +42 -0
- data/assets/styles/input.css +3 -0
- data/exe/cosensee +36 -0
- data/lib/cosensee/api/page_data.rb +43 -0
- data/lib/cosensee/bracket_parser.rb +21 -16
- data/lib/cosensee/bracket_serializer.rb +3 -1
- data/lib/cosensee/cli/initializer.rb +64 -0
- data/lib/cosensee/cli/option.rb +65 -0
- data/lib/cosensee/cli/parser.rb +83 -0
- data/lib/cosensee/delegatable.rb +33 -0
- data/lib/cosensee/html_builder.rb +26 -17
- data/lib/cosensee/line_parser.rb +29 -16
- data/lib/cosensee/node/blank_bracket.rb +10 -0
- data/lib/cosensee/node/code.rb +20 -0
- data/lib/cosensee/node/codeblock.rb +32 -0
- data/lib/cosensee/node/command_line.rb +20 -0
- data/lib/cosensee/node/decorate_bracket.rb +23 -0
- data/lib/cosensee/node/double_bracket.rb +22 -0
- data/lib/cosensee/node/empty_bracket.rb +10 -0
- data/lib/cosensee/node/external_link_bracket.rb +10 -0
- data/lib/cosensee/node/formula_bracket.rb +10 -0
- data/lib/cosensee/node/gyazo_image_bracket.rb +10 -0
- data/lib/cosensee/node/hash_tag.rb +21 -0
- data/lib/cosensee/node/icon_bracket.rb +10 -0
- data/lib/cosensee/node/image_bracket.rb +10 -0
- data/lib/cosensee/node/indent.rb +27 -0
- data/lib/cosensee/node/internal_link_bracket.rb +10 -0
- data/lib/cosensee/node/link.rb +20 -0
- data/lib/cosensee/node/quote.rb +26 -0
- data/lib/cosensee/node/spotify_playlist_bracket.rb +10 -0
- data/lib/cosensee/node/text_bracket.rb +12 -0
- data/lib/cosensee/node/youtube_bracket.rb +10 -0
- data/lib/cosensee/page.rb +46 -16
- data/lib/cosensee/page_store.rb +25 -2
- data/lib/cosensee/parsed_line.rb +24 -6
- data/lib/cosensee/project.rb +21 -6
- data/lib/cosensee/render_class_findable.rb +13 -0
- data/lib/cosensee/tailwind_command.rb +24 -0
- data/lib/cosensee/tailwind_renderer/blank_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/code.rb +1 -1
- data/lib/cosensee/tailwind_renderer/codeblock.rb +22 -0
- data/lib/cosensee/tailwind_renderer/command_line.rb +1 -1
- data/lib/cosensee/tailwind_renderer/decorate_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/double_bracket.rb +2 -2
- data/lib/cosensee/tailwind_renderer/empty_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/external_link_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/formula_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/gyazo_image_bracket.rb +15 -0
- data/lib/cosensee/tailwind_renderer/hash_tag.rb +1 -1
- data/lib/cosensee/tailwind_renderer/icon_bracket.rb +4 -3
- data/lib/cosensee/tailwind_renderer/image_bracket.rb +3 -3
- data/lib/cosensee/tailwind_renderer/inline_svg_text.rb +37 -0
- data/lib/cosensee/tailwind_renderer/internal_link_bracket.rb +1 -1
- data/lib/cosensee/tailwind_renderer/link.rb +1 -1
- data/lib/cosensee/tailwind_renderer/page.rb +3 -18
- data/lib/cosensee/tailwind_renderer/parsed_line.rb +4 -3
- data/lib/cosensee/tailwind_renderer/quote.rb +2 -2
- data/lib/cosensee/tailwind_renderer/spotify_playlist_bracket.rb +11 -0
- data/lib/cosensee/tailwind_renderer/text_bracket.rb +2 -2
- data/lib/cosensee/tailwind_renderer/youtube_bracket.rb +11 -0
- data/lib/cosensee/tailwind_renderer.rb +6 -11
- data/lib/cosensee/user.rb +7 -1
- data/lib/cosensee/version.rb +1 -1
- data/lib/cosensee/web_content_generator.rb +77 -0
- data/lib/cosensee/web_server/static_file_handler.rb +43 -0
- data/lib/cosensee/web_server.rb +38 -0
- data/lib/cosensee.rb +45 -20
- data/templates/index.html.erb +49 -27
- data/templates/page.html.erb +85 -29
- metadata +114 -23
- data/lib/cosensee/blank_bracket.rb +0 -8
- data/lib/cosensee/code.rb +0 -18
- data/lib/cosensee/codeblock.rb +0 -18
- data/lib/cosensee/command_line.rb +0 -18
- data/lib/cosensee/decorate_bracket.rb +0 -16
- data/lib/cosensee/double_bracket.rb +0 -20
- data/lib/cosensee/empty_bracket.rb +0 -8
- data/lib/cosensee/external_link_bracket.rb +0 -8
- data/lib/cosensee/formula_bracket.rb +0 -8
- data/lib/cosensee/hash_tag.rb +0 -19
- data/lib/cosensee/icon_bracket.rb +0 -8
- data/lib/cosensee/image_bracket.rb +0 -8
- data/lib/cosensee/indent.rb +0 -25
- data/lib/cosensee/internal_link_bracket.rb +0 -8
- data/lib/cosensee/line.rb +0 -47
- data/lib/cosensee/link.rb +0 -18
- data/lib/cosensee/quote.rb +0 -22
- data/lib/cosensee/tailwind_renderer/codeblock_builder.rb +0 -37
- data/lib/cosensee/text_bracket.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d86134550a97a17819c4280b1805d789f96d53eecbfb27ca62ef84a893d0064
|
4
|
+
data.tar.gz: 1e3fb5ffcc426841dd1a68a9b9b23fd9d42dcc0f8232545c97dfcf01ffbc60e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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:
|
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
|
-
|
17
|
+
First, create project directory and move to it.
|
18
18
|
|
19
|
-
$
|
19
|
+
$ cosensee --init <project dir>
|
20
|
+
$ cd <project dir>
|
20
21
|
|
21
|
-
|
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
|
-
$
|
24
|
+
$ cosensee -f <json file>
|
24
25
|
|
25
|
-
|
26
|
+
If you want to output the HTML file to a directory other than `public`, use the `-d` option.
|
26
27
|
|
27
|
-
|
28
|
+
The HTML files will be output to the `public` directory. You can view them locally by using the `-s` option.
|
28
29
|
|
29
|
-
|
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
data/assets/js/search.js
ADDED
@@ -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
|
+
}
|
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
|
-
|
64
|
-
|
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
|
-
|
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
|
@@ -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,
|
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
|
-
@
|
15
|
-
|
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, :
|
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.
|
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
|
-
|
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
|
-
|
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(
|
47
|
+
path = File.join(output_dir, "#{title.gsub(/ /, '_').gsub('/', '%2F')}.html")
|
48
48
|
return if File.exist?(path)
|
49
49
|
|
50
|
-
|
51
|
-
|
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
|
56
|
-
"#{
|
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
|