rspress-plugin-map 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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # rspress-plugin-map
2
+
3
+ Rspress v2 插件,用于在文章中插入交互式地图,支持 Google 地图、高德地图、百度地图、Geoq 地图和 OpenStreetMap。
4
+
5
+ ## 特性
6
+
7
+ - 支持多种地图类型:混合地图(可切换多个图层)、Google 地图、高德地图、百度地图、Geoq 地图、OpenStreetMap
8
+ - 支持标记点和提示文本
9
+ - 支持自定义地图容器宽高、缩放等级、经纬度等参数
10
+ - 使用 HTML 标签方式调用,简单易用
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install rspress-plugin-map --save
16
+ ```
17
+
18
+ ## 配置
19
+
20
+ 在 `rspress.config.ts` 中添加插件配置:
21
+
22
+ ```typescript
23
+ import { defineConfig } from 'rspress/config';
24
+ import pluginMap from 'rspress-plugin-map';
25
+
26
+ export default defineConfig({
27
+ plugins: [
28
+ pluginMap()
29
+ ]
30
+ });
31
+ ```
32
+
33
+ ## 使用
34
+
35
+ 在 Markdown 文件中使用 `<rspress-map>` 标签插入地图:
36
+
37
+ ### 基本用法
38
+
39
+ ```html
40
+ <rspress-map></rspress-map>
41
+ ```
42
+
43
+ ### 自定义参数
44
+
45
+ 所有参数都是可选的,未指定时使用默认值:
46
+
47
+ ```html
48
+ <rspress-map
49
+ type="hybrid"
50
+ lat="39.9042"
51
+ lng="116.4074"
52
+ zoom="12"
53
+ width="100%"
54
+ height="500px"
55
+ marker="true"
56
+ marker-text="北京市"
57
+ ></rspress-map>
58
+ ```
59
+
60
+ ### 参数说明
61
+
62
+ | 参数 | 说明 | 默认值 | 可选值 |
63
+ |------|------|--------|--------|
64
+ | type | 地图类型 | hybrid | hybrid, google, gaode, baidu, geoq, openstreet |
65
+ | lat | 纬度 | 39.9042 | 数值 |
66
+ | lng | 经度 | 116.4074 | 数值 |
67
+ | zoom | 缩放等级 | 10 | 1-18 |
68
+ | width | 容器宽度 | 100% | CSS 尺寸值 |
69
+ | height | 容器高度 | 400px | CSS 尺寸值 |
70
+ | marker | 是否显示标记点 | true | true, false |
71
+ | marker-text | 标记点提示文本 | "" | 字符串 |
72
+
73
+ ### 地图类型说明
74
+
75
+ - **hybrid**: 混合地图,支持切换 Google、高德、百度、Geoq、OpenStreetMap 等多个图层
76
+ - **google**: Google 地图
77
+ - **gaode**: 高德地图
78
+ - **baidu**: 百度地图
79
+ - **geoq**: Geoq 地图
80
+ - **openstreet**: OpenStreetMap
81
+
82
+ ## 注意事项
83
+
84
+ 1. 插件会自动加载所需的地图脚本,无需手动引入。
85
+
86
+ 2. 混合地图使用 Leaflet 实现,依赖 `leaflet` 和 `leaflet.chinatmsproviders` 库。
87
+
88
+ ## 示例
89
+
90
+ ### 混合地图
91
+
92
+ ```html
93
+ <rspress-map type="hybrid" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
94
+ ```
95
+
96
+ ### Google 地图
97
+
98
+ ```html
99
+ <rspress-map type="google" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
100
+ ```
101
+
102
+ ### 高德地图
103
+
104
+ ```html
105
+ <rspress-map type="gaode" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
106
+ ```
107
+
108
+ ### 百度地图
109
+
110
+ ```html
111
+ <rspress-map type="baidu" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
112
+ ```
113
+
114
+ ### Geoq 地图
115
+
116
+ ```html
117
+ <rspress-map type="geoq" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
118
+ ```
119
+
120
+ ### OpenStreetMap
121
+
122
+ ```html
123
+ <rspress-map type="openstreet" lat="39.9042" lng="116.4074" zoom="12" marker-text="北京市"></rspress-map>
124
+ ```
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ export interface RspressMapProps {
3
+ type?: 'hybrid' | 'google' | 'gaode' | 'baidu' | 'geoq' | 'openstreet' | string;
4
+ lat?: string | number;
5
+ lng?: string | number;
6
+ zoom?: string | number;
7
+ width?: string;
8
+ height?: string;
9
+ marker?: boolean | string;
10
+ markerText?: string;
11
+ layerType?: number | string;
12
+ }
13
+ declare global {
14
+ interface Window {
15
+ L: any;
16
+ }
17
+ }
18
+ export declare const RspressMap: React.FC<RspressMapProps>;
19
+ export default RspressMap;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RspressMap = void 0;
13
+ const jsx_runtime_1 = require("react/jsx-runtime");
14
+ const react_1 = require("react");
15
+ const loadScript = (src) => {
16
+ return new Promise((resolve, reject) => {
17
+ if (typeof window !== 'undefined' && document.querySelector(`script[src="${src}"]`)) {
18
+ // Already added, wait for it to load if it hasn't mapped to window yet
19
+ let attempts = 0;
20
+ const check = () => {
21
+ if (window.L && src.includes('leaflet@'))
22
+ return resolve();
23
+ if (window.L && window.L.tileLayer && window.L.tileLayer.chinaProvider && src.includes('ChineseTmsProviders'))
24
+ return resolve();
25
+ attempts++;
26
+ if (attempts > 50)
27
+ return reject(new Error(`Timeout loading ${src}`)); // 5 seconds
28
+ setTimeout(check, 100);
29
+ };
30
+ check();
31
+ return;
32
+ }
33
+ const script = document.createElement('script');
34
+ script.src = src;
35
+ script.onload = () => resolve();
36
+ script.onerror = (e) => reject(e);
37
+ document.head.appendChild(script);
38
+ });
39
+ };
40
+ const loadCss = (href) => {
41
+ if (typeof window !== 'undefined' && document.querySelector(`link[href="${href}"]`))
42
+ return;
43
+ const link = document.createElement('link');
44
+ link.rel = 'stylesheet';
45
+ link.href = href;
46
+ document.head.appendChild(link);
47
+ };
48
+ const RspressMap = (props) => {
49
+ const { type = 'gaode', lat = 39.9042, lng = 116.4074, zoom = 10, width = '100%', height = '400px', marker = true, markerText = '', layerType } = props;
50
+ const mapRef = (0, react_1.useRef)(null);
51
+ const leafletMapRef = (0, react_1.useRef)(null);
52
+ const [isLoaded, setIsLoaded] = (0, react_1.useState)(false);
53
+ const id = (0, react_1.useRef)(`map-${Math.random().toString(36).substr(2, 9)}`);
54
+ (0, react_1.useEffect)(() => {
55
+ if (typeof window === 'undefined')
56
+ return;
57
+ const initLeaflet = () => __awaiter(void 0, void 0, void 0, function* () {
58
+ try {
59
+ loadCss('//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.css');
60
+ yield loadScript('//unpkg.com/hexo-tag-map/lib/leaflet@1.7.1.js');
61
+ yield loadScript('//unpkg.com/hexo-tag-map/lib/leaflet.ChineseTmsProviders@1.0.4.js');
62
+ setIsLoaded(true);
63
+ }
64
+ catch (error) {
65
+ console.error('Failed to load map scripts:', error);
66
+ }
67
+ });
68
+ if (window.L && window.L.tileLayer && window.L.tileLayer.chinaProvider) {
69
+ setIsLoaded(true);
70
+ }
71
+ else {
72
+ initLeaflet();
73
+ }
74
+ }, []);
75
+ (0, react_1.useEffect)(() => {
76
+ if (!isLoaded || !mapRef.current)
77
+ return;
78
+ const latNum = Number(lat);
79
+ const lngNum = Number(lng);
80
+ const zoomNum = Number(zoom);
81
+ const showMarker = marker === true || marker === 'true';
82
+ const L = window.L;
83
+ // Cleanup previous map instance if it exists
84
+ if (leafletMapRef.current) {
85
+ leafletMapRef.current.remove();
86
+ leafletMapRef.current = null;
87
+ }
88
+ // Setup providers based on hexo-tag-map config
89
+ const normalm = L.tileLayer.chinaProvider('GaoDe.Normal.Map', { maxZoom: 20, minZoom: 1, attribution: '高德地图' });
90
+ const imgm = L.tileLayer.chinaProvider('GaoDe.Satellite.Map', { maxZoom: 20, minZoom: 1, attribution: '高德地图' });
91
+ const imga = L.tileLayer.chinaProvider('GaoDe.Satellite.Annotion', { maxZoom: 20, minZoom: 1, attribution: '高德地图' });
92
+ const googleNormal = L.tileLayer.chinaProvider('Google.Normal.Map', { maxZoom: 21, minZoom: 1, attribution: 'Google Maps' });
93
+ const googleSatellite = L.tileLayer.chinaProvider('Google.Satellite.Map', { maxZoom: 21, minZoom: 1, attribution: 'Google Maps' });
94
+ const googleRoute = L.tileLayer.chinaProvider('Google.Satellite.Annotion', { maxZoom: 21, minZoom: 1, attribution: 'Google Maps' });
95
+ const geoqNormal = L.tileLayer.chinaProvider('Geoq.Normal.Map', { maxZoom: 21, minZoom: 1, attribution: 'GeoQ' });
96
+ const normalGaode = L.layerGroup([normalm]);
97
+ const imageGaode = L.layerGroup([imgm, imga]);
98
+ // Define base layers control
99
+ const baseLayers = {
100
+ "高德地图": normalGaode,
101
+ "智图地图": geoqNormal,
102
+ "谷歌地图": googleNormal,
103
+ "高德卫星地图": imgm,
104
+ "谷歌卫星地图": googleSatellite,
105
+ "高德卫星标注": imageGaode,
106
+ "谷歌卫星标注": googleRoute
107
+ };
108
+ let defaultLayer = normalGaode;
109
+ // Follow hexo-tag-map layer logic if layerType (tuceng) is specifically provided
110
+ if (layerType !== undefined) {
111
+ const layerNum = Number(layerType);
112
+ if (layerNum === 2)
113
+ defaultLayer = geoqNormal;
114
+ else if (layerNum === 3)
115
+ defaultLayer = googleNormal;
116
+ else if (layerNum === 4)
117
+ defaultLayer = imgm;
118
+ else if (layerNum === 5)
119
+ defaultLayer = googleSatellite;
120
+ else if (layerNum === 6)
121
+ defaultLayer = imageGaode;
122
+ else if (layerNum === 7)
123
+ defaultLayer = googleRoute;
124
+ }
125
+ else {
126
+ // Select the default layer based on type if no layerType is provided
127
+ if (type === 'google')
128
+ defaultLayer = googleNormal;
129
+ else if (type === 'hybrid')
130
+ defaultLayer = imageGaode;
131
+ else if (type === 'geoq')
132
+ defaultLayer = geoqNormal;
133
+ else if (type === 'baidu')
134
+ defaultLayer = geoqNormal; // Fallback since Baidu uses different coords traditionally, mapping it to GeoQ or Gaode is safer for standard WGS84 coords
135
+ }
136
+ const map = L.map(mapRef.current, {
137
+ center: [latNum, lngNum],
138
+ zoom: zoomNum,
139
+ layers: [defaultLayer],
140
+ zoomControl: false // hexo-tag-map disables default to add custom one
141
+ });
142
+ L.control.layers(baseLayers, null).addTo(map);
143
+ L.control.zoom({ zoomInTitle: '放大', zoomOutTitle: '缩小' }).addTo(map);
144
+ if (showMarker) {
145
+ const m = L.marker([latNum, lngNum]).addTo(map);
146
+ if (markerText) {
147
+ m.bindPopup(markerText).openPopup();
148
+ }
149
+ }
150
+ leafletMapRef.current = map;
151
+ return () => {
152
+ if (leafletMapRef.current) {
153
+ leafletMapRef.current.remove();
154
+ leafletMapRef.current = null;
155
+ }
156
+ };
157
+ }, [isLoaded, lat, lng, zoom, type, marker, markerText, layerType]);
158
+ const heightValue = height || '400px';
159
+ const widthValue = width || '100%';
160
+ return ((0, jsx_runtime_1.jsx)("div", { className: "map-box", style: { margin: '0.8rem 0 1.6rem 0' }, children: (0, jsx_runtime_1.jsx)("div", { id: id.current, ref: mapRef, style: {
161
+ maxWidth: widthValue,
162
+ height: heightValue,
163
+ display: 'block',
164
+ margin: '0 auto',
165
+ zIndex: 1,
166
+ borderRadius: '5px'
167
+ } }) }));
168
+ };
169
+ exports.RspressMap = RspressMap;
170
+ exports.default = exports.RspressMap;
@@ -0,0 +1,11 @@
1
+ import type { RspressPlugin } from '@rspress/core';
2
+ /**
3
+ * Rspress map plugin options
4
+ */
5
+ export interface MapPluginOptions {
6
+ }
7
+ /**
8
+ * Rspress plugin for embedding interactive maps
9
+ */
10
+ export declare function pluginMap(options?: MapPluginOptions): RspressPlugin;
11
+ export default pluginMap;
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.pluginMap = pluginMap;
7
+ const path_1 = __importDefault(require("path"));
8
+ const remark_rspress_map_1 = require("./remark-rspress-map");
9
+ const rehype_rspress_map_1 = require("./rehype-rspress-map");
10
+ /**
11
+ * Rspress plugin for embedding interactive maps
12
+ */
13
+ function pluginMap(options = {}) {
14
+ return {
15
+ name: 'rspress-plugin-map',
16
+ // Register the RspressMap component as a global component available in MDX
17
+ markdown: {
18
+ globalComponents: [
19
+ path_1.default.join(__dirname, 'components', 'RspressMap.js')
20
+ ],
21
+ // Use remark plugin to transform <rspress-map> HTML tags to MDX JSX components
22
+ remarkPlugins: [remark_rspress_map_1.remarkRspressMap],
23
+ // Also use rehype plugin as fallback for any remaining HTML elements
24
+ rehypePlugins: [rehype_rspress_map_1.rehypeRspressMap]
25
+ },
26
+ // Add runtime module to handle component mapping
27
+ addRuntimeModules() {
28
+ return {
29
+ 'virtual:rspress-map-runtime': `
30
+ // Runtime module for rspress-map component
31
+ // This ensures the component is available globally
32
+ export { default as RspressMap } from '${path_1.default.join(__dirname, 'components', 'RspressMap.js')}';
33
+ `
34
+ };
35
+ }
36
+ };
37
+ }
38
+ exports.default = pluginMap;
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'unified';
2
+ import type { Root } from 'hast';
3
+ /**
4
+ * Rehype plugin to transform <rspress-map> tags to React components
5
+ */
6
+ export declare const rehypeRspressMap: Plugin<[], Root>;
7
+ export default rehypeRspressMap;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rehypeRspressMap = void 0;
4
+ const unist_util_visit_1 = require("unist-util-visit");
5
+ /**
6
+ * Rehype plugin to transform <rspress-map> tags to React components
7
+ */
8
+ const rehypeRspressMap = () => {
9
+ return (tree) => {
10
+ (0, unist_util_visit_1.visit)(tree, 'element', (node) => {
11
+ if (node.tagName === 'rspress-map') {
12
+ // Transform to MDX JSX element
13
+ const mdxNode = {
14
+ type: 'mdxJsxFlowElement',
15
+ name: 'RspressMap',
16
+ attributes: [],
17
+ children: node.children || []
18
+ };
19
+ // Convert HTML attributes to JSX attributes
20
+ if (node.properties) {
21
+ for (const [key, value] of Object.entries(node.properties)) {
22
+ // Convert kebab-case to camelCase for React props
23
+ // marker-text -> markerText
24
+ const jsxKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
25
+ const attr = {
26
+ type: 'mdxJsxAttribute',
27
+ name: jsxKey
28
+ };
29
+ // Set value based on type
30
+ // For MDX, string values should be wrapped in quotes
31
+ if (typeof value === 'string') {
32
+ attr.value = { type: 'mdxJsxAttributeValueExpression', value: JSON.stringify(value) };
33
+ }
34
+ else if (typeof value === 'boolean') {
35
+ attr.value = { type: 'mdxJsxAttributeValueExpression', value: String(value) };
36
+ }
37
+ else {
38
+ attr.value = { type: 'mdxJsxAttributeValueExpression', value: JSON.stringify(value) };
39
+ }
40
+ mdxNode.attributes.push(attr);
41
+ }
42
+ }
43
+ // Replace the node
44
+ Object.assign(node, mdxNode);
45
+ }
46
+ });
47
+ };
48
+ };
49
+ exports.rehypeRspressMap = rehypeRspressMap;
50
+ exports.default = exports.rehypeRspressMap;
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from 'unified';
2
+ import type { Root } from 'mdast';
3
+ /**
4
+ * Remark plugin to transform <rspress-map> HTML tags to MDX JSX components
5
+ * This runs before rehype, so we can transform HTML elements in markdown
6
+ */
7
+ export declare const remarkRspressMap: Plugin<[], Root>;
8
+ export default remarkRspressMap;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.remarkRspressMap = void 0;
4
+ const unist_util_visit_1 = require("unist-util-visit");
5
+ /**
6
+ * Remark plugin to transform <rspress-map> HTML tags to MDX JSX components
7
+ * This runs before rehype, so we can transform HTML elements in markdown
8
+ */
9
+ const remarkRspressMap = () => {
10
+ return (tree) => {
11
+ (0, unist_util_visit_1.visit)(tree, ['html', 'mdxJsxFlowElement', 'mdxJsxTextElement'], (node, index, parent) => {
12
+ // Handle HTML nodes
13
+ if (node.type === 'html') {
14
+ const htmlContent = node.value;
15
+ // Debug: log all HTML nodes to see what we're processing
16
+ if (htmlContent && htmlContent.includes('rspress-map')) {
17
+ console.log('[remarkRspressMap] Found literal HTML rspress-map tag:', htmlContent);
18
+ }
19
+ // Match <rspress-map> tags - handle both self-closing and with closing tag
20
+ // Pattern: <rspress-map ... /> or <rspress-map ...></rspress-map>
21
+ const tagRegex = /<rspress-map\s+([^>]*?)(?:\s*\/>|>)/;
22
+ const match = htmlContent.match(tagRegex);
23
+ if (match) {
24
+ console.log('[remarkRspressMap] Matched HTML tag, transforming...');
25
+ const attrsString = match[1];
26
+ const attrs = [];
27
+ // Parse attributes: key="value" or key='value'
28
+ const attrRegex = /(\w+(?:-\w+)*)\s*=\s*["']([^"']*)["']/g;
29
+ let attrMatch;
30
+ while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
31
+ const key = attrMatch[1];
32
+ const value = attrMatch[2];
33
+ // Convert kebab-case to camelCase: marker-text -> markerText
34
+ const jsxKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
35
+ attrs.push({ name: jsxKey, value });
36
+ }
37
+ // Create MDX JSX flow element
38
+ const mdxNode = {
39
+ type: 'mdxJsxFlowElement',
40
+ name: 'RspressMap',
41
+ attributes: attrs.map(attr => ({
42
+ type: 'mdxJsxAttribute',
43
+ name: attr.name,
44
+ value: attr.value
45
+ })),
46
+ children: []
47
+ };
48
+ // Replace the HTML node with the MDX JSX node
49
+ if (parent && typeof index === 'number') {
50
+ parent.children[index] = mdxNode;
51
+ }
52
+ }
53
+ }
54
+ // Handle natively parsed MDX JSX elements
55
+ else if ((node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && node.name === 'rspress-map') {
56
+ console.log('[remarkRspressMap] Matched mdx tag, transforming name to RspressMap...');
57
+ // Change the element name to match the exported React component
58
+ node.name = 'RspressMap';
59
+ // Convert existing attribute names from kebab-case to camelCase
60
+ if (node.attributes && Array.isArray(node.attributes)) {
61
+ node.attributes.forEach((attr) => {
62
+ if (attr.type === 'mdxJsxAttribute' && typeof attr.name === 'string') {
63
+ attr.name = attr.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
64
+ }
65
+ });
66
+ }
67
+ }
68
+ });
69
+ };
70
+ };
71
+ exports.remarkRspressMap = remarkRspressMap;
72
+ exports.default = exports.remarkRspressMap;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "rspress-plugin-map",
3
+ "version": "0.1.0",
4
+ "description": "Rspress plugin for embedding interactive maps",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "clean": "rm -rf dist",
12
+ "build": "tsc",
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "keywords": [
16
+ "rspress",
17
+ "plugin",
18
+ "map",
19
+ "google-maps",
20
+ "amap",
21
+ "baidu-maps",
22
+ "openstreetmap",
23
+ "leaflet"
24
+ ],
25
+ "author": "buyfakett <work@tteam.icu>",
26
+ "license": "MIT",
27
+ "peerDependencies": {
28
+ "@rspress/core": "^2.0.0",
29
+ "react": "^18.0.0 || ^19.0.0",
30
+ "react-dom": "^18.0.0 || ^19.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@rspress/core": "^2.0.3",
34
+ "@types/react": "^19.2.14",
35
+ "@types/react-dom": "^19.2.3",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }