ucode 0.1.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 +7 -0
- data/CLAUDE.md +211 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +406 -0
- data/README.md +469 -0
- data/Rakefile +18 -0
- data/TODO.new/00-README.md +66 -0
- data/TODO.new/01-pillar-terminology-alignment.md +69 -0
- data/TODO.new/02-audit-schema-design.md +255 -0
- data/TODO.new/03-directory-output-spec.md +203 -0
- data/TODO.new/04-fontist-org-contract.md +173 -0
- data/TODO.new/05-baseline-unicode17-coverage-audit.md +144 -0
- data/TODO.new/06-audit-namespace-skeleton.md +105 -0
- data/TODO.new/07-audit-models-port.md +132 -0
- data/TODO.new/08-extractors-cheap-port.md +113 -0
- data/TODO.new/09-extractors-expensive-port.md +99 -0
- data/TODO.new/10-aggregations-ucd-rewrite.md +168 -0
- data/TODO.new/11-differ-and-library-auditor-port.md +102 -0
- data/TODO.new/12-formatters-port.md +115 -0
- data/TODO.new/13-directory-emitter.md +147 -0
- data/TODO.new/14-html-face-browser.md +144 -0
- data/TODO.new/15-html-library-browser.md +102 -0
- data/TODO.new/16-cli-audit-subcommands.md +142 -0
- data/TODO.new/17-fontisan-cleanup-audit.md +147 -0
- data/TODO.new/18-fontisan-cleanup-ucd.md +156 -0
- data/TODO.new/19-fontisan-docs-update.md +155 -0
- data/TODO.new/20-canonical-resolver-4-tier.md +182 -0
- data/TODO.new/21-canonical-unicode17-build.md +148 -0
- data/TODO.new/22-implementation-order.md +176 -0
- data/UCODE_CHANGELOG.md +97 -0
- data/exe/ucode +8 -0
- data/lib/ucode/aggregator.rb +77 -0
- data/lib/ucode/audit/block_aggregator.rb +90 -0
- data/lib/ucode/audit/codepoint_range_coalescer.rb +42 -0
- data/lib/ucode/audit/context.rb +137 -0
- data/lib/ucode/audit/discrepancy_detector.rb +213 -0
- data/lib/ucode/audit/extractors/aggregations.rb +70 -0
- data/lib/ucode/audit/extractors/base.rb +21 -0
- data/lib/ucode/audit/extractors/color_capabilities.rb +143 -0
- data/lib/ucode/audit/extractors/coverage.rb +55 -0
- data/lib/ucode/audit/extractors/hinting.rb +199 -0
- data/lib/ucode/audit/extractors/identity.rb +65 -0
- data/lib/ucode/audit/extractors/licensing.rb +75 -0
- data/lib/ucode/audit/extractors/metrics.rb +108 -0
- data/lib/ucode/audit/extractors/opentype_layout.rb +71 -0
- data/lib/ucode/audit/extractors/provenance.rb +34 -0
- data/lib/ucode/audit/extractors/style.rb +88 -0
- data/lib/ucode/audit/extractors/variation_detail.rb +101 -0
- data/lib/ucode/audit/extractors.rb +31 -0
- data/lib/ucode/audit/plane_aggregator.rb +37 -0
- data/lib/ucode/audit/registry.rb +63 -0
- data/lib/ucode/audit/script_aggregator.rb +92 -0
- data/lib/ucode/audit.rb +27 -0
- data/lib/ucode/cache.rb +113 -0
- data/lib/ucode/cli.rb +272 -0
- data/lib/ucode/commands/build.rb +68 -0
- data/lib/ucode/commands/cache.rb +46 -0
- data/lib/ucode/commands/fetch.rb +62 -0
- data/lib/ucode/commands/font_coverage.rb +57 -0
- data/lib/ucode/commands/glyphs.rb +136 -0
- data/lib/ucode/commands/lookup.rb +65 -0
- data/lib/ucode/commands/parse.rb +62 -0
- data/lib/ucode/commands/site.rb +33 -0
- data/lib/ucode/commands.rb +19 -0
- data/lib/ucode/config.rb +110 -0
- data/lib/ucode/coordinator/indices.rb +34 -0
- data/lib/ucode/coordinator.rb +397 -0
- data/lib/ucode/database.rb +214 -0
- data/lib/ucode/db_builder.rb +107 -0
- data/lib/ucode/error.rb +96 -0
- data/lib/ucode/fetch/code_charts.rb +57 -0
- data/lib/ucode/fetch/http.rb +83 -0
- data/lib/ucode/fetch/ucd_zip.rb +57 -0
- data/lib/ucode/fetch/unihan_zip.rb +57 -0
- data/lib/ucode/fetch.rb +14 -0
- data/lib/ucode/glyphs/cell_extractor.rb +130 -0
- data/lib/ucode/glyphs/dvisvgm_renderer.rb +29 -0
- data/lib/ucode/glyphs/embedded_fonts/catalog.rb +372 -0
- data/lib/ucode/glyphs/embedded_fonts/content_stream_correlator.rb +228 -0
- data/lib/ucode/glyphs/embedded_fonts/font_entry.rb +126 -0
- data/lib/ucode/glyphs/embedded_fonts/renderer.rb +47 -0
- data/lib/ucode/glyphs/embedded_fonts/source.rb +94 -0
- data/lib/ucode/glyphs/embedded_fonts/svg.rb +123 -0
- data/lib/ucode/glyphs/embedded_fonts/tounicode.rb +103 -0
- data/lib/ucode/glyphs/embedded_fonts/writer.rb +76 -0
- data/lib/ucode/glyphs/embedded_fonts.rb +50 -0
- data/lib/ucode/glyphs/grid.rb +30 -0
- data/lib/ucode/glyphs/grid_detector.rb +165 -0
- data/lib/ucode/glyphs/last_resort/cmap_index.rb +96 -0
- data/lib/ucode/glyphs/last_resort/contents.rb +74 -0
- data/lib/ucode/glyphs/last_resort/glif.rb +124 -0
- data/lib/ucode/glyphs/last_resort/renderer.rb +67 -0
- data/lib/ucode/glyphs/last_resort/source.rb +125 -0
- data/lib/ucode/glyphs/last_resort/svg.rb +247 -0
- data/lib/ucode/glyphs/last_resort/writer.rb +83 -0
- data/lib/ucode/glyphs/last_resort.rb +36 -0
- data/lib/ucode/glyphs/monolith_page_map.rb +181 -0
- data/lib/ucode/glyphs/mutool_renderer.rb +28 -0
- data/lib/ucode/glyphs/page_renderer.rb +221 -0
- data/lib/ucode/glyphs/path_bbox.rb +62 -0
- data/lib/ucode/glyphs/pdf2svg_renderer.rb +26 -0
- data/lib/ucode/glyphs/pdf_fetcher.rb +102 -0
- data/lib/ucode/glyphs/pdftocairo_renderer.rb +32 -0
- data/lib/ucode/glyphs/real_fonts/block_coverage.rb +45 -0
- data/lib/ucode/glyphs/real_fonts/coverage_auditor.rb +117 -0
- data/lib/ucode/glyphs/real_fonts/font_coverage_report.rb +45 -0
- data/lib/ucode/glyphs/real_fonts/font_locator.rb +95 -0
- data/lib/ucode/glyphs/real_fonts/unicode_17_blocks.rb +104 -0
- data/lib/ucode/glyphs/real_fonts/writer.rb +50 -0
- data/lib/ucode/glyphs/real_fonts.rb +32 -0
- data/lib/ucode/glyphs/writer.rb +250 -0
- data/lib/ucode/glyphs.rb +27 -0
- data/lib/ucode/index.rb +106 -0
- data/lib/ucode/index_builder.rb +94 -0
- data/lib/ucode/models/audit/audit_axis.rb +30 -0
- data/lib/ucode/models/audit/audit_diff.rb +77 -0
- data/lib/ucode/models/audit/audit_report.rb +137 -0
- data/lib/ucode/models/audit/baseline.rb +32 -0
- data/lib/ucode/models/audit/block_summary.rb +72 -0
- data/lib/ucode/models/audit/codepoint_detail.rb +45 -0
- data/lib/ucode/models/audit/codepoint_range.rb +39 -0
- data/lib/ucode/models/audit/codepoint_set_diff.rb +34 -0
- data/lib/ucode/models/audit/color_capabilities.rb +91 -0
- data/lib/ucode/models/audit/discrepancy.rb +38 -0
- data/lib/ucode/models/audit/duplicate_group.rb +23 -0
- data/lib/ucode/models/audit/embedding_type.rb +81 -0
- data/lib/ucode/models/audit/field_change.rb +28 -0
- data/lib/ucode/models/audit/fs_selection_flags.rb +65 -0
- data/lib/ucode/models/audit/gasp_range.rb +63 -0
- data/lib/ucode/models/audit/hinting.rb +99 -0
- data/lib/ucode/models/audit/library_summary.rb +40 -0
- data/lib/ucode/models/audit/licensing.rb +48 -0
- data/lib/ucode/models/audit/metrics.rb +111 -0
- data/lib/ucode/models/audit/named_instance.rb +41 -0
- data/lib/ucode/models/audit/opentype_layout.rb +38 -0
- data/lib/ucode/models/audit/plane_summary.rb +31 -0
- data/lib/ucode/models/audit/script_coverage_row.rb +26 -0
- data/lib/ucode/models/audit/script_features.rb +28 -0
- data/lib/ucode/models/audit/script_summary.rb +54 -0
- data/lib/ucode/models/audit/variation_detail.rb +42 -0
- data/lib/ucode/models/audit.rb +50 -0
- data/lib/ucode/models/bidi_bracket_pair.rb +20 -0
- data/lib/ucode/models/bidi_mirroring.rb +19 -0
- data/lib/ucode/models/binary_property_assignment.rb +26 -0
- data/lib/ucode/models/block.rb +36 -0
- data/lib/ucode/models/case_folding_rule.rb +23 -0
- data/lib/ucode/models/cjk_radical.rb +23 -0
- data/lib/ucode/models/codepoint/bidi.rb +28 -0
- data/lib/ucode/models/codepoint/break_segmentation.rb +22 -0
- data/lib/ucode/models/codepoint/case_folding.rb +25 -0
- data/lib/ucode/models/codepoint/casing.rb +32 -0
- data/lib/ucode/models/codepoint/decomposition.rb +27 -0
- data/lib/ucode/models/codepoint/display.rb +24 -0
- data/lib/ucode/models/codepoint/emoji.rb +29 -0
- data/lib/ucode/models/codepoint/hangul.rb +20 -0
- data/lib/ucode/models/codepoint/identifier.rb +30 -0
- data/lib/ucode/models/codepoint/indic.rb +20 -0
- data/lib/ucode/models/codepoint/joining.rb +20 -0
- data/lib/ucode/models/codepoint/normalization.rb +35 -0
- data/lib/ucode/models/codepoint/numeric_value.rb +35 -0
- data/lib/ucode/models/codepoint.rb +122 -0
- data/lib/ucode/models/name_alias.rb +21 -0
- data/lib/ucode/models/named_sequence.rb +19 -0
- data/lib/ucode/models/names_list_entry.rb +38 -0
- data/lib/ucode/models/plane.rb +36 -0
- data/lib/ucode/models/property_alias.rb +24 -0
- data/lib/ucode/models/property_value_alias.rb +26 -0
- data/lib/ucode/models/relationship/compat_equiv.rb +18 -0
- data/lib/ucode/models/relationship/cross_reference.rb +17 -0
- data/lib/ucode/models/relationship/footnote.rb +24 -0
- data/lib/ucode/models/relationship/informal_alias.rb +18 -0
- data/lib/ucode/models/relationship/sample_sequence.rb +24 -0
- data/lib/ucode/models/relationship/variation_sequence.rb +19 -0
- data/lib/ucode/models/relationship.rb +57 -0
- data/lib/ucode/models/script.rb +41 -0
- data/lib/ucode/models/special_casing_rule.rb +28 -0
- data/lib/ucode/models/standardized_variant.rb +24 -0
- data/lib/ucode/models/unihan_entry.rb +23 -0
- data/lib/ucode/models.rb +47 -0
- data/lib/ucode/parsers/auxiliary.rb +26 -0
- data/lib/ucode/parsers/base.rb +137 -0
- data/lib/ucode/parsers/bidi_brackets.rb +41 -0
- data/lib/ucode/parsers/bidi_mirroring.rb +37 -0
- data/lib/ucode/parsers/blocks.rb +63 -0
- data/lib/ucode/parsers/case_folding.rb +53 -0
- data/lib/ucode/parsers/cjk_radicals.rb +102 -0
- data/lib/ucode/parsers/derived_age.rb +59 -0
- data/lib/ucode/parsers/derived_core_properties.rb +60 -0
- data/lib/ucode/parsers/extracted_properties.rb +74 -0
- data/lib/ucode/parsers/name_aliases.rb +44 -0
- data/lib/ucode/parsers/named_sequences.rb +51 -0
- data/lib/ucode/parsers/names_list.rb +250 -0
- data/lib/ucode/parsers/property_aliases.rb +41 -0
- data/lib/ucode/parsers/property_value_aliases.rb +46 -0
- data/lib/ucode/parsers/script_extensions.rb +64 -0
- data/lib/ucode/parsers/scripts.rb +60 -0
- data/lib/ucode/parsers/special_casing.rb +62 -0
- data/lib/ucode/parsers/standardized_variants.rb +56 -0
- data/lib/ucode/parsers/unicode_data/hangul_name.rb +73 -0
- data/lib/ucode/parsers/unicode_data.rb +268 -0
- data/lib/ucode/parsers/unihan.rb +125 -0
- data/lib/ucode/parsers.rb +35 -0
- data/lib/ucode/range_entry.rb +58 -0
- data/lib/ucode/repo/aggregate_writer.rb +364 -0
- data/lib/ucode/repo/atomic_writes.rb +48 -0
- data/lib/ucode/repo/codepoint_writer.rb +96 -0
- data/lib/ucode/repo/paths.rb +122 -0
- data/lib/ucode/repo.rb +22 -0
- data/lib/ucode/site/config_emitter.rb +124 -0
- data/lib/ucode/site/generator.rb +178 -0
- data/lib/ucode/site/search_index.rb +68 -0
- data/lib/ucode/site/template/.gitignore +4 -0
- data/lib/ucode/site/template/.vitepress/config.ts +8 -0
- data/lib/ucode/site/template/.vitepress/theme/index.js +20 -0
- data/lib/ucode/site/template/char/[codepoint].md +13 -0
- data/lib/ucode/site/template/components/BlockView.vue +57 -0
- data/lib/ucode/site/template/components/CharView.vue +85 -0
- data/lib/ucode/site/template/components/PlaneView.vue +56 -0
- data/lib/ucode/site/template/components/SearchView.vue +66 -0
- data/lib/ucode/site/template/index.md +25 -0
- data/lib/ucode/site/template/package.json +18 -0
- data/lib/ucode/site/template/search.md +9 -0
- data/lib/ucode/site.rb +13 -0
- data/lib/ucode/version.rb +5 -0
- data/lib/ucode/version_resolver.rb +76 -0
- data/lib/ucode.rb +74 -0
- data/ucode.gemspec +56 -0
- metadata +404 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require "ucode/repo/atomic_writes"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Site
|
|
10
|
+
# Writes `site/.vitepress/config.ts` from the JSON output tree.
|
|
11
|
+
#
|
|
12
|
+
# Reads:
|
|
13
|
+
# output/planes/<n>.json → top nav (17 planes)
|
|
14
|
+
# output/blocks/index.json → sidebar groups (one per plane)
|
|
15
|
+
#
|
|
16
|
+
# Writes:
|
|
17
|
+
# site/.vitepress/config.ts — TypeScript module that exports the
|
|
18
|
+
# Vitepress `defineConfig` payload. Idempotent via AtomicWrites.
|
|
19
|
+
#
|
|
20
|
+
# **Pure**: reads JSON, writes TS. No Vitepress dependency on the
|
|
21
|
+
# Ruby side; the site is a thin shell that imports this generated
|
|
22
|
+
# config and re-exports `defineConfig(config)`.
|
|
23
|
+
class ConfigEmitter
|
|
24
|
+
include Repo::AtomicWrites
|
|
25
|
+
|
|
26
|
+
# @param output_root [String, Pathname] dataset root
|
|
27
|
+
# @param site_root [String, Pathname] Vitepress project root
|
|
28
|
+
def initialize(output_root:, site_root:)
|
|
29
|
+
@output_root = Pathname.new(output_root)
|
|
30
|
+
@site_root = Pathname.new(site_root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Pathname] the config file path that #emit writes
|
|
34
|
+
def target_path
|
|
35
|
+
@site_root.join(".vitepress", "config.ts")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Read the output tree, render config.ts, write atomically.
|
|
39
|
+
# @return [Boolean] true if the file was (re)written, false if skipped
|
|
40
|
+
def emit
|
|
41
|
+
write_atomic(target_path, render(load_planes, load_blocks))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def load_planes
|
|
47
|
+
(0..16).map do |n|
|
|
48
|
+
path = @output_root.join("planes", "#{n}.json")
|
|
49
|
+
next nil unless path.exist?
|
|
50
|
+
|
|
51
|
+
JSON.parse(path.read).tap do |plane|
|
|
52
|
+
plane["number"] = n
|
|
53
|
+
end
|
|
54
|
+
end.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def load_blocks
|
|
58
|
+
path = @output_root.join("blocks", "index.json")
|
|
59
|
+
return [] unless path.exist?
|
|
60
|
+
|
|
61
|
+
JSON.parse(path.read)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render(planes, blocks)
|
|
65
|
+
<<~TS
|
|
66
|
+
// AUTO-GENERATED by Ucode::Site::ConfigEmitter. Do not edit.
|
|
67
|
+
import { defineConfig } from "vitepress";
|
|
68
|
+
|
|
69
|
+
export const planes = #{planes_to_ts(planes)};
|
|
70
|
+
|
|
71
|
+
export const blocks = #{blocks_to_ts(blocks)};
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
title: "ucode",
|
|
75
|
+
description: "Unicode Character Database — code charts, properties, relationships",
|
|
76
|
+
cleanUrls: true,
|
|
77
|
+
themeConfig: {
|
|
78
|
+
siteTitle: "ucode",
|
|
79
|
+
nav: [
|
|
80
|
+
{ text: "Planes", link: "/plane/0" },
|
|
81
|
+
{ text: "Search", link: "/search" },
|
|
82
|
+
],
|
|
83
|
+
sidebar: sidebar(planes, blocks),
|
|
84
|
+
socialLinks: [
|
|
85
|
+
{ icon: "github", link: "https://github.com/fontist/ucode" },
|
|
86
|
+
],
|
|
87
|
+
search: { provider: "local" },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function sidebar(planes, blocks) {
|
|
92
|
+
const groups = {};
|
|
93
|
+
for (const plane of planes) {
|
|
94
|
+
groups[plane.number] = {
|
|
95
|
+
text: plane.abbrev + " — " + plane.name,
|
|
96
|
+
items: blocks
|
|
97
|
+
.filter((b) => b.plane_number === plane.number)
|
|
98
|
+
.map((b) => ({ text: b.name, link: "/block/" + b.id })),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Sidebar per top-level section: plane pages share one sidebar,
|
|
102
|
+
// block pages get their plane's sidebar.
|
|
103
|
+
return {
|
|
104
|
+
"/plane/": Object.values(groups),
|
|
105
|
+
"/block/": groups,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
TS
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def planes_to_ts(planes)
|
|
112
|
+
JSON.pretty_generate(planes.map { |p|
|
|
113
|
+
{ number: p["number"], name: p["name"], abbrev: p["abbrev"] }
|
|
114
|
+
})
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def blocks_to_ts(blocks)
|
|
118
|
+
JSON.pretty_generate(blocks.map { |b|
|
|
119
|
+
{ id: b["id"], name: b["name"], plane_number: b["plane_number"] }
|
|
120
|
+
})
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "find"
|
|
5
|
+
require "json"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
require "ucode/repo/atomic_writes"
|
|
9
|
+
require "ucode/site/config_emitter"
|
|
10
|
+
require "ucode/site/search_index"
|
|
11
|
+
|
|
12
|
+
module Ucode
|
|
13
|
+
module Site
|
|
14
|
+
# Orchestrates `ucode site init` and `ucode site build`.
|
|
15
|
+
#
|
|
16
|
+
# **init** copies the static Vitepress template from
|
|
17
|
+
# `lib/ucode/site/template/` into the user's `site/`. The template
|
|
18
|
+
# ships package.json, theme, and the dynamic route components
|
|
19
|
+
# (`char/[codepoint].vue`, `block/[id].vue`, `plane/[n].md` stub).
|
|
20
|
+
#
|
|
21
|
+
# **build** regenerates only the parts that depend on the dataset:
|
|
22
|
+
# - `.vitepress/config.ts` — ConfigEmitter
|
|
23
|
+
# - `public/data/` — symlinked or copied from `output/`
|
|
24
|
+
# - `public/data/index/search.json` — SearchIndex
|
|
25
|
+
# - `plane/<n>.md` — one thin stub per plane (frontmatter)
|
|
26
|
+
# - `block/<id>.md` — one thin stub per block
|
|
27
|
+
#
|
|
28
|
+
# Static pages are markdown stubs that mount a Vue component; the
|
|
29
|
+
# component fetches the JSON for that plane/block at runtime. This
|
|
30
|
+
# keeps the generator cheap (~363 small writes) and the per-character
|
|
31
|
+
# route dynamic (~160k static pages is infeasible).
|
|
32
|
+
#
|
|
33
|
+
# **Idempotent**: every write goes through AtomicWrites.
|
|
34
|
+
class Generator
|
|
35
|
+
include Repo::AtomicWrites
|
|
36
|
+
|
|
37
|
+
TemplateDir = File.expand_path("template", __dir__).freeze
|
|
38
|
+
private_constant :TemplateDir
|
|
39
|
+
|
|
40
|
+
# @param output_root [String, Pathname] dataset root (read)
|
|
41
|
+
# @param site_root [String, Pathname] Vitepress project root (write)
|
|
42
|
+
def initialize(output_root:, site_root:)
|
|
43
|
+
@output_root = Pathname.new(output_root)
|
|
44
|
+
@site_root = Pathname.new(site_root)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Copy the static template into `site_root`. No-op for any file
|
|
48
|
+
# that already exists with identical content (AtomicWrites).
|
|
49
|
+
# @return [Integer] number of files written
|
|
50
|
+
def init
|
|
51
|
+
count = 0
|
|
52
|
+
each_template_file do |src, rel|
|
|
53
|
+
dst = @site_root.join(rel)
|
|
54
|
+
count += 1 if write_atomic(dst, src.read)
|
|
55
|
+
end
|
|
56
|
+
count
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Regenerate config + pages + search index from the current
|
|
60
|
+
# `output/` tree. Returns a tally of what changed.
|
|
61
|
+
# @return [Hash{Symbol => Integer}]
|
|
62
|
+
def build
|
|
63
|
+
tally = { config: 0, pages: 0, search: 0, data_link: 0 }
|
|
64
|
+
|
|
65
|
+
tally[:config] = config_emitter.emit ? 1 : 0
|
|
66
|
+
tally[:pages] = write_pages
|
|
67
|
+
tally[:search] = search_index.build ? 1 : 0
|
|
68
|
+
tally[:data_link] = link_data_dir ? 1 : 0
|
|
69
|
+
|
|
70
|
+
tally
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def config_emitter
|
|
76
|
+
ConfigEmitter.new(output_root: @output_root, site_root: @site_root)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def search_index
|
|
80
|
+
SearchIndex.new(@output_root)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Walk TemplateDir, yielding (src_path, relative_path) for each file.
|
|
84
|
+
def each_template_file(&block)
|
|
85
|
+
return unless Dir.exist?(TemplateDir)
|
|
86
|
+
|
|
87
|
+
Find.find(TemplateDir) do |src|
|
|
88
|
+
next if File.directory?(src)
|
|
89
|
+
|
|
90
|
+
rel = Pathname.new(src).relative_path_from(Pathname.new(TemplateDir))
|
|
91
|
+
yield Pathname.new(src), rel
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Write plane and block markdown stubs from the output tree.
|
|
96
|
+
def write_pages
|
|
97
|
+
count = 0
|
|
98
|
+
count += write_plane_pages
|
|
99
|
+
count += write_block_pages
|
|
100
|
+
count
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def write_plane_pages
|
|
104
|
+
planes_dir = @output_root.join("planes")
|
|
105
|
+
return 0 unless planes_dir.directory?
|
|
106
|
+
|
|
107
|
+
planes_dir.children.sort.sum do |path|
|
|
108
|
+
next 0 unless path.file? && path.extname == ".json"
|
|
109
|
+
|
|
110
|
+
plane = JSON.parse(path.read)
|
|
111
|
+
n = plane["number"]
|
|
112
|
+
next 0 unless n
|
|
113
|
+
|
|
114
|
+
payload = plane_md(n)
|
|
115
|
+
write_atomic(@site_root.join("plane", "#{n}.md"), payload) ? 1 : 0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write_block_pages
|
|
120
|
+
blocks = read_json_list(@output_root.join("blocks", "index.json"))
|
|
121
|
+
blocks.sum do |block|
|
|
122
|
+
id = block["id"]
|
|
123
|
+
next 0 unless id
|
|
124
|
+
|
|
125
|
+
payload = block_md(id)
|
|
126
|
+
write_atomic(@site_root.join("block", "#{id}.md"), payload) ? 1 : 0
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_json_list(path)
|
|
131
|
+
return [] unless path&.exist?
|
|
132
|
+
|
|
133
|
+
JSON.parse(path.read)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def plane_md(plane_number)
|
|
137
|
+
<<~MD
|
|
138
|
+
---
|
|
139
|
+
layout: plane
|
|
140
|
+
title: "Plane #{plane_number}"
|
|
141
|
+
plane: #{plane_number}
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
<PlaneView plane="#{plane_number}" />
|
|
145
|
+
MD
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def block_md(block_id)
|
|
149
|
+
<<~MD
|
|
150
|
+
---
|
|
151
|
+
layout: block
|
|
152
|
+
title: "#{block_id}"
|
|
153
|
+
block: "#{block_id}"
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
<BlockView block="#{block_id}" />
|
|
157
|
+
MD
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Vitepress serves `site/public/` at the site root. Symlink the
|
|
161
|
+
# dataset into `public/data/` so the Vue components can fetch
|
|
162
|
+
# `/data/...` URLs. Falls back to a recursive copy on filesystems
|
|
163
|
+
# that don't support symlinks.
|
|
164
|
+
def link_data_dir
|
|
165
|
+
link = @site_root.join("public", "data")
|
|
166
|
+
return 0 if link.symlink? && link.dirname.exist?
|
|
167
|
+
|
|
168
|
+
link.dirname.mkpath
|
|
169
|
+
begin
|
|
170
|
+
File.symlink(@output_root.relative_path_from(link.dirname).to_s, link.to_s)
|
|
171
|
+
rescue SystemCallError
|
|
172
|
+
FileUtils.cp_r(@output_root, link)
|
|
173
|
+
end
|
|
174
|
+
1
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require "ucode/repo/atomic_writes"
|
|
7
|
+
require "ucode/repo/paths"
|
|
8
|
+
|
|
9
|
+
module Ucode
|
|
10
|
+
module Site
|
|
11
|
+
# Builds the client-side search payload consumed by MiniSearch.
|
|
12
|
+
#
|
|
13
|
+
# Input: `output/index/labels.json` written by `Repo::AggregateWriter`
|
|
14
|
+
# — a flat `{ "U+XXXX" => { name, gc, sc } }` map (~160k entries,
|
|
15
|
+
# ~5 MB raw).
|
|
16
|
+
#
|
|
17
|
+
# Output: `output/index/search.json` — an array of `{ id, name, gc, sc }`
|
|
18
|
+
# objects, ready to feed `new MiniSearch(payload, ...)`.
|
|
19
|
+
#
|
|
20
|
+
# **Streaming**: labels.json is parsed incrementally via the stdlib
|
|
21
|
+
# JSON parser; the entire payload is materialised once for atomic
|
|
22
|
+
# write. For ~160k codepoints this peaks around ~30 MB — acceptable
|
|
23
|
+
# for a build-time tool.
|
|
24
|
+
#
|
|
25
|
+
# **Idempotent**: re-runs are byte-compared no-ops via AtomicWrites.
|
|
26
|
+
class SearchIndex
|
|
27
|
+
include Repo::AtomicWrites
|
|
28
|
+
|
|
29
|
+
# @param output_root [String, Pathname]
|
|
30
|
+
def initialize(output_root)
|
|
31
|
+
@output_root = Pathname.new(output_root)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Build and write `search.json`. Returns the entry count, or nil
|
|
35
|
+
# if labels.json is absent (nothing to index).
|
|
36
|
+
# @return [Integer, nil]
|
|
37
|
+
def build
|
|
38
|
+
labels = load_labels
|
|
39
|
+
return nil unless labels
|
|
40
|
+
|
|
41
|
+
entries = labels.map { |cp_id, meta| entry_for(cp_id, meta) }
|
|
42
|
+
payload = JSON.generate(entries)
|
|
43
|
+
write_atomic(target_path, payload)
|
|
44
|
+
entries.size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The path that #build writes to. Exposed so specs and the site
|
|
48
|
+
# generator can reference it without duplicating the convention.
|
|
49
|
+
# @return [Pathname]
|
|
50
|
+
def target_path
|
|
51
|
+
Pathname(@output_root).join("index", "search.json")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def load_labels
|
|
57
|
+
path = Repo::Paths.labels_index_path(@output_root)
|
|
58
|
+
return nil unless path.exist?
|
|
59
|
+
|
|
60
|
+
JSON.parse(path.read)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def entry_for(cp_id, meta)
|
|
64
|
+
{ id: cp_id, name: meta["name"], gc: meta["gc"], sc: meta["sc"] }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Placeholder config; replaced by `ucode site build` (which writes
|
|
2
|
+
// config.ts from the current output/ tree). Safe to delete.
|
|
3
|
+
import { defineConfig } from "vitepress";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
title: "ucode",
|
|
7
|
+
description: "Run `ucode site build` to regenerate this config from output/.",
|
|
8
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import DefaultTheme from "vitepress/theme";
|
|
2
|
+
import { defineComponent, h } from "vue";
|
|
3
|
+
import PlaneView from "../components/PlaneView.vue";
|
|
4
|
+
import BlockView from "../components/BlockView.vue";
|
|
5
|
+
import CharView from "../components/CharView.vue";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
...DefaultTheme,
|
|
9
|
+
Layout: defineComponent({
|
|
10
|
+
name: "UcodeLayout",
|
|
11
|
+
setup() {
|
|
12
|
+
return () => h(DefaultTheme.Layout, null, {});
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
enhanceApp({ app }) {
|
|
16
|
+
app.component("PlaneView", PlaneView);
|
|
17
|
+
app.component("BlockView", BlockView);
|
|
18
|
+
app.component("CharView", CharView);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: doc
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { useRoute } from "vitepress";
|
|
7
|
+
import CharView from "../components/CharView.vue";
|
|
8
|
+
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
const codepoint = route.params.codepoint;
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<CharView :codepoint="codepoint" />
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({ block: { type: String, required: true } });
|
|
5
|
+
|
|
6
|
+
const blockMeta = ref(null);
|
|
7
|
+
const cpToBlock = ref({});
|
|
8
|
+
const labels = ref({});
|
|
9
|
+
|
|
10
|
+
const cells = computed(() => {
|
|
11
|
+
if (!blockMeta.value) return [];
|
|
12
|
+
return blockMeta.value.codepoint_ids.map((cpId) => ({
|
|
13
|
+
id: cpId,
|
|
14
|
+
label: labels.value[cpId]?.name,
|
|
15
|
+
glyphUrl: `/data/blocks/${props.block}/${cpId}/glyph.svg`,
|
|
16
|
+
detailUrl: `/char/${cpId.slice(2)}`,
|
|
17
|
+
}));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
onMounted(async () => {
|
|
21
|
+
const [blockRes, labelsRes] = await Promise.all([
|
|
22
|
+
fetch(`/data/blocks/${props.block}.json`),
|
|
23
|
+
fetch(`/data/index/labels.json`),
|
|
24
|
+
]);
|
|
25
|
+
blockMeta.value = await blockRes.json();
|
|
26
|
+
labels.value = await labelsRes.json();
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="block">
|
|
32
|
+
<header v-if="blockMeta">
|
|
33
|
+
<h1>{{ blockMeta.name }}</h1>
|
|
34
|
+
<p class="lead">
|
|
35
|
+
<code>{{ blockMeta.id }}</code>
|
|
36
|
+
— U+{{ blockMeta.range_first.toString(16).toUpperCase().padStart(4, "0") }}
|
|
37
|
+
– U+{{ blockMeta.range_last.toString(16).toUpperCase().padStart(4, "0") }}
|
|
38
|
+
({{ cells.length }} codepoints)
|
|
39
|
+
</p>
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
<section class="grid">
|
|
43
|
+
<a v-for="cell in cells" :key="cell.id" :href="cell.detailUrl" class="cell">
|
|
44
|
+
<img :src="cell.glyphUrl" :alt="cell.label" loading="lazy" />
|
|
45
|
+
<span class="cp-id">{{ cell.id }}</span>
|
|
46
|
+
</a>
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.lead { color: var(--vp-c-text-2); font-family: monospace; }
|
|
53
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 1px; background: var(--vp-c-divider); border: 1px solid var(--vp-c-divider); }
|
|
54
|
+
.cell { display: flex; flex-direction: column; align-items: center; padding: 0.5rem; background: var(--vp-c-bg); text-decoration: none; }
|
|
55
|
+
.cell img { width: 64px; height: 64px; object-fit: contain; }
|
|
56
|
+
.cp-id { font-family: monospace; font-size: 0.7rem; color: var(--vp-c-text-3); margin-top: 0.25rem; }
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({ codepoint: { type: [String, Number], required: true } });
|
|
5
|
+
|
|
6
|
+
const cpData = ref(null);
|
|
7
|
+
const cpToBlock = ref({});
|
|
8
|
+
const error = ref(null);
|
|
9
|
+
|
|
10
|
+
const cpId = computed(() => {
|
|
11
|
+
const n = Number(props.codepoint);
|
|
12
|
+
return `U+${n.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const blockId = computed(() => cpToBlock.value[cpId.value]);
|
|
16
|
+
const glyphUrl = computed(() =>
|
|
17
|
+
blockId.value ? `/data/blocks/${blockId.value}/${cpId.value}/glyph.svg` : null
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
onMounted(async () => {
|
|
21
|
+
try {
|
|
22
|
+
const [mappingRes] = await Promise.all([
|
|
23
|
+
fetch(`/data/index/codepoint_to_block.json`),
|
|
24
|
+
]);
|
|
25
|
+
cpToBlock.value = await mappingRes.json();
|
|
26
|
+
if (!blockId.value) {
|
|
27
|
+
error.value = `No block found for ${cpId.value}`;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const dataRes = await fetch(`/data/blocks/${blockId.value}/${cpId.value}/index.json`);
|
|
31
|
+
if (!dataRes.ok) {
|
|
32
|
+
error.value = `No data file for ${cpId.value} (HTTP ${dataRes.status})`;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cpData.value = await dataRes.json();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
error.value = e.message;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const propertyRows = computed(() => {
|
|
42
|
+
if (!cpData.value) return [];
|
|
43
|
+
return Object.entries(cpData.value)
|
|
44
|
+
.filter(([_, v]) => v !== null && v !== undefined && v !== "")
|
|
45
|
+
.map(([k, v]) => ({ key: k, value: JSON.stringify(v) }));
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="char">
|
|
51
|
+
<header>
|
|
52
|
+
<h1><code>{{ cpId }}</code></h1>
|
|
53
|
+
<p v-if="cpData" class="name">{{ cpData.name }}</p>
|
|
54
|
+
<p v-if="error" class="error">{{ error }}</p>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<div v-if="cpData" class="body">
|
|
58
|
+
<aside v-if="glyphUrl" class="glyph">
|
|
59
|
+
<img :src="glyphUrl" :alt="cpData.name" />
|
|
60
|
+
</aside>
|
|
61
|
+
|
|
62
|
+
<section class="props">
|
|
63
|
+
<h2>Properties</h2>
|
|
64
|
+
<table>
|
|
65
|
+
<tbody>
|
|
66
|
+
<tr v-for="row in propertyRows" :key="row.key">
|
|
67
|
+
<th>{{ row.key }}</th>
|
|
68
|
+
<td><code>{{ row.value }}</code></td>
|
|
69
|
+
</tr>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</section>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.name { font-family: monospace; color: var(--vp-c-text-2); }
|
|
79
|
+
.error { color: var(--vp-c-danger-1); }
|
|
80
|
+
.body { display: grid; grid-template-columns: 200px 1fr; gap: 2rem; }
|
|
81
|
+
.glyph img { width: 100%; height: auto; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-alt); }
|
|
82
|
+
.props table { border-collapse: collapse; }
|
|
83
|
+
.props th, .props td { text-align: left; padding: 0.25rem 0.75rem; border-bottom: 1px solid var(--vp-c-divider); vertical-align: top; }
|
|
84
|
+
.props th { font-weight: 500; color: var(--vp-c-text-2); white-space: nowrap; }
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({ plane: { type: [String, Number], required: true } });
|
|
5
|
+
|
|
6
|
+
const planeMeta = ref(null);
|
|
7
|
+
const blocksIndex = ref([]);
|
|
8
|
+
|
|
9
|
+
const planeNumber = computed(() => Number(props.plane));
|
|
10
|
+
const blocksForPlane = computed(() =>
|
|
11
|
+
blocksIndex.value.filter((b) => b.plane_number === planeNumber.value)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
onMounted(async () => {
|
|
15
|
+
const [planeRes, blocksRes] = await Promise.all([
|
|
16
|
+
fetch(`/data/planes/${planeNumber.value}.json`),
|
|
17
|
+
fetch(`/data/blocks/index.json`),
|
|
18
|
+
]);
|
|
19
|
+
planeMeta.value = await planeRes.json();
|
|
20
|
+
blocksIndex.value = await blocksRes.json();
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="plane">
|
|
26
|
+
<header v-if="planeMeta">
|
|
27
|
+
<h1>
|
|
28
|
+
<code>U+{{ planeNumber.toString(16).toUpperCase().padStart(2, "0") }}0000</code>
|
|
29
|
+
–
|
|
30
|
+
<code>U+{{ planeNumber.toString(16).toUpperCase().padStart(2, "0") }}FFFF</code>
|
|
31
|
+
</h1>
|
|
32
|
+
<p class="lead">{{ planeMeta.name }} ({{ planeMeta.abbrev }})</p>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<section>
|
|
36
|
+
<h2>Blocks ({{ blocksForPlane.length }})</h2>
|
|
37
|
+
<ul class="block-list">
|
|
38
|
+
<li v-for="b in blocksForPlane" :key="b.id">
|
|
39
|
+
<a :href="`/block/${b.id}`">{{ b.name }}</a>
|
|
40
|
+
<small>
|
|
41
|
+
U+{{ b.first_cp.toString(16).toUpperCase().padStart(4, "0") }}
|
|
42
|
+
–
|
|
43
|
+
U+{{ b.last_cp.toString(16).toUpperCase().padStart(4, "0") }}
|
|
44
|
+
</small>
|
|
45
|
+
</li>
|
|
46
|
+
</ul>
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.lead { color: var(--vp-c-text-2); }
|
|
53
|
+
.block-list { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.5rem; }
|
|
54
|
+
.block-list li { padding: 0.5rem 0.75rem; border: 1px solid var(--vp-c-divider); border-radius: 4px; }
|
|
55
|
+
.block-list small { display: block; color: var(--vp-c-text-3); font-family: monospace; }
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, computed } from "vue";
|
|
3
|
+
import MiniSearch from "minisearch";
|
|
4
|
+
|
|
5
|
+
const query = ref("");
|
|
6
|
+
const results = ref([]);
|
|
7
|
+
const status = ref("loading");
|
|
8
|
+
let index = null;
|
|
9
|
+
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch("/data/index/search.json");
|
|
13
|
+
const docs = await res.json();
|
|
14
|
+
index = new MiniSearch({
|
|
15
|
+
fields: ["name", "id"],
|
|
16
|
+
storeFields: ["name", "gc", "sc"],
|
|
17
|
+
idField: "id",
|
|
18
|
+
});
|
|
19
|
+
await index.addAllAsync(docs);
|
|
20
|
+
status.value = "ready";
|
|
21
|
+
} catch (e) {
|
|
22
|
+
status.value = "error";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const run = () => {
|
|
27
|
+
if (!index || query.value.trim().length < 2) {
|
|
28
|
+
results.value = [];
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
results.value = index.search(query.value, { prefix: true, fuzzy: 0.2, limit: 50 });
|
|
32
|
+
};
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<div class="search">
|
|
37
|
+
<h1>Search</h1>
|
|
38
|
+
<p v-if="status === 'loading'">Loading index…</p>
|
|
39
|
+
<p v-else-if="status === 'error'">Failed to load search index.</p>
|
|
40
|
+
<div v-else>
|
|
41
|
+
<input
|
|
42
|
+
v-model="query"
|
|
43
|
+
@input="run"
|
|
44
|
+
placeholder="Search by name (e.g. LATIN CAPITAL LETTER A) or U+0041"
|
|
45
|
+
autofocus
|
|
46
|
+
/>
|
|
47
|
+
<p class="meta">{{ results.length }} result(s)</p>
|
|
48
|
+
<ul class="results">
|
|
49
|
+
<li v-for="r in results" :key="r.id">
|
|
50
|
+
<a :href="`/char/${r.id.slice(2)}`">
|
|
51
|
+
<code>{{ r.id }}</code> — {{ r.name }}
|
|
52
|
+
</a>
|
|
53
|
+
<small v-if="r.gc || r.sc">({{ [r.gc, r.sc].filter(Boolean).join(", ") }})</small>
|
|
54
|
+
</li>
|
|
55
|
+
</ul>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style scoped>
|
|
61
|
+
input { width: 100%; padding: 0.5rem 0.75rem; font-size: 1rem; border: 1px solid var(--vp-c-divider); border-radius: 4px; background: var(--vp-c-bg); color: var(--vp-c-text-1); }
|
|
62
|
+
.meta { color: var(--vp-c-text-3); font-size: 0.85rem; }
|
|
63
|
+
.results { list-style: none; padding: 0; }
|
|
64
|
+
.results li { padding: 0.5rem 0; border-bottom: 1px solid var(--vp-c-divider); }
|
|
65
|
+
.results small { color: var(--vp-c-text-3); margin-left: 0.5rem; }
|
|
66
|
+
</style>
|