asciidoctor-epub3 1.5.0.alpha.6 → 1.5.0.alpha.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +134 -0
  3. data/Gemfile +10 -0
  4. data/README.adoc +248 -172
  5. data/asciidoctor-epub3.gemspec +42 -0
  6. data/bin/adb-push-ebook +19 -10
  7. data/data/styles/epub3-css3-only.css +28 -11
  8. data/data/styles/epub3.css +40 -39
  9. data/lib/asciidoctor-epub3/converter.rb +188 -121
  10. data/lib/asciidoctor-epub3/core_ext/string.rb +1 -1
  11. data/lib/asciidoctor-epub3/font_icon_map.rb +1 -1
  12. data/lib/asciidoctor-epub3/packager.rb +240 -104
  13. data/lib/asciidoctor-epub3/spine_item_processor.rb +22 -11
  14. data/lib/asciidoctor-epub3/version.rb +1 -1
  15. metadata +24 -35
  16. data/data/samples/asciidoctor-epub3-readme.adoc +0 -849
  17. data/data/samples/asciidoctor-js-browser-extension.adoc +0 -46
  18. data/data/samples/asciidoctor-js-introduction.adoc +0 -91
  19. data/data/samples/i18n.adoc +0 -161
  20. data/data/samples/images/asciidoctor-js-chrome-extension.png +0 -0
  21. data/data/samples/images/avatars/graphitefriction.jpg +0 -0
  22. data/data/samples/images/avatars/mogztter.jpg +0 -0
  23. data/data/samples/images/avatars/mojavelinux.jpg +0 -0
  24. data/data/samples/images/correct-text-justification.png +0 -0
  25. data/data/samples/images/incorrect-text-justification.png +0 -0
  26. data/data/samples/images/screenshots/chapter-title-day.png +0 -0
  27. data/data/samples/images/screenshots/chapter-title.png +0 -0
  28. data/data/samples/images/screenshots/figure-admonition.png +0 -0
  29. data/data/samples/images/screenshots/section-title-paragraph.png +0 -0
  30. data/data/samples/images/screenshots/sidebar.png +0 -0
  31. data/data/samples/images/screenshots/table.png +0 -0
  32. data/data/samples/images/screenshots/text.png +0 -0
  33. data/data/samples/sample-book.adoc +0 -21
  34. data/data/samples/sample-content.adoc +0 -168
  35. data/scripts/generate-font-subsets.pe +0 -235
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('lib/asciidoctor-epub3/version', File.dirname(__FILE__))
3
+ require 'open3' unless defined? Open3
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'asciidoctor-epub3'
7
+ s.version = Asciidoctor::Epub3::VERSION
8
+
9
+ s.summary = 'Converts AsciiDoc documents to EPUB3 and KF8/MOBI (Kindle) e-book formats'
10
+ s.description = <<-EOS
11
+ An extension for Asciidoctor that converts AsciiDoc documents to EPUB3 and KF8/MOBI (Kindle) e-book archives.
12
+ EOS
13
+
14
+ s.authors = ['Dan Allen', 'Sarah White']
15
+ s.email = 'dan@opendevise.com'
16
+ s.homepage = 'https://github.com/asciidoctor/asciidoctor-epub3'
17
+ s.license = 'MIT'
18
+
19
+ s.required_ruby_version = '>= 1.9.3'
20
+
21
+ files = begin
22
+ (result = Open3.popen3('git ls-files -z') {|_, out| out.read }.split %(\0)).empty? ? Dir['**/*'] : result
23
+ rescue
24
+ Dir['**/*']
25
+ end
26
+ s.files = files.grep %r/^(?:(?:data\/(?:fonts|images|styles)|lib)\/.+|Gemfile|Rakefile|(?:CHANGELOG|LICENSE|NOTICE|README)\.adoc|#{s.name}\.gemspec)$/
27
+ s.executables = %w(asciidoctor-epub3 adb-push-ebook)
28
+ s.test_files = s.files.grep(/^(?:test|spec|feature)\/.*$/)
29
+
30
+ s.require_paths = ['lib']
31
+
32
+ s.has_rdoc = true
33
+ s.rdoc_options = ['--charset=UTF-8', '--title="Asciidoctor EPUB3"', '--main=README.adoc', '-ri']
34
+ s.extra_rdoc_files = ['CHANGELOG.adoc', 'LICENSE.adoc', 'NOTICE.adoc', 'README.adoc']
35
+
36
+ s.add_development_dependency 'rake'
37
+ #s.add_development_dependency 'rdoc', '~> 4.1.0'
38
+
39
+ s.add_runtime_dependency 'asciidoctor', '~> 1.5.0'
40
+ s.add_runtime_dependency 'gepub', '~> 0.6.9.2'
41
+ s.add_runtime_dependency 'thread_safe', '~> 0.3.6'
42
+ end
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  ADB = ENV['ADB'] || 'adb'
4
+ TARGETS = {
5
+ '.epub' => '/sdcard/',
6
+ '.mobi' => '/sdcard/Android/data/com.amazon.kindle/files/'
7
+ }
4
8
 
5
9
  unless ::File.executable? ADB
6
10
  warn %(adb-push-ebook: `adb` not found.\nPlease set the ADB environment variable or add `adb` to your PATH.)
@@ -10,16 +14,21 @@ end
10
14
  require 'open3'
11
15
  require 'shellwords'
12
16
 
13
- docname = ARGV[0] || '_output/sample-book'
17
+ payload_file = ARGV[0] || '_output/sample-book'
14
18
 
15
- targets = {
16
- 'epub' => '/sdcard/',
17
- 'mobi' => '/sdcard/Android/data/com.amazon.kindle/files/'
18
- }
19
+ if (payload_file_ext = File.extname payload_file).empty?
20
+ transfers = TARGETS.map do |(ext, target_dir)|
21
+ {
22
+ src: %(#{payload_file}#{ext}),
23
+ dest: target_dir
24
+ }
25
+ end
26
+ else
27
+ transfers = [{ src: payload_file, dest: TARGETS[payload_file_ext] }]
28
+ end
19
29
 
20
- targets.each {|(ext, target)|
21
- file = %(#{docname}.#{ext})
22
- Open3.popen2e(Shellwords.join [ADB, 'push', file, target]) {|input, output, wait_thr|
30
+ transfers.each do |transfer|
31
+ Open3.popen2e(Shellwords.join [ADB, 'push', transfer[:src], transfer[:dest]]) do |input, output, wait_thr|
23
32
  output.each {|line| puts line }
24
- } if File.file? file
25
- }
33
+ end if File.file? transfer[:src]
34
+ end
@@ -1,17 +1,30 @@
1
- /* Gitden & Namo default to 16px font-size; bump it to 20px (125%) */
1
+ /* @page is for EPUB2 only */
2
+ @page {
3
+ margin: 0;
4
+ }
5
+
6
+ body.calibre-desktop {
7
+ padding: 20pt 0 !important;
8
+ }
9
+
10
+ body.calibre-desktop > section {
11
+ margin: 0 25pt;
12
+ }
13
+
14
+ /* Gitden & Namo default to 16px font-size; bump it to 19px (118.75%) */
2
15
  body.gitden-reader,
3
16
  body.namo-epub-library {
4
- font-size: 125%;
17
+ font-size: 118.75%;
5
18
  }
6
19
 
7
- /* Gitden doesn't give us much margin, so let's match Kindle */
20
+ /* Gitden doesn't give us much margin, so let's roughly match Aldiko and Kindle (narrow setting) */
8
21
  body.gitden-reader {
9
- margin: 0 25pt;
22
+ margin: 0 5pt !important;
10
23
  }
11
24
 
12
25
  /* Namo has the same margin problem, except setting side margins doesn't work */
13
26
  /*body.namo-epub-library > section.chapter {
14
- margin: 0 25pt;
27
+ margin: 0 5pt;
15
28
  }*/
16
29
 
17
30
  /* Use tighter margins and smaller font (18px) on phones (Nexus 4 and smaller) */
@@ -22,15 +35,19 @@ only screen and (max-device-width: 1280px) and (max-device-height: 768px) {
22
35
  font-size: 112.5%;
23
36
  }
24
37
 
25
- body.gitden-reader {
26
- margin: 0 5pt;
27
- }
38
+ /*body.gitden-reader {
39
+ margin: 0 5pt !important;
40
+ }*/
28
41
 
29
42
  /*body.namo-epub-library > section.chapter {
30
43
  margin: 0 5pt;
31
44
  }*/
32
45
  }
33
46
 
47
+ body.gitden-reader pre {
48
+ white-space: pre-wrap !important; /* Gitden must be applying white-space: pre !important */
49
+ }
50
+
34
51
  body h1, body h2, body h3:not(.list-heading), body h4, body h5, body h6,
35
52
  h1 :not(code), h2 :not(code), h3:not(.list-heading) :not(code), h4 :not(code), h5 :not(code), h6 :not(code) {
36
53
  /* !important required to override custom font setting in Kindle / Gitden / Namo */
@@ -79,7 +96,7 @@ body code, body kbd, body pre, pre :not(code) {
79
96
  h1.chapter-title .subtitle > b:last-child {
80
97
  padding-right: 0;
81
98
  }
82
-
99
+
83
100
  h1.chapter-title .subtitle::after {
84
101
  display: table;
85
102
  content: ' ';
@@ -104,13 +121,13 @@ body code, body kbd, body pre, pre :not(code) {
104
121
  .icon-1_5x {
105
122
  padding: 0 0.25em;
106
123
  -webkit-transform: scale(1.5, 1.5);
107
- transform: scale(1.5, 1.5);
124
+ transform: scale(1.5, 1.5);
108
125
  }
109
126
 
110
127
  .icon-2x {
111
128
  padding: 0 0.5em;
112
129
  -webkit-transform: scale(2, 2);
113
- transform: scale(2, 2);
130
+ transform: scale(2, 2);
114
131
  }
115
132
 
116
133
  .icon-small {
@@ -25,7 +25,7 @@ html {
25
25
  body {
26
26
  padding: 0 !important;
27
27
  /* add margin to ~ match Kindle's narrow setting */
28
- /* don't use !important on margin as it breaks calibre */
28
+ /* don't use !important on margin as it interferes with reader overrides (Calibre and Kindle) */
29
29
  margin: 0;
30
30
  font-size: 100%;
31
31
  /* NOTE putting optimizeLegibility on the body slows down rendering considerably */
@@ -38,10 +38,10 @@ html body {
38
38
  background-color: #FFFFFF;
39
39
  }
40
40
 
41
- /* sets minimum margin permitted */
42
- /* @page not supported by Kindle or GitDen */
41
+ /* @page only applies to EPUB2 readers; not supported by EPUB3 readers such as Kindle and Gitden */
43
42
  @page {
44
- /* push the top & bottom margins down in Aldiko to emulate Kindle (Kindle uses ~ 10% of screen by default )*/
43
+ /* sets minimum margin permitted */
44
+ /* pushes the top & bottom margins down in Aldiko to emulate Kindle (Kindle uses ~ 10% of screen by default )*/
45
45
  margin: 1cm;
46
46
  }
47
47
 
@@ -895,7 +895,7 @@ blockquote footer {
895
895
 
896
896
  blockquote footer .context {
897
897
  font-size: 0.9em;
898
- letter-spacing: -0.1em;
898
+ letter-spacing: -0.05em;
899
899
  color: #666665;
900
900
  }
901
901
 
@@ -916,8 +916,7 @@ pre {
916
916
  margin-top: 1em; /* 0.85rem */
917
917
  /*margin-top: 1.176em;*/ /* 1rem */
918
918
  white-space: pre-wrap;
919
- /*word-break: break-all;*/ /* break at the end of the line, no matter what */
920
- word-wrap: break-word; /* break in middle of long word if no other break opportunities are available */
919
+ overflow-wrap: break-word; /* break in middle of long word if no other break opportunities are available */
921
920
  font-size: 0.85em;
922
921
  line-height: 1.4; /* matches what Kindle uses and can't go less */
923
922
  background-color: #F2F2F2;
@@ -985,28 +984,28 @@ aside[class~="admonition"] {
985
984
  }
986
985
 
987
986
  aside.note {
988
- border-left-color: #B3B3B1;
989
- background-color: #E1E1E1; /* 25% opacity of border */
987
+ border-left-color: #FFC14F;
988
+ background-color: #FFF0D4; /* 25% opacity of border */
990
989
  }
991
990
 
992
991
  aside.tip {
993
- border-left-color: #57AD68;
994
- background-color: #D4EAD9; /* 25% opacity of border */
992
+ border-left-color: #40403E;
993
+ background-color: #D0D0CF; /* 25% opacity of border */
995
994
  }
996
995
 
997
996
  aside.caution {
998
- border-left-color: #666665;
999
- background-color: #D8D8D8; /* 25% opacity of border */
997
+ border-left-color: #7F7F7D;
998
+ background-color: #DFDFDF; /* 25% opacity of border */
1000
999
  }
1001
1000
 
1002
1001
  aside.warning {
1003
1002
  border-left-color: #C83737;
1004
- background-color: #F1CCCC; /* 25% opacity of border */
1003
+ background-color: #F1CECE; /* 25% opacity of border */
1005
1004
  }
1006
1005
 
1007
1006
  aside.important {
1008
- border-left-color: #FFC14F;
1009
- background-color: #FFEFD2; /* 25% opacity of border */
1007
+ border-left-color: #F2642A;
1008
+ background-color: #FCD9CA; /* 25% opacity of border */
1010
1009
  }
1011
1010
 
1012
1011
  aside.admonition::before {
@@ -1018,7 +1017,7 @@ aside.admonition::before {
1018
1017
  text-align: center;
1019
1018
  margin-bottom: -0.25em;
1020
1019
  margin-left: -0.5em;
1021
- text-shadow: 0px 1px 2px rgba(102, 102, 101, 0.3);
1020
+ text-shadow: 0px 1px 1px rgba(102, 102, 101, 0.15);
1022
1021
  }
1023
1022
 
1024
1023
  aside.admonition > div.content {
@@ -1039,41 +1038,38 @@ aside[class~="admonition"] > div[class~="content"] {
1039
1038
  }
1040
1039
 
1041
1040
  aside.note::before {
1042
- /*content: "\f0f4";*/ /* fa-coffee */
1043
1041
  content: "\f040"; /* fa-pencil */
1044
- color: #B3B3B1; /* 179,179,177 */
1042
+ color: #FFC14F;
1045
1043
  }
1046
1044
 
1047
1045
  aside[class~="note"] > div[class~="content"] {
1048
- background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #B3B3B1 45%, #B3B3B1 55%, rgba(255,255,255,0) 57.5%);
1049
- background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #B3B3B1 45%, #B3B3B1 55%, rgba(255,255,255,0) 57.5%);
1046
+ background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #FFC14F 45%, #FFC14F 55%, rgba(255,255,255,0) 57.5%);
1047
+ background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #FFC14F 45%, #FFC14F 55%, rgba(255,255,255,0) 57.5%);
1050
1048
  }
1051
1049
 
1052
1050
  aside.tip::before {
1053
- /*content: "\f069";*/ /* fa-asterisk */
1054
- /*content: "\f0d6";*/ /* fa-money */
1055
- content: "\f15a"; /* fa-bitcoin */
1056
- color: #57AD68; /* 87,173,104 */
1051
+ content: "\f0eb"; /* fa-lightbulb-o */
1052
+ color: #40403E;
1057
1053
  }
1058
1054
 
1059
1055
  aside[class~="tip"] > div[class~="content"] {
1060
- background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #57AD68 45%, #57AD68 55%, rgba(255,255,255,0) 57.5%);
1061
- background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #57AD68 45%, #57AD68 55%, rgba(255,255,255,0) 57.5%);
1056
+ background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #40403E 45%, #40403E 55%, rgba(255,255,255,0) 57.5%);
1057
+ background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #40403E 45%, #40403E 55%, rgba(255,255,255,0) 57.5%);
1062
1058
  }
1063
1059
 
1064
1060
  aside.caution::before {
1065
1061
  content: "\f0c2"; /* fa-cloud */
1066
- color: #666665; /* 102,102,101 */
1062
+ color: #7F7F7D;
1067
1063
  }
1068
1064
 
1069
1065
  aside[class~="caution"] > div[class~="content"] {
1070
- background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #666665 45%, #666665 55%, rgba(255,255,255,0) 57.5%);
1071
- background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #666665 45%, #666665 55%, rgba(255,255,255,0) 57.5%);
1066
+ background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #7F7F7D 45%, #7F7F7D 55%, rgba(255,255,255,0) 57.5%);
1067
+ background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #7F7F7D 45%, #7F7F7D 55%, rgba(255,255,255,0) 57.5%);
1072
1068
  }
1073
1069
 
1074
1070
  aside.warning::before {
1075
1071
  content: "\f0e7"; /* fa-bolt */
1076
- color: #C83737; /* 200,55,55 */
1072
+ color: #C83737;
1077
1073
  }
1078
1074
 
1079
1075
  aside[class~="warning"] > div[class~="content"] {
@@ -1083,12 +1079,12 @@ aside[class~="warning"] > div[class~="content"] {
1083
1079
 
1084
1080
  aside.important::before {
1085
1081
  content: "\f12a"; /* fa-exclamation */
1086
- color: #FFC14F; /* 255,193,79 */
1082
+ color: #F2642A;
1087
1083
  }
1088
1084
 
1089
1085
  aside[class~="important"] > div[class~="content"] {
1090
- background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #FFC14F 45%, #FFC14F 55%, rgba(255,255,255,0) 57.5%);
1091
- background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #FFC14F 45%, #FFC14F 55%, rgba(255,255,255,0) 57.5%);
1086
+ background-image: -webkit-linear-gradient(left, rgba(255,255,255,0) 42.5%, #F2642A 45%, #F2642A 55%, rgba(255,255,255,0) 57.5%);
1087
+ background-image: linear-gradient(to right, rgba(255,255,255,0) 42.5%, #F2642A 45%, #F2642A 55%, rgba(255,255,255,0) 57.5%);
1092
1088
  }
1093
1089
 
1094
1090
  aside.admonition > h2 {
@@ -1137,13 +1133,18 @@ table.table thead th {
1137
1133
  border-bottom: 1px solid #80807F;
1138
1134
  }
1139
1135
 
1140
- table.table td > p {
1136
+ table.table td > p,
1137
+ table.table div.embed > * {
1141
1138
  margin-top: 0;
1139
+ }
1140
+
1141
+ table.table td > p {
1142
1142
  text-align: left;
1143
1143
  }
1144
1144
 
1145
1145
  /* REVIEW */
1146
- table.table td > p + p {
1146
+ table.table td > p + p,
1147
+ table.table div.embed > * + * {
1147
1148
  margin-top: 1em;
1148
1149
  }
1149
1150
 
@@ -1167,7 +1168,7 @@ table.table-framed-sides {
1167
1168
 
1168
1169
  table.table-grid th,
1169
1170
  table.table-grid td {
1170
- border-width: 0 1px 1px 0;
1171
+ border-width: 0 1px 1px 0;
1171
1172
  border-style: solid;
1172
1173
  border-color: #80807F;
1173
1174
  }
@@ -1183,14 +1184,14 @@ table.table-grid tbody tr:last-child > td {
1183
1184
 
1184
1185
  table.table-grid-rows tbody th,
1185
1186
  table.table-grid-rows tbody td {
1186
- border-width: 1px 0 0 0;
1187
+ border-width: 1px 0 0 0;
1187
1188
  border-style: solid;
1188
1189
  border-color: #80807F;
1189
1190
  }
1190
1191
 
1191
1192
  table.table-grid-cols th,
1192
1193
  table.table-grid-cols td {
1193
- border-width: 0 1px 0 0;
1194
+ border-width: 0 1px 0 0;
1194
1195
  border-style: solid;
1195
1196
  border-color: #80807F;
1196
1197
  }
@@ -4,9 +4,6 @@ require_relative 'font_icon_map'
4
4
 
5
5
  module Asciidoctor
6
6
  module Epub3
7
- # tried 8288, but it didn't work in older readers
8
- WordJoiner = [65279].pack 'U*'
9
- WordJoinerRx = RUBY_ENGINE_JRUBY ? /\uFEFF/ : WordJoiner
10
7
 
11
8
  # Public: The main converter for the epub3 backend that handles packaging the
12
9
  # EPUB3 or KF8 publication file.
@@ -25,15 +22,23 @@ class Converter
25
22
  @extract = false
26
23
  end
27
24
 
28
- def convert spine_doc, name = nil
29
- @validate = true if spine_doc.attr? 'ebook-validate'
30
- @extract = true if spine_doc.attr? 'ebook-extract'
31
- Packager.new spine_doc, (spine_doc.references[:spine_items] || [spine_doc]), spine_doc.attributes['ebook-format'].to_sym
25
+ def convert node, name = nil
26
+ if (name ||= node.node_name) == 'document'
27
+ @validate = node.attr? 'ebook-validate'
28
+ @extract = node.attr? 'ebook-extract'
29
+ @compress = node.attr 'ebook-compress'
30
+ Packager.new node, (node.references[:spine_items] || [node]), node.attributes['ebook-format'].to_sym
31
+ # converting an element from the spine document, such as an inline node in the doctitle
32
+ elsif name.start_with? 'inline_'
33
+ (@content_converter ||= ::Asciidoctor::Converter::Factory.default.create('epub3-xhtml5')).convert node, name
34
+ else
35
+ raise ::ArgumentError, %(Encountered unexpected node in epub3 package converter: #{name})
36
+ end
32
37
  end
33
38
 
34
39
  # FIXME we have to package in write because we don't have access to target before this point
35
40
  def write packager, target
36
- packager.package validate: @validate, extract: @extract, target: target
41
+ packager.package validate: @validate, extract: @extract, compress: @compress, target: target
37
42
  nil
38
43
  end
39
44
  end
@@ -45,14 +50,14 @@ class ContentConverter
45
50
 
46
51
  register_for 'epub3-xhtml5'
47
52
 
48
- WordJoiner = Epub3::WordJoiner
49
- EOL = "\n"
53
+ EOL = %(\n)
50
54
  NoBreakSpace = '&#xa0;'
51
55
  ThinNoBreakSpace = '&#x202f;'
52
56
  RightAngleQuote = '&#x203a;'
57
+ CalloutStartNum = %(\u2460)
53
58
 
54
59
  XmlElementRx = /<\/?.+?>/
55
- CharEntityRx = /&#(\d{2,5});/
60
+ CharEntityRx = /&#(\d{2,6});/
56
61
  NamedEntityRx = /&([A-Z]+);/
57
62
  UppercaseTagRx = /<(\/)?([A-Z]+)>/
58
63
 
@@ -80,7 +85,7 @@ class ContentConverter
80
85
  basebackend 'html'
81
86
  outfilesuffix '.xhtml'
82
87
  htmlsyntax 'xml'
83
- @xrefs_used = ::Set.new
88
+ @xrefs_seen = ::Set.new
84
89
  @icon_names = []
85
90
  end
86
91
 
@@ -88,36 +93,10 @@ class ContentConverter
88
93
  if respond_to?(name ||= node.node_name)
89
94
  send name, node
90
95
  else
91
- warn %(conversion missing in epub3 backend for #{name})
96
+ warn %(asciidoctor: WARNING: conversion missing in epub3 backend for #{name})
92
97
  end
93
98
  end
94
99
 
95
- # TODO aggregate authors of spine document into authors attribute(s) on main document
96
- def navigation_document node, spine
97
- doctitle_sanitized = (node.doctitle sanitize: true, use_fallback: true).gsub WordJoinerRx, ''
98
- lines = [%(<!DOCTYPE html>
99
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = (node.attr 'lang', 'en')}" lang="#{lang}">
100
- <head>
101
- <meta charset="UTF-8"/>
102
- <title>#{doctitle_sanitized}</title>
103
- <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
104
- <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
105
- </head>
106
- <body>
107
- <h1>#{doctitle_sanitized}</h1>
108
- <nav epub:type="toc" id="toc">
109
- <h2>#{node.attr 'toc-title'}</h2>
110
- <ol>)]
111
- spine.each do |item|
112
- lines << %(<li><a href="#{item.id || (item.attr 'docname')}.xhtml">#{(item.doctitle sanitize: true, use_fallback: true).gsub WordJoinerRx, ''}</a></li>)
113
- end
114
- lines << %(</ol>
115
- </nav>
116
- </body>
117
- </html>)
118
- lines * EOL
119
- end
120
-
121
100
  def document node
122
101
  docid = node.id
123
102
 
@@ -131,19 +110,22 @@ class ContentConverter
131
110
  subtitle = doctitle.combined
132
111
  end
133
112
 
134
- doctitle_sanitized = (node.doctitle sanitize: true, use_fallback: true).gsub WordJoinerRx, ''
135
- subtitle_formatted = subtitle.gsub(WordJoinerRx, '').split(' ').map {|w| %(<b>#{w}</b>) } * ' '
136
- # FIXME make this uppercase routine more intelligent, less fragile
113
+ doctitle_sanitized = doctitle.combined
114
+ subtitle_formatted = subtitle.split.map {|w| %(<b>#{w}</b>) } * ' '
115
+ # FIXME use uppercase pcdata helper to make less fragile (see logic in Asciidoctor PDF)
137
116
  subtitle_formatted_upper = subtitle_formatted.upcase
138
117
  .gsub(UppercaseTagRx) { %(<#{$1}#{$2.downcase}>) }
139
118
  .gsub(NamedEntityRx) { %(&#{$1.downcase};) }
140
119
 
141
- author = node.attr 'author'
142
- username = node.attr 'username', 'default'
143
- # FIXME needs to resolve to the imagesdir of the spine document, not this document
144
- #imagesdir = (node.attr 'imagesdir', '.').chomp '/'
145
- #imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
146
- imagesdir = 'images/'
120
+ if (node.attr 'publication-type', 'book') == 'book'
121
+ byline = nil
122
+ else
123
+ author = node.attr 'author'
124
+ username = node.attr 'username', 'default'
125
+ imagesdir = (node.references[:spine].attr 'imagesdir', '.').chomp '/'
126
+ imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
127
+ byline = %(<p class="byline"><img src="#{imagesdir}avatars/#{username}.jpg"/> <b class="author">#{author}</b></p>#{EOL})
128
+ end
147
129
 
148
130
  mark_last_paragraph node
149
131
  content = node.content
@@ -176,14 +158,12 @@ class ContentConverter
176
158
  <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
177
159
  <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
178
160
  #{icon_css_head}<script type="text/javascript">
179
- document.addEventListener('DOMContentLoaded', function(event) {
180
- var epubReader = navigator.epubReadingSystem;
181
- if (!epubReader) {
182
- if (window.parent == window || !(epubReader = window.parent.navigator.epubReadingSystem)) {
183
- return;
184
- }
161
+ document.addEventListener('DOMContentLoaded', function(event, reader) {
162
+ if (!(reader = navigator.epubReadingSystem)) {
163
+ if (navigator.userAgent.indexOf(' calibre/') >= 0) reader = { name: 'calibre-desktop' };
164
+ else if (window.parent == window || !(reader = window.parent.navigator.epubReadingSystem)) return;
185
165
  }
186
- document.body.setAttribute('class', epubReader.name.toLowerCase().replace(/ /g, '-'));
166
+ document.body.setAttribute('class', reader.name.toLowerCase().replace(/ /g, '-'));
187
167
  });
188
168
  </script>
189
169
  </head>
@@ -191,8 +171,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
191
171
  <section class="chapter" title="#{doctitle_sanitized.gsub '"', '&quot;'}" epub:type="chapter" id="#{docid}">
192
172
  #{icon_css_scoped}<header>
193
173
  <div class="chapter-header">
194
- <p class="byline"><img src="#{imagesdir}avatars/#{username}.jpg"/> <b class="author">#{author}</b></p>
195
- <h1 class="chapter-title">#{title_upper}#{subtitle ? %[ <small class="subtitle">#{subtitle_formatted_upper}</small>] : nil}</h1>
174
+ #{byline}<h1 class="chapter-title">#{title_upper}#{subtitle ? %[ <small class="subtitle">#{subtitle_formatted_upper}</small>] : nil}</h1>
196
175
  </div>
197
176
  </header>
198
177
  #{content})]
@@ -219,6 +198,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
219
198
  lines * EOL
220
199
  end
221
200
 
201
+ # NOTE embedded is used for AsciiDoc table cell content
202
+ def embedded node
203
+ node.content
204
+ end
205
+
222
206
  def section node
223
207
  hlevel = node.level + 1
224
208
  epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : nil
@@ -248,9 +232,16 @@ document.addEventListener('DOMContentLoaded', function(event) {
248
232
  end
249
233
  end
250
234
 
251
- # QUESTION use convert_content?
252
235
  def open node
253
- node.content
236
+ id_attr = node.id ? %( id="#{node.id}") : nil
237
+ class_attr = node.role ? %( class="#{node.role}") : nil
238
+ if id_attr || class_attr
239
+ %(<div#{id_attr}#{class_attr}>
240
+ #{convert_content node}
241
+ </div>)
242
+ else
243
+ convert_content node
244
+ end
254
245
  end
255
246
 
256
247
  def abstract node
@@ -263,11 +254,10 @@ document.addEventListener('DOMContentLoaded', function(event) {
263
254
  role = node.role
264
255
  # stack-head is the alternative to the default, inline-head (where inline means "run-in")
265
256
  head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
257
+ # FIXME promote regexp to constant
266
258
  head = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ /[[:punct:]]$/ ? head_stop : nil}</strong> ) : nil
267
259
  if role
268
- if node.has_role? 'signature'
269
- node.set_option 'hardbreaks'
270
- end
260
+ node.set_option 'hardbreaks' if node.has_role? 'signature'
271
261
  %(<p class="#{role}">#{head}#{node.content}</p>)
272
262
  else
273
263
  %(<p>#{head}#{node.content}</p>)
@@ -330,15 +320,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
330
320
  def listing node
331
321
  figure_classes = ['listing']
332
322
  figure_classes << 'coalesce' if node.option? 'unbreakable'
333
- pre_classes = if node.style == 'source'
334
- ['source', %(language-#{node.attr 'language'})]
335
- else
336
- ['screen']
337
- end
323
+ pre_classes = node.style == 'source' ? ['source', %(language-#{node.attr 'language'})] : ['screen']
338
324
  title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>
339
325
  ) : nil
340
326
  # patches conums to fix extra or missing leading space
341
- # TODO apply this patch upstream to Asciidoctor
327
+ # TODO remove patch once upgrading to Asciidoctor 1.5.6
342
328
  %(<figure class="#{figure_classes * ' '}">
343
329
  #{title_div}<pre class="#{pre_classes * ' '}"><code>#{(node.content || '').gsub(/(?<! )<i class="conum"| +<i class="conum"/, ' <i class="conum"')}</code></pre>
344
330
  </figure>)
@@ -359,11 +345,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
359
345
 
360
346
  def quote node
361
347
  footer_content = []
362
- if attribution = (node.attr 'attribution')
363
- footer_content << attribution
348
+ if (attribution = node.attr 'attribution')
349
+ footer_content << attribution
364
350
  end
365
351
 
366
- if citetitle = (node.attr 'citetitle')
352
+ if (citetitle = node.attr 'citetitle')
367
353
  citetitle_sanitized = xml_sanitize citetitle
368
354
  footer_content << %(<cite title="#{citetitle_sanitized}">#{citetitle}</cite>)
369
355
  end
@@ -386,11 +372,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
386
372
 
387
373
  def verse node
388
374
  footer_content = []
389
- if attribution = (node.attr 'attribution')
390
- footer_content << attribution
375
+ if (attribution = node.attr 'attribution')
376
+ footer_content << attribution
391
377
  end
392
378
 
393
- if citetitle = (node.attr 'citetitle')
379
+ if (citetitle = node.attr 'citetitle')
394
380
  citetitle_sanitized = xml_sanitize citetitle
395
381
  footer_content << %(<cite title="#{citetitle_sanitized}">#{citetitle}</cite>)
396
382
  end
@@ -409,6 +395,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
409
395
  title = node.title
410
396
  title_sanitized = xml_sanitize title
411
397
  title_attr = %( title="#{title_sanitized}")
398
+ # FIXME use uppercase pcdata helper to make less fragile (see logic in Asciidoctor PDF)
412
399
  title_upper = title.upcase.gsub(NamedEntityRx) { %(&#{$1.downcase};) }
413
400
  title_el = %(<h2>#{title_upper}</h2>
414
401
  )
@@ -444,10 +431,10 @@ document.addEventListener('DOMContentLoaded', function(event) {
444
431
  end
445
432
  table_class_attr = %( class="#{table_classes * ' '}")
446
433
  table_styles = []
447
- unless node.option? 'autowidth'
448
- table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
434
+ unless (node.option? 'autowidth') && !(node.attr? 'width', nil, false)
435
+ table_styles << %(width: #{node.attr 'tablepcwidth'}%)
449
436
  end
450
- table_style_attr = table_styles.size > 0 ? %( style="#{table_styles * ' '}") : nil
437
+ table_style_attr = table_styles.size > 0 ? %( style="#{table_styles * '; '}") : nil
451
438
 
452
439
  lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
453
440
  lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
@@ -460,7 +447,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
460
447
  end
461
448
  #else
462
449
  # node.columns.each do |col|
463
- # lines << %(<col style="width: #{col.attr 'colpcwidth'}%;"/>)
450
+ # lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>)
464
451
  # end
465
452
  #end
466
453
  lines << '</colgroup>'
@@ -474,7 +461,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
474
461
  else
475
462
  case cell.style
476
463
  when :asciidoc
477
- cell_content = %(<div>#{cell.content}</div>)
464
+ cell_content = %(<div class="embed">#{cell.content}</div>)
478
465
  when :verse
479
466
  cell_content = %(<div class="verse">#{cell.text}</div>)
480
467
  when :literal
@@ -498,7 +485,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
498
485
  cell_class_attr = cell_classes.size > 0 ? %( class="#{cell_classes * ' '}") : nil
499
486
  cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : nil
500
487
  cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil
501
- cell_style_attr = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : nil
488
+ cell_style_attr = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'}") : nil
502
489
  lines << %(<#{cell_tag_name}#{cell_class_attr}#{cell_colspan_attr}#{cell_rowspan_attr}#{cell_style_attr}>#{cell_content}</#{cell_tag_name}>)
503
490
  end
504
491
  lines << '</tr>'
@@ -515,7 +502,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
515
502
  def colist node
516
503
  lines = ['<div class="callout-list">
517
504
  <ol>']
518
- num = "\u2460"
505
+ num = CalloutStartNum
519
506
  node.items.each_with_index do |item, i|
520
507
  lines << %(<li><i class="conum" data-value="#{i + 1}">#{num}</i> #{item.text}</li>)
521
508
  num = num.next
@@ -545,7 +532,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
545
532
  lines << '<li>'
546
533
  if dd
547
534
  # NOTE: must wrap remaining text in a span to help webkit justify the text properly
548
- lines << %(<span class="principal">#{subject_element}#{dd.text? ? %[ <span class="supporting">#{dd.text}</span>] : nil}</span>)
535
+ lines << %(<span class="principal">#{subject_element}#{dd.text? ? %[ <span class="supporting">#{dd.text}</span>] : nil}</span>)
549
536
  lines << dd.content if dd.blocks?
550
537
  else
551
538
  lines << %(<span class="principal">#{subject_element}</span>)
@@ -611,7 +598,6 @@ document.addEventListener('DOMContentLoaded', function(event) {
611
598
  def ulist node
612
599
  complex = false
613
600
  div_classes = ['itemized-list', node.style, node.role].compact
614
- # TODO could strip WordJoiner if brief since not using justify
615
601
  ul_classes = [node.style, ((node.option? 'brief') ? 'brief' : nil)].compact
616
602
  ul_class_attr = ul_classes.empty? ? nil : %( class="#{ul_classes * ' '}")
617
603
  id_attribute = node.id ? %( id="#{node.id}") : nil
@@ -642,7 +628,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
642
628
  img_attrs = [%(alt="#{node.attr 'alt'}")]
643
629
  case type
644
630
  when 'svg'
645
- img_attrs << %(style="width: #{node.attr 'scaledwidth', '100%'};")
631
+ img_attrs << %(style="width: #{node.attr 'scaledwidth', '100%'}")
646
632
  # TODO make this a convenience method on document
647
633
  epub_properties = (node.document.attr 'epub-properties') || []
648
634
  unless epub_properties.include? 'svg'
@@ -651,7 +637,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
651
637
  end
652
638
  else
653
639
  if node.attr? 'scaledwidth'
654
- img_attrs << %(style="width: #{node.attr 'scaledwidth'};")
640
+ img_attrs << %(style="width: #{node.attr 'scaledwidth'}")
655
641
  end
656
642
  end
657
643
  =begin
@@ -664,7 +650,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
664
650
  # img_attrs << %(width="#{node.attr 'scaledheight'}" height="#{node.attr 'scaledheight'}")
665
651
  # ePub3
666
652
  elsif node.attr? 'scaledheight'
667
- img_attrs << %(height="#{node.attr 'scaledheight'}" style="max-height: #{node.attr 'scaledheight'} !important;")
653
+ img_attrs << %(height="#{node.attr 'scaledheight'}" style="max-height: #{node.attr 'scaledheight'} !important")
668
654
  else
669
655
  # Aldiko doesn't not scale width to 100% by default
670
656
  img_attrs << %(width="100%")
@@ -682,22 +668,58 @@ document.addEventListener('DOMContentLoaded', function(event) {
682
668
  def inline_anchor node
683
669
  target = node.target
684
670
  case node.type
685
- when :xref
686
- refid = (node.attr 'refid') || target
687
- id_attr = unless @xrefs_used.include? refid
688
- @xrefs_used << refid
689
- %( id="xref-#{refid}")
671
+ when :xref # TODO would be helpful to know what type the target is (e.g., bibref)
672
+ doc, refid, text, path = node.document, ((node.attr 'refid') || target), node.text, (node.attr 'path')
673
+ # NOTE if path is non-nil, we have an inter-document xref
674
+ # QUESTION should we drop the id attribute for an inter-document xref?
675
+ if path
676
+ # ex. chapter-id#section-id
677
+ if node.attr 'fragment'
678
+ refdoc_id, refdoc_refid = refid.split '#', 2
679
+ if refdoc_id == refdoc_refid
680
+ target = target[0...(target.index '#')]
681
+ id_attr = %( id="xref--#{refdoc_id}")
682
+ else
683
+ id_attr = %( id="xref--#{refdoc_id}--#{refdoc_refid}")
684
+ end
685
+ # ex. chapter-id#
686
+ else
687
+ refdoc_id = refdoc_refid = refid
688
+ # inflate key to spine item root (e.g., transform chapter-id to chapter-id#chapter-id)
689
+ refid = %(#{refid}##{refid})
690
+ id_attr = %( id="xref--#{refdoc_id}")
691
+ end
692
+ id_attr = nil unless @xrefs_seen.add? refid
693
+ refdoc = doc.references[:spine_items].find {|it| refdoc_id == (it.id || (it.attr 'docname')) }
694
+ if refdoc
695
+ if (reftext = refdoc.references[:ids][refdoc_refid])
696
+ text ||= reftext
697
+ else
698
+ warn %(asciidoctor: WARNING: #{::File.basename(doc.attr 'docfile')}: invalid reference to unknown anchor in #{refdoc_id} chapter: #{refdoc_refid})
699
+ end
700
+ else
701
+ warn %(asciidoctor: WARNING: #{::File.basename(doc.attr 'docfile')}: invalid reference to anchor in unknown chapter: #{refdoc_id})
702
+ end
703
+ else
704
+ id_attr = (@xrefs_seen.add? refid) ? %( id="xref-#{refid}") : nil
705
+ if (reftext = doc.references[:ids][refid])
706
+ text ||= reftext
707
+ else
708
+ # FIXME we get false negatives for reference to bibref
709
+ warn %(asciidoctor: WARNING: #{::File.basename(doc.attr 'docfile')}: invalid reference to unknown local anchor (or valid bibref): #{refid})
710
+ end
690
711
  end
691
- # FIXME seems like text should be prepared already
692
- # FIXME would be nice to know what type the target is (e.g., bibref)
693
- text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
694
- %(<a#{id_attr} href="#{target}" class="xref">#{text}</a>#{WordJoiner})
712
+ %(<a#{id_attr} href="#{target}" class="xref">#{text || "[#{refid}]"}</a>)
695
713
  when :ref
696
714
  %(<a id="#{target}"></a>)
697
715
  when :link
698
- %(<a href="#{target}" class="link">#{node.text}</a>#{WordJoiner})
716
+ %(<a href="#{target}" class="link">#{node.text}</a>)
699
717
  when :bibref
700
- %(<a id="#{target}" href="#xref-#{target}">[#{target}]</a>#{WordJoiner})
718
+ if @xrefs_seen.include? target
719
+ %(<a id="#{target}" href="#xref-#{target}">[#{target}]</a>)
720
+ else
721
+ %(<a id="#{target}"></a>[#{target}])
722
+ end
701
723
  end
702
724
  end
703
725
 
@@ -706,11 +728,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
706
728
  end
707
729
 
708
730
  def inline_button node
709
- %(<b class="button">[<span class="label">#{node.text}</span>]</b>#{WordJoiner})
731
+ %(<b class="button">[<span class="label">#{node.text}</span>]</b>)
710
732
  end
711
733
 
712
734
  def inline_callout node
713
- num = "\u2460"
735
+ num = CalloutStartNum
714
736
  int_num = node.text.to_i
715
737
  (int_num - 1).times { num = num.next }
716
738
  %(<i class="conum" data-value="#{int_num}">#{num}</i>)
@@ -725,7 +747,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
725
747
  end
726
748
 
727
749
  def inline_image node
728
- if (type = node.type) == 'icon'
750
+ if node.type == 'icon'
729
751
  @icon_names << (icon_name = node.target)
730
752
  i_classes = ['icon', %(i-#{icon_name})]
731
753
  i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
@@ -748,7 +770,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
748
770
  if (keys = node.attr 'keys').size == 1
749
771
  %(<kbd>#{keys[0]}</kbd>)
750
772
  else
751
- key_combo = keys.map {|key| %(<kbd>#{key}</kbd>+) }.join.chop
773
+ key_combo = keys.map {|key| %(<kbd>#{key}</kbd>) }.join '+'
752
774
  %(<span class="keyseq">#{key_combo}</span>)
753
775
  end
754
776
  end
@@ -770,11 +792,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
770
792
  def inline_quoted node
771
793
  case node.type
772
794
  when :strong
773
- %(<strong>#{node.text}</strong>#{WordJoiner})
795
+ %(<strong>#{node.text}</strong>)
774
796
  when :emphasis
775
- %(<em>#{node.text}</em>#{WordJoiner})
797
+ %(<em>#{node.text}</em>)
776
798
  when :monospaced
777
- %(<code class="literal">#{node.text}</code>#{WordJoiner})
799
+ %(<code class="literal">#{node.text}</code>)
778
800
  when :double
779
801
  #%(&#x201c;#{node.text}&#x201d;)
780
802
  %(“#{node.text}”)
@@ -782,28 +804,26 @@ document.addEventListener('DOMContentLoaded', function(event) {
782
804
  #%(&#x2018;#{node.text}&#x2019;)
783
805
  %(‘#{node.text}’)
784
806
  when :superscript
785
- %(<sup>#{node.text}</sup>#{WordJoiner})
807
+ %(<sup>#{node.text}</sup>)
786
808
  when :subscript
787
- %(<sub>#{node.text}</sub>#{WordJoiner})
809
+ %(<sub>#{node.text}</sub>)
788
810
  else
789
811
  node.text
790
812
  end
791
813
  end
792
814
 
793
815
  def convert_content node
794
- if node.content_model == :simple
795
- %(<p>#{node.content}</p>)
796
- else
797
- node.content
798
- end
816
+ node.content_model == :simple ? %(<p>#{node.content}</p>) : node.content
799
817
  end
800
818
 
819
+ # FIXME merge into with xml_sanitize helper
801
820
  def xml_sanitize value, target = :attribute
802
- sanitized = (value.include? '<') ? value.gsub(XmlElementRx, '').tr_s(' ', ' ').strip : value
821
+ sanitized = (value.include? '<') ? value.gsub(XmlElementRx, '').strip.tr_s(' ', ' ') : value
803
822
  if target == :plain && (sanitized.include? ';')
804
- sanitized = sanitized.gsub(CharEntityRx) { [$1.to_i].pack('U*') }.gsub(FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap)
823
+ sanitized = sanitized.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if sanitized.include? '&#'
824
+ sanitized = sanitized.gsub(FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap)
805
825
  elsif target == :attribute
806
- sanitized = sanitized.gsub(WordJoinerRx, '').gsub('"', '&quot;')
826
+ sanitized = sanitized.gsub '"', '&quot;' if sanitized.include? '"'
807
827
  end
808
828
  sanitized
809
829
  end
@@ -822,17 +842,58 @@ document.addEventListener('DOMContentLoaded', function(event) {
822
842
  end
823
843
 
824
844
  class DocumentIdGenerator
845
+ ReservedIds = %w(cover nav ncx)
846
+ CharRefRx = /&(?:([a-zA-Z]{2,})|#(\d{2,6})|#x([a-fA-F0-9]{2,5}));/
847
+ if defined? __dir__
848
+ InvalidIdCharsRx = /[^\p{Word}]+/
849
+ LeadingDigitRx = /^\p{Nd}/
850
+ else
851
+ InvalidIdCharsRx = /[^[:word:]]+/
852
+ LeadingDigitRx = /^[[:digit:]]/
853
+ end
825
854
  class << self
826
- def generate_id doc
855
+ def generate_id doc, pre = nil, sep = nil
856
+ synthetic = false
827
857
  unless (id = doc.id)
828
- id = if doc.header?
829
- doc.doctitle(sanitize: true).gsub(WordJoinerRx, '').downcase.delete(':').tr_s(' ', '-').tr_s('-', '-')
858
+ # NOTE we assume pre is a valid ID prefix and that pre and sep only contain valid ID chars
859
+ pre ||= '_'
860
+ sep = sep ? sep.chr : '_'
861
+ if doc.header?
862
+ id = doc.doctitle sanitize: true
863
+ id = id.gsub CharRefRx do
864
+ $1 ? ($1 == 'amp' ? 'and' : sep) : ((d = $2 ? $2.to_i : $3.hex) == 8217 ? '' : ([d].pack 'U*'))
865
+ end if id.include? '&'
866
+ id = id.downcase.gsub InvalidIdCharsRx, sep
867
+ if id.empty?
868
+ id, synthetic = nil, true
869
+ else
870
+ unless sep.empty?
871
+ if (id = id.tr_s sep, sep).end_with? sep
872
+ if id == sep
873
+ id, synthetic = nil, true
874
+ else
875
+ id = (id.start_with? sep) ? id[1..-2] : id.chop
876
+ end
877
+ elsif id.start_with? sep
878
+ id = id[1..-1]
879
+ end
880
+ end
881
+ unless synthetic
882
+ if pre.empty?
883
+ id = %(_#{id}) if LeadingDigitRx =~ id
884
+ elsif !(id.start_with? pre)
885
+ id = %(#{pre}#{id})
886
+ end
887
+ end
888
+ end
830
889
  elsif (first_section = doc.first_section)
831
- first_section.id
890
+ id = first_section.id
832
891
  else
833
- %(document-#{doc.object_id})
892
+ synthetic = true
834
893
  end
894
+ id = %(#{pre}document#{sep}#{doc.object_id}) if synthetic
835
895
  end
896
+ warn %(asciidoctor: ERROR: chapter uses a reserved ID: #{id}) if !synthetic && (ReservedIds.include? id)
836
897
  id
837
898
  end
838
899
  end
@@ -854,14 +915,20 @@ Extensions.register do
854
915
  when 'epub3', 'kf8'
855
916
  # all good
856
917
  when 'mobi'
857
- document.attributes['ebook-format'] = 'kf8'
918
+ ebook_format = document.attributes['ebook-format'] = 'kf8'
858
919
  else
920
+ # QUESTION should we display a warning?
859
921
  ebook_format = document.attributes['ebook-format'] = 'epub3'
860
922
  end
861
923
  document.attributes[%(ebook-format-#{ebook_format})] = ''
862
924
  # Only fire SpineItemProcessor for top-level include directives
863
925
  include_processor SpineItemProcessor.new(document)
864
- treeprocessor { process {|doc| doc.id = DocumentIdGenerator.generate_id doc } }
926
+ treeprocessor do
927
+ process do |doc|
928
+ doc.id = DocumentIdGenerator.generate_id doc, (doc.attr 'idprefix'), (doc.attr 'idseparator')
929
+ nil
930
+ end
931
+ end
865
932
  end
866
933
  end
867
934
  end