aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-10-17 10:38:15 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-10-17 10:38:15 +0800
commitd91501af0d3860da1022e960199115c33b4a63a6 (patch)
tree7a9c02421b33dcb36460acceed381d4a116c148d
parent9c90bd1cbdc5de5b50def0eb4cb3e65e80194af3 (diff)
parent9a66411258781a57d5c953b7113403fdf0d218cf (diff)
Merge branch "addon"
-rw-r--r--.gitignore3
-rw-r--r--addon/index.mjs42
-rw-r--r--addon/manifest.json14
-rw-r--r--package.json3
-rw-r--r--scripts/rollup.config.js56
-rw-r--r--src/MenuItem.mjs12
-rw-r--r--src/css/dumbymap.css4
-rw-r--r--src/dumbymap.mjs153
-rw-r--r--src/utils.mjs32
9 files changed, 243 insertions, 76 deletions
diff --git a/.gitignore b/.gitignore
index 3e80267..b42d1dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
1dist/ 1dist/
2docs/ 2docs/
3addon/**js
4addon/**css
5!addon/index**
3doc-coverage/ 6doc-coverage/
4node_modules/ 7node_modules/
5package-lock.json 8package-lock.json
diff --git a/addon/index.mjs b/addon/index.mjs
new file mode 100644
index 0000000..b5d71ba
--- /dev/null
+++ b/addon/index.mjs
@@ -0,0 +1,42 @@
1const url = new URL(window.location)
2if (url.host === 'www.ptt.cc') {
3 const content = document.querySelector('#main-content')
4 Array.from(content.childNodes)
5 .filter(n => !(n instanceof window.HTMLElement))
6 .forEach(text => {
7 const span = document.createElement('span')
8 span.innerText = text.textContent
9 text.replaceWith(span)
10 })
11}
12
13const blockSelectors = {
14 'developer.mozilla': '.section-content',
15 'hackmd.io': '#doc > *',
16 'www.ptt.cc': '#main-content > span',
17}
18const blockSelector = blockSelectors[url.host]
19
20const addBlocks = blockSelector
21 ? root => Array.from(root.querySelectorAll(blockSelector))
22 : undefined
23
24const simpleRender = window.mapclay.renderWith(config => ({
25 use: 'Leaflet',
26 width: '100%',
27 height: '200px',
28 XYZ: 'https://tile.openstreetmap.jp/styles/osm-bright/512/{z}/{x}/{y}.png',
29 ...config,
30 aliases: {
31 use: window.mapclay.renderers,
32 ...(config.aliases ?? {}),
33 },
34}))
35
36window.generateMaps(document.querySelector('main') ?? document.body, {
37 crs: url.searchParams.get('crs') ?? 'EPSG:4326',
38 addBlocks,
39 initialLayout: '',
40 render: simpleRender,
41 autoMap: true,
42})
diff --git a/addon/manifest.json b/addon/manifest.json
index 27433b3..2d7b1ed 100644
--- a/addon/manifest.json
+++ b/addon/manifest.json
@@ -11,8 +11,15 @@
11 11
12 "content_scripts": [ 12 "content_scripts": [
13 { 13 {
14 "matches": ["*://*.mozilla.org/*"], 14 "matches": [
15 "js": ["index.mjs"], 15 "*://developer.mozilla.org/*",
16 "*://hackmd.io/*",
17 "*://*.ptt.cc/*"
18 ],
19 "js": [
20 "dumbymap.mjs",
21 "index.mjs"
22 ],
16 "css": [ 23 "css": [
17 "css/dumbymap.css" 24 "css/dumbymap.css"
18 ] 25 ]
@@ -22,6 +29,7 @@
22 "permissions": [ 29 "permissions": [
23 "activeTab", 30 "activeTab",
24 "tabs", 31 "tabs",
25 "scripting" 32 "scripting",
33 "https://epsg.io/*"
26 ] 34 ]
27} 35}
diff --git a/package.json b/package.json
index d6f8b40..392ca85 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
30 "style": "scripts/stylelint.sh", 30 "style": "scripts/stylelint.sh",
31 "docs": "jsdoc -c scripts/jsdoc.conf src/; xdg-open http://localhost:8080/docs/", 31 "docs": "jsdoc -c scripts/jsdoc.conf src/; xdg-open http://localhost:8080/docs/",
32 "prepack": "npm run lint && npm run style && npm run build", 32 "prepack": "npm run lint && npm run style && npm run build",
33 "postpack": "rm -rf dist/css dist/renderers; npm run build-resources; ln -sf `pwd`/src/css dist; cp node_modules/easymde/dist/easymde.min.css src/css; ln -sf `pwd`/node_modules/mapclay/dist/renderers dist" 33 "postpack": "rm -rf dist/css dist/renderers; npm run build-resources; ln -sf `pwd`/src/css dist; cp node_modules/easymde/dist/easymde.min.css src/css; ln -sf `pwd`/node_modules/mapclay/dist/renderers dist",
34 "addon": "cp src/css/dumbymap.css addon/css; ADDON=true rollup -c scripts/rollup.config.js"
34 }, 35 },
35 "devDependencies": { 36 "devDependencies": {
36 "@rollup/plugin-alias": "^5.1.1", 37 "@rollup/plugin-alias": "^5.1.1",
diff --git a/scripts/rollup.config.js b/scripts/rollup.config.js
index d4af05f..434bd85 100644
--- a/scripts/rollup.config.js
+++ b/scripts/rollup.config.js
@@ -5,7 +5,25 @@ import { existsSync } from 'fs'
5import { join } from 'path' 5import { join } from 'path'
6import { bundleStats } from 'rollup-plugin-bundle-stats' 6import { bundleStats } from 'rollup-plugin-bundle-stats'
7 7
8const production = !process.env.ROLLUP_WATCH 8const prod = process.env.PRODUCTION
9const addon = process.env.ADDON
10
11function resolve (file, origin) {
12 // Your way to resolve local include path
13}
14
15function pathResolve (options) {
16 return {
17 resolveId: function (file, origin) {
18 // Your local include path must either starts with `./` or `../`
19 if (file.startsWith('./') || file.startsWith('../')) {
20 // Return an absolute include path
21 return resolve(file, origin)
22 }
23 return null // Continue to the next plugins!
24 },
25 }
26}
9 27
10const general = { 28const general = {
11 output: [ 29 output: [
@@ -13,7 +31,6 @@ const general = {
13 dir: './dist', 31 dir: './dist',
14 format: 'esm', 32 format: 'esm',
15 entryFileNames: '[name].mjs', 33 entryFileNames: '[name].mjs',
16 sourcemap: 'true',
17 }, 34 },
18 ], 35 ],
19 watch: { 36 watch: {
@@ -42,12 +59,13 @@ const general = {
42 return null 59 return null
43 }, 60 },
44 }, 61 },
62 pathResolve(),
45 node(), 63 node(),
46 commonjs(), 64 commonjs(),
47 production && terser({ 65 prod && terser({
48 keep_fnames: true, 66 keep_fnames: true,
49 }), 67 }),
50 production && bundleStats(), 68 prod && bundleStats(),
51 ], 69 ],
52} 70}
53 71
@@ -60,4 +78,32 @@ export default [
60 }, 78 },
61] 79]
62 .map(config => ({ ...general, ...config })) 80 .map(config => ({ ...general, ...config }))
63 .filter((config) => production || config.input.match(/editor/)) 81 .filter(config => {
82 if (addon) return config.input.match(/dumbymap/)
83 if (!prod) return config.input.match(/editor/)
84 return true
85 })
86 .map(config => {
87 if (!addon) return config
88
89 config.output.forEach(o => { o.dir = './addon' })
90 config.plugins.push({
91 name: 'remove-exports',
92 transform (code, id) {
93 if (id.includes(config.input)) {
94 // remove export keyword for addon
95 const transformedCode = code.replace(/\n(\s*)export\s*/g, '$1')
96 return {
97 code: [
98 transformedCode,
99 'window.generateMaps = generateMaps',
100 'window.mapclay = mapclay',
101 ].join('\n'),
102 }
103 }
104 return null
105 },
106 })
107
108 return config
109 })
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs
index 7ce75f6..74b01d5 100644
--- a/src/MenuItem.mjs
+++ b/src/MenuItem.mjs
@@ -40,7 +40,9 @@ export class Item extends window.HTMLDivElement {
40 } 40 }
41 } 41 }
42} 42}
43window.customElements.define('dumby-menu-item', Item, { extends: 'div' }) 43if (!window.customElements.get('dumby-menu-item')) {
44 window.customElements.define('dumby-menu-item', Item, { extends: 'div' })
45}
44 46
45/** 47/**
46 * Basic Element for menu item that generates a submenu on hover 48 * Basic Element for menu item that generates a submenu on hover
@@ -80,7 +82,9 @@ export class Folder extends window.HTMLDivElement {
80 } 82 }
81 } 83 }
82} 84}
83window.customElements.define('menu-folder', Folder, { extends: 'div' }) 85if (!window.customElements.get('menu-folder')) {
86 window.customElements.define('menu-folder', Folder, { extends: 'div' })
87}
84 88
85/** 89/**
86 * Creates a menu item for picking a map 90 * Creates a menu item for picking a map
@@ -232,7 +236,9 @@ export class Suggestion extends Item {
232 } 236 }
233 } 237 }
234} 238}
235window.customElements.define('menu-item-suggestion', Suggestion, { extends: 'div' }) 239if (!window.customElements.get('menu-item-suggestion')) {
240 window.customElements.define('menu-item-suggestion', Suggestion, { extends: 'div' })
241}
236 242
237/** 243/**
238 * renderResults. return a menu item for reporting render results 244 * renderResults. return a menu item for reporting render results
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css
index dc1f59b..c0bb1a9 100644
--- a/src/css/dumbymap.css
+++ b/src/css/dumbymap.css
@@ -161,12 +161,12 @@ root {
161 &:hover, &.drag { 161 &:hover, &.drag {
162 background-image: none; 162 background-image: none;
163 163
164 font-weight: bolder; 164 /* font-weight: bolder; */
165 text-decoration: none; 165 text-decoration: none;
166 } 166 }
167} 167}
168 168
169.dumby-block { 169.Dumby:not([data-layout=""]) .dumby-block {
170 padding: 1rem 1rem 1rem 2rem; 170 padding: 1rem 1rem 1rem 2rem;
171 171
172 position: relative; 172 position: relative;
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 8e26ee6..706e874 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -3,7 +3,7 @@ import MarkdownItAnchor from 'markdown-it-anchor'
3import MarkdownItFootnote from 'markdown-it-footnote' 3import MarkdownItFootnote from 'markdown-it-footnote'
4import MarkdownItFrontMatter from 'markdown-it-front-matter' 4import MarkdownItFrontMatter from 'markdown-it-front-matter'
5import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' 5import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers'
6import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' 6import * as mapclay from 'mapclay'
7import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' 7import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils'
8import { Layout, SideBySide, Overlay } from './Layout' 8import { Layout, SideBySide, Overlay } from './Layout'
9import * as utils from './dumbyUtils' 9import * as utils from './dumbyUtils'
@@ -14,7 +14,7 @@ import { register, fromEPSGCode } from 'ol/proj/proj4'
14import LeaderLine from 'leader-line' 14import LeaderLine from 'leader-line'
15 15
16/** CSS Selector for main components */ 16/** CSS Selector for main components */
17const mapBlockSelector = 'pre:has(.language-map)' 17const mapBlockSelector = 'pre:has(.language-map), .mapclay-container'
18const docLinkSelector = 'a[href^="#"][title^="=>"]' 18const docLinkSelector = 'a[href^="#"][title^="=>"]'
19const geoLinkSelector = 'a[href^="geo:"]' 19const geoLinkSelector = 'a[href^="geo:"]'
20 20
@@ -86,6 +86,68 @@ export const markdown2HTML = (container, mdContent) => {
86} 86}
87 87
88/** 88/**
89 * defaultBlocks.
90 * @description Default way to get blocks from Semantic HTML
91 * @param {HTMLElement} root
92 */
93const defaultBlocks = root => {
94 const articles = root.querySelectorAll('article')
95 if (articles.length > 2) return articles
96
97 const others = Array.from(
98 root.querySelectorAll(':has(>p, >blockquote, >pre, >ul, >ol, >table, >details)'),
99 )
100 .map(e => {
101 e.classList.add('dumby-block')
102 return e
103 })
104 .filter(e => {
105 const isContained = e.parentElement.closest('.dumby-block')
106 if (isContained) e.classList.remove('dumby-block')
107 return !isContained
108 })
109
110 return others
111}
112
113/**
114 * updateAttributeByStep.
115 * @description Update data attribute by steps of map render
116 * @param {Object} - renderer which is running steps
117 */
118const updateAttributeByStep = ({ results, target, steps }) => {
119 let passNum = results.filter(
120 r => r.type === 'render' && r.state.match(/success|skip/),
121 ).length
122 const total = steps.length
123 passNum += `/${total}`
124
125 const final = results.filter(r => r.type === 'render').length === total
126
127 // FIXME HACK use MutationObserver for animation
128 if (!target.animations) target.animations = Promise.resolve()
129 target.animations = target.animations.then(async () => {
130 await new Promise(resolve => setTimeout(resolve, 100))
131 if (final) passNum += '\x20'
132 target.dataset.report = passNum
133
134 if (final) setTimeout(() => delete target.dataset.report, 100)
135 })
136}
137
138/** Get default render method by converter */
139const defaultRender = mapclay.renderWith(config => ({
140 use: config.use ?? 'Leaflet',
141 width: '100%',
142 ...config,
143 aliases: {
144 ...mapclay.defaultAliases,
145 ...(config.aliases ?? {}),
146 },
147 stepCallback: updateAttributeByStep,
148}))
149
150/**
89 * Generates maps based on the provided configuration 151 * Generates maps based on the provided configuration
90 * 152 *
91 * @param {HTMLElement} container - The container element for the maps 153 * @param {HTMLElement} container - The container element for the maps
@@ -96,30 +158,38 @@ export const markdown2HTML = (container, mdContent) => {
96 */ 158 */
97export const generateMaps = (container, { 159export const generateMaps = (container, {
98 crs = 'EPSG:4326', 160 crs = 'EPSG:4326',
161 initialLayout,
99 layouts = [], 162 layouts = [],
100 delay, 163 delay,
101 renderCallback, 164 renderCallback,
102 addBlocks = htmlHolder => Array.from(htmlHolder.querySelectorAll('article')), 165 addBlocks = defaultBlocks,
166 autoMap = false,
167 render = defaultRender,
103} = {}) => { 168} = {}) => {
104 /** Prepare Contaner/HTML-Holder/Showcase */ 169 /** Prepare Contaner */
105 container.classList.add('Dumby') 170 container.classList.add('Dumby')
106 delete container.dataset.layout 171 delete container.dataset.layout
107 container.dataset.layout = defaultLayouts[0].name 172 container.dataset.layout = initialLayout ?? defaultLayouts[0].name
108 173
109 const htmlHolder = container.querySelector('.SemanticHtml, :has(article, section)') ?? container.firstElementChild 174 /** Prepare Semantic HTML part and blocks of contents inside */
175 const htmlHolder = container.querySelector('.SemanticHtml') ??
176 Array.from(container.children).find(e => e.id?.includes('main') || e.className.includes('main')) ??
177 Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0)
110 htmlHolder.classList.add('.SemanticHtml') 178 htmlHolder.classList.add('.SemanticHtml')
179
111 const blocks = addBlocks(htmlHolder) 180 const blocks = addBlocks(htmlHolder)
112 blocks.forEach(b => { 181 blocks.forEach(b => {
113 b.classList.add('dumby-block') 182 b.classList.add('dumby-block')
114 b.dataset.total = blocks.length 183 b.dataset.total = blocks.length
115 }) 184 })
116 185
186 /** Prepare Showcase */
117 const showcase = document.createElement('div') 187 const showcase = document.createElement('div')
118 container.appendChild(showcase) 188 container.appendChild(showcase)
119 showcase.classList.add('Showcase') 189 showcase.classList.add('Showcase')
120 const renderPromises = []
121 190
122 /** Prepare Modal */ 191 /** Prepare Other Variables */
192 const renderPromises = []
123 const modalContent = document.createElement('div') 193 const modalContent = document.createElement('div')
124 container.appendChild(modalContent) 194 container.appendChild(modalContent)
125 const modal = new PlainModal(modalContent) 195 const modal = new PlainModal(modalContent)
@@ -175,13 +245,10 @@ export const generateMaps = (container, {
175 register(proj4) 245 register(proj4)
176 fromEPSGCode(crs).then(() => resolve()) 246 fromEPSGCode(crs).then(() => resolve())
177 }) 247 })
178 const addGeoSchemeByText = new Promise(resolve => { 248 const addGeoSchemeByText = (async () => {
179 const coordPatterns = [ 249 const coordPatterns = /(-?\d+\.?\d*)([,\x2F\uFF0C])(-?\d+\.?\d*)/
180 /[\x28\x5B\uFF08]\D*(-?\d+\.?\d*)([\x2F\s])(-?\d+\.?\d*)\D*[\x29\x5D\uFF09]/, 250 const re = new RegExp(coordPatterns, 'g')
181 /(-?\d+\.?\d*)([,\uFF0C])(-?\d+\.?\d*)/, 251 htmlHolder.querySelectorAll('.dumby-block')
182 ]
183 const re = new RegExp(coordPatterns.map(p => p.source).join('|'), 'g')
184 htmlHolder.querySelectorAll('p')
185 .forEach(p => { 252 .forEach(p => {
186 replaceTextNodes(p, re, match => { 253 replaceTextNodes(p, re, match => {
187 const a = document.createElement('a') 254 const a = document.createElement('a')
@@ -190,8 +257,7 @@ export const generateMaps = (container, {
190 return a 257 return a
191 }) 258 })
192 }) 259 })
193 resolve() 260 })()
194 })
195 261
196 Promise.all([setCRS, addGeoSchemeByText]).then(() => { 262 Promise.all([setCRS, addGeoSchemeByText]).then(() => {
197 Array.from(container.querySelectorAll(geoLinkSelector)) 263 Array.from(container.querySelectorAll(geoLinkSelector))
@@ -392,63 +458,26 @@ export const generateMaps = (container, {
392 const elementsWithMapConfig = Array.from( 458 const elementsWithMapConfig = Array.from(
393 container.querySelectorAll(mapBlockSelector) ?? [], 459 container.querySelectorAll(mapBlockSelector) ?? [],
394 ) 460 )
395 /** 461 if (autoMap && elementsWithMapConfig.length === 0) {
396 * updateAttributeByStep. 462 const mapContainer = document.createElement('pre')
397 * 463 mapContainer.className = 'mapclay-container'
398 * @param {Object} - renderer which is running steps 464 mapContainer.textContent = '#Created by DumbyMap'
399 */ 465 htmlHolder.insertBefore(mapContainer, htmlHolder.firstElementChild)
400 const updateAttributeByStep = ({ results, target, steps }) => { 466 elementsWithMapConfig.push(mapContainer)
401 let passNum = results.filter(
402 r => r.type === 'render' && r.state.match(/success|skip/),
403 ).length
404 const total = steps.length
405 passNum += `/${total}`
406
407 const final = results.filter(r => r.type === 'render').length === total
408
409 // FIXME HACK use MutationObserver for animation
410 if (!target.animations) target.animations = Promise.resolve()
411 target.animations = target.animations.then(async () => {
412 await new Promise(resolve => setTimeout(resolve, 100))
413 if (final) passNum += '\x20'
414 target.dataset.report = passNum
415
416 if (final) setTimeout(() => delete target.dataset.report, 100)
417 })
418 } 467 }
419 /**
420 * config converter for mapclay.renderWith()
421 *
422 * @param {Object} config
423 * @return {Object} - converted config
424 */
425 const configConverter = config => ({
426 use: config.use ?? 'Leaflet',
427 width: '100%',
428 ...config,
429 aliases: {
430 ...defaultAliases,
431 ...(config.aliases ?? {}),
432 },
433 stepCallback: updateAttributeByStep,
434 })
435
436 /** Get render method by converter */
437 const render = renderWith(configConverter)
438 468
439 /** Render each taget element for maps */ 469 /** Render each taget element for maps */
440 let order = 0 470 let order = 0
441 elementsWithMapConfig.forEach(target => { 471 elementsWithMapConfig.forEach(target => {
442 // Get text in code block starts with markdown text '```map' 472 // Get text in code block starts with markdown text '```map'
443 const configText = target 473 const configText = target
444 .querySelector('.language-map')
445 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content 474 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
446 // replace it by normal space 475 // replace it by normal space
447 .replace(/\u00A0/g, '\u0020') 476 .replace(/\u00A0/g, '\u0020')
448 477
449 let configList = [] 478 let configList = []
450 try { 479 try {
451 configList = parseConfigsFromYaml(configText).map(assignMapId) 480 configList = mapclay.parseConfigsFromYaml(configText).map(assignMapId)
452 } catch (_) { 481 } catch (_) {
453 console.warn('Fail to parse yaml config for element', target) 482 console.warn('Fail to parse yaml config for element', target)
454 return 483 return
@@ -510,7 +539,7 @@ export const generateMaps = (container, {
510 container.oncontextmenu = e => { 539 container.oncontextmenu = e => {
511 menu.replaceChildren() 540 menu.replaceChildren()
512 menu.style.display = 'block' 541 menu.style.display = 'block'
513 menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;` 542 menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;`
514 e.preventDefault() 543 e.preventDefault()
515 544
516 // Menu Items for map 545 // Menu Items for map
diff --git a/src/utils.mjs b/src/utils.mjs
index 1fe3ce5..327bee4 100644
--- a/src/utils.mjs
+++ b/src/utils.mjs
@@ -177,3 +177,35 @@ export const replaceTextNodes = (
177 node = nodeIterator.nextNode() 177 node = nodeIterator.nextNode()
178 } 178 }
179} 179}
180
181/**
182 * Get the common ancestor of two or more elements
183 * {@link https://gist.github.com/kieranbarker/cd86310d0782b7c52ce90cd7f45bb3eb}
184 * @param {String} selector A valid CSS selector
185 * @returns {Element} The common ancestor
186 */
187export function getCommonAncestor (selector) {
188 // Get the elements matching the selector
189 const elems = document.querySelectorAll(selector)
190
191 // If there are no elements, return null
192 if (elems.length < 1) return null
193
194 // If there's only one element, return it
195 if (elems.length < 2) return elems[0]
196
197 // Otherwise, create a new Range
198 const range = document.createRange()
199
200 // Start at the beginning of the first element
201 range.setStart(elems[0], 0)
202
203 // Stop at the end of the last element
204 range.setEnd(
205 elems[elems.length - 1],
206 elems[elems.length - 1].childNodes.length,
207 )
208
209 // Return the common ancestor
210 return range.commonAncestorContainer
211}