@jsonresume/utils 0.2.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/LICENSE +21 -0
- package/package.json +52 -0
- package/src/__tests__/calculations.test.js +56 -0
- package/src/__tests__/counts.test.js +118 -0
- package/src/__tests__/dates.test.js +167 -0
- package/src/__tests__/education.test.js +89 -0
- package/src/__tests__/experience.test.js +150 -0
- package/src/__tests__/fixtures.js +103 -0
- package/src/__tests__/keyMetrics.test.js +56 -0
- package/src/__tests__/resume.test.js +78 -0
- package/src/__tests__/url.test.js +113 -0
- package/src/__tests__/workHistory.test.js +107 -0
- package/src/dates.js +247 -0
- package/src/index.d.ts +76 -0
- package/src/index.js +54 -0
- package/src/metrics/counts.js +107 -0
- package/src/metrics/education.js +74 -0
- package/src/metrics/experience.js +111 -0
- package/src/metrics/index.js +45 -0
- package/src/metrics/keyMetrics.js +98 -0
- package/src/metrics/workHistory.js +80 -0
- package/src/resume.js +72 -0
- package/src/url.js +176 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Data Calculation Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for computing metrics and insights from JSON Resume data.
|
|
5
|
+
* Use these helpers in themes to display calculated metrics, statistics, and insights.
|
|
6
|
+
*
|
|
7
|
+
* This module is a thin re-export barrel. The implementations live in focused
|
|
8
|
+
* sibling modules grouped by concern:
|
|
9
|
+
* - ./experience.js work tenure & career progression
|
|
10
|
+
* - ./counts.js simple section tallies
|
|
11
|
+
* - ./education.js education years & highest degree
|
|
12
|
+
* - ./workHistory.js employment status, industries, volunteer tenure
|
|
13
|
+
* - ./keyMetrics.js dashboard metrics aggregation
|
|
14
|
+
*
|
|
15
|
+
* @module @jsonresume/utils/metrics
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
calculateTotalExperience,
|
|
20
|
+
calculateCurrentRoleExperience,
|
|
21
|
+
countCareerPositions,
|
|
22
|
+
getCareerProgressionRate,
|
|
23
|
+
countTotalHighlights,
|
|
24
|
+
} from './experience.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
countCompanies,
|
|
28
|
+
countProjects,
|
|
29
|
+
countPublications,
|
|
30
|
+
countAwards,
|
|
31
|
+
countTotalSkills,
|
|
32
|
+
countSkillCategories,
|
|
33
|
+
countLanguages,
|
|
34
|
+
} from './counts.js';
|
|
35
|
+
|
|
36
|
+
export { calculateEducationYears, getHighestDegree } from './education.js';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
calculateVolunteerYears,
|
|
40
|
+
getUniqueIndustries,
|
|
41
|
+
getCurrentEmployer,
|
|
42
|
+
isCurrentlyEmployed,
|
|
43
|
+
} from './workHistory.js';
|
|
44
|
+
|
|
45
|
+
export { calculateKeyMetrics } from './keyMetrics.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Key Metrics Aggregation Helper
|
|
3
|
+
*
|
|
4
|
+
* Composes the focused calculation helpers into a dashboard-ready summary
|
|
5
|
+
* of key metrics derived from a complete JSON Resume object.
|
|
6
|
+
*
|
|
7
|
+
* @module @jsonresume/utils/metrics/keyMetrics
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { calculateTotalExperience } from './experience.js';
|
|
11
|
+
import {
|
|
12
|
+
countCompanies,
|
|
13
|
+
countProjects,
|
|
14
|
+
countTotalSkills,
|
|
15
|
+
countPublications,
|
|
16
|
+
countAwards,
|
|
17
|
+
countLanguages,
|
|
18
|
+
} from './counts.js';
|
|
19
|
+
import { getHighestDegree } from './education.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calculate comprehensive Key Metrics object for dashboard displays
|
|
23
|
+
* @param {Object} resume - Complete JSON Resume object
|
|
24
|
+
* @returns {Array<Object>} Array of metric objects with label and value
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const metrics = calculateKeyMetrics(resume);
|
|
28
|
+
* // => [
|
|
29
|
+
* // { label: 'Years Experience', value: 8 },
|
|
30
|
+
* // { label: 'Companies', value: 5 },
|
|
31
|
+
* // { label: 'Projects', value: 12 },
|
|
32
|
+
* // { label: 'Core Skills', value: 42 }
|
|
33
|
+
* // ]
|
|
34
|
+
*/
|
|
35
|
+
export function calculateKeyMetrics(resume) {
|
|
36
|
+
const metrics = [];
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
work = [],
|
|
40
|
+
projects = [],
|
|
41
|
+
skills = [],
|
|
42
|
+
publications = [],
|
|
43
|
+
awards = [],
|
|
44
|
+
education = [],
|
|
45
|
+
languages = [],
|
|
46
|
+
} = resume;
|
|
47
|
+
|
|
48
|
+
// Experience
|
|
49
|
+
const experience = calculateTotalExperience(work);
|
|
50
|
+
if (experience > 0) {
|
|
51
|
+
metrics.push({ label: 'Years Experience', value: experience });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Companies
|
|
55
|
+
const companies = countCompanies(work);
|
|
56
|
+
if (companies > 0) {
|
|
57
|
+
metrics.push({ label: 'Companies', value: companies });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Projects
|
|
61
|
+
const projectCount = countProjects(projects);
|
|
62
|
+
if (projectCount > 0) {
|
|
63
|
+
metrics.push({ label: 'Projects', value: projectCount });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Skills
|
|
67
|
+
const skillCount = countTotalSkills(skills);
|
|
68
|
+
if (skillCount > 0) {
|
|
69
|
+
metrics.push({ label: 'Core Skills', value: skillCount });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Publications
|
|
73
|
+
const pubCount = countPublications(publications);
|
|
74
|
+
if (pubCount > 0) {
|
|
75
|
+
metrics.push({ label: 'Publications', value: pubCount });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Awards
|
|
79
|
+
const awardCount = countAwards(awards);
|
|
80
|
+
if (awardCount > 0) {
|
|
81
|
+
metrics.push({ label: 'Awards', value: awardCount });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Education
|
|
85
|
+
const degree = getHighestDegree(education);
|
|
86
|
+
if (degree) {
|
|
87
|
+
metrics.push({ label: 'Education', value: degree });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Languages
|
|
91
|
+
const langCount = countLanguages(languages);
|
|
92
|
+
if (langCount > 1) {
|
|
93
|
+
// Only show if multilingual
|
|
94
|
+
metrics.push({ label: 'Languages', value: langCount });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return metrics;
|
|
98
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Work History Insight Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for deriving employment status, industries, and volunteer
|
|
5
|
+
* tenure from JSON Resume data.
|
|
6
|
+
*
|
|
7
|
+
* @module @jsonresume/utils/metrics/workHistory
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculate total volunteer hours (if duration data is available)
|
|
12
|
+
* @param {Array} volunteer - Array of volunteer objects with startDate and endDate
|
|
13
|
+
* @returns {number} Total volunteer years
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const volunteerYears = calculateVolunteerYears(resume.volunteer);
|
|
17
|
+
* // => 4
|
|
18
|
+
*/
|
|
19
|
+
export function calculateVolunteerYears(volunteer = []) {
|
|
20
|
+
if (!Array.isArray(volunteer) || volunteer.length === 0) return 0;
|
|
21
|
+
|
|
22
|
+
const totalYears = volunteer.reduce((acc, vol) => {
|
|
23
|
+
if (!vol.startDate) return acc;
|
|
24
|
+
|
|
25
|
+
const start = new Date(vol.startDate);
|
|
26
|
+
const end = vol.endDate ? new Date(vol.endDate) : new Date();
|
|
27
|
+
const years = (end - start) / (1000 * 60 * 60 * 24 * 365.25);
|
|
28
|
+
|
|
29
|
+
return acc + years;
|
|
30
|
+
}, 0);
|
|
31
|
+
|
|
32
|
+
return Math.round(totalYears);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get all unique industries from work experience
|
|
37
|
+
* @param {Array} work - Array of work experience objects with industry field
|
|
38
|
+
* @returns {Array<string>} Array of unique industries
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const industries = getUniqueIndustries(resume.work);
|
|
42
|
+
* // => ["Technology", "Finance", "Healthcare"]
|
|
43
|
+
*/
|
|
44
|
+
export function getUniqueIndustries(work = []) {
|
|
45
|
+
if (!Array.isArray(work) || work.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const industries = new Set(work.map((job) => job.industry).filter(Boolean));
|
|
48
|
+
|
|
49
|
+
return Array.from(industries);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get most recent/current employer
|
|
54
|
+
* @param {Array} work - Array of work experience objects
|
|
55
|
+
* @returns {Object|null} Most recent work object
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const current = getCurrentEmployer(resume.work);
|
|
59
|
+
* // => { name: "Google", position: "Senior Engineer", ... }
|
|
60
|
+
*/
|
|
61
|
+
export function getCurrentEmployer(work = []) {
|
|
62
|
+
if (!Array.isArray(work) || work.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
// Find first job without endDate (current), or first in array
|
|
65
|
+
return work.find((job) => !job.endDate) || work[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if currently employed
|
|
70
|
+
* @param {Array} work - Array of work experience objects
|
|
71
|
+
* @returns {boolean} True if currently employed
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const employed = isCurrentlyEmployed(resume.work);
|
|
75
|
+
* // => true
|
|
76
|
+
*/
|
|
77
|
+
export function isCurrentlyEmployed(work = []) {
|
|
78
|
+
if (!Array.isArray(work) || work.length === 0) return false;
|
|
79
|
+
return work.some((job) => !job.endDate);
|
|
80
|
+
}
|
package/src/resume.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resume-shape utilities for JSON Resume.
|
|
3
|
+
*
|
|
4
|
+
* Framework-free helpers for formatting and normalizing resume data.
|
|
5
|
+
*
|
|
6
|
+
* @module @jsonresume/utils/resume
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The eleven standard array sections of a JSON Resume. Together with `basics`
|
|
11
|
+
* these make up the twelve standard sections normalizeResume guarantees.
|
|
12
|
+
* @type {ReadonlyArray<string>}
|
|
13
|
+
*/
|
|
14
|
+
const STANDARD_ARRAY_SECTIONS = [
|
|
15
|
+
'work',
|
|
16
|
+
'volunteer',
|
|
17
|
+
'education',
|
|
18
|
+
'awards',
|
|
19
|
+
'certificates',
|
|
20
|
+
'publications',
|
|
21
|
+
'skills',
|
|
22
|
+
'languages',
|
|
23
|
+
'interests',
|
|
24
|
+
'references',
|
|
25
|
+
'projects',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format a JSON Resume location into a single display line.
|
|
30
|
+
* Drops empty parts and joins the rest with ', '.
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} [location] - JSON Resume location object
|
|
33
|
+
* @returns {string} e.g. 'City, Region, CC' (empty string when nothing is set)
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* formatLocation({ city: 'Berlin', region: 'BE', countryCode: 'DE' })
|
|
37
|
+
* // 'Berlin, BE, DE'
|
|
38
|
+
*/
|
|
39
|
+
export function formatLocation(location) {
|
|
40
|
+
if (!location || typeof location !== 'object') {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [location.city, location.region, location.countryCode]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(', ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a resume into an object with all twelve standard array sections
|
|
51
|
+
* defaulted to [] and `basics` defaulted to an object. Never mutates the input.
|
|
52
|
+
* Unknown extra fields are preserved (JSON Resume allows additional properties).
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} [resume] - JSON Resume object (possibly partial)
|
|
55
|
+
* @returns {Object} A new object with standard sections guaranteed present
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* normalizeResume({ basics: { name: 'A' } }).work // []
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeResume(resume) {
|
|
61
|
+
const source = resume && typeof resume === 'object' ? resume : {};
|
|
62
|
+
|
|
63
|
+
const normalized = { ...source };
|
|
64
|
+
normalized.basics =
|
|
65
|
+
source.basics && typeof source.basics === 'object' ? source.basics : {};
|
|
66
|
+
|
|
67
|
+
for (const section of STANDARD_ARRAY_SECTIONS) {
|
|
68
|
+
normalized[section] = Array.isArray(source[section]) ? source[section] : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
package/src/url.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL safety utilities for JSON Resume.
|
|
3
|
+
*
|
|
4
|
+
* Framework-free. Prevents XSS attacks and ensures safe URL handling.
|
|
5
|
+
*
|
|
6
|
+
* @module @jsonresume/utils/url
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes URLs to prevent XSS attacks
|
|
11
|
+
* - Blocks javascript:, data:, vbscript: schemes
|
|
12
|
+
* - Allows http:, https:, mailto:, tel: schemes
|
|
13
|
+
* - Returns null for invalid/dangerous URLs
|
|
14
|
+
*
|
|
15
|
+
* @param {string} url - The URL to sanitize
|
|
16
|
+
* @returns {string|null} - Safe URL or null if dangerous
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* safeUrl('https://example.com') // 'https://example.com'
|
|
20
|
+
* safeUrl('javascript:alert(1)') // null
|
|
21
|
+
* safeUrl('mailto:user@example.com') // 'mailto:user@example.com'
|
|
22
|
+
*/
|
|
23
|
+
export function safeUrl(url) {
|
|
24
|
+
if (!url || typeof url !== 'string') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Trim whitespace
|
|
29
|
+
const trimmed = url.trim();
|
|
30
|
+
|
|
31
|
+
// Check for dangerous protocols
|
|
32
|
+
const dangerousProtocols = /^(javascript|data|vbscript|file|about):/i;
|
|
33
|
+
if (dangerousProtocols.test(trimmed)) {
|
|
34
|
+
// Dangerous URL blocked. (Intentionally silent: no console output so this
|
|
35
|
+
// stays usable in SSR/library contexts that forbid console writes.)
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Allow safe protocols
|
|
40
|
+
const safeProtocols = /^(https?|mailto|tel|sms|ftp):/i;
|
|
41
|
+
if (safeProtocols.test(trimmed)) {
|
|
42
|
+
return trimmed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Allow relative URLs (starting with / or .)
|
|
46
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('.')) {
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Allow URLs without protocol (assume https)
|
|
51
|
+
if (/^www\./i.test(trimmed)) {
|
|
52
|
+
return `https://${trimmed}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For other cases, check if it looks like a valid domain
|
|
56
|
+
// Allow alphanumeric, hyphens, dots, and common TLDs
|
|
57
|
+
if (/^[a-z0-9][a-z0-9.-]+\.[a-z]{2,}$/i.test(trimmed)) {
|
|
58
|
+
return `https://${trimmed}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If we can't determine safety, return the original (let the browser handle
|
|
62
|
+
// it). Intentionally silent — no console output in this library context.
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns proper rel attribute for external links
|
|
68
|
+
* Adds security attributes for links opening in new windows
|
|
69
|
+
*
|
|
70
|
+
* @param {string} url - The URL to check
|
|
71
|
+
* @param {boolean} [openInNewTab=false] - Whether link opens in new tab
|
|
72
|
+
* @returns {string} - rel attribute value
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* getLinkRel('https://example.com', true) // 'noopener noreferrer'
|
|
76
|
+
* getLinkRel('mailto:user@example.com', false) // ''
|
|
77
|
+
*/
|
|
78
|
+
export function getLinkRel(url, openInNewTab = false) {
|
|
79
|
+
if (!url || typeof url !== 'string') {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only add security attributes for http(s) links opening in new tabs
|
|
84
|
+
if (openInNewTab && /^https?:/i.test(url)) {
|
|
85
|
+
return 'noopener noreferrer';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sanitizes HTML to prevent XSS
|
|
93
|
+
* Simple implementation that escapes dangerous characters
|
|
94
|
+
* For more complex needs, use DOMPurify
|
|
95
|
+
*
|
|
96
|
+
* @param {string} html - HTML string to sanitize
|
|
97
|
+
* @returns {string} - Sanitized HTML
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* sanitizeHtml('<script>alert(1)</script>') // '<script>alert(1)</script>'
|
|
101
|
+
*/
|
|
102
|
+
export function sanitizeHtml(html) {
|
|
103
|
+
if (!html || typeof html !== 'string') {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return html
|
|
108
|
+
.replace(/&/g, '&')
|
|
109
|
+
.replace(/</g, '<')
|
|
110
|
+
.replace(/>/g, '>')
|
|
111
|
+
.replace(/"/g, '"')
|
|
112
|
+
.replace(/'/g, ''');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Checks if a URL is external (different origin)
|
|
117
|
+
*
|
|
118
|
+
* @param {string} url - URL to check
|
|
119
|
+
* @param {string} [currentOrigin=null] - Current site origin (default: window.location.origin if available)
|
|
120
|
+
* @returns {boolean} - True if URL is external
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* isExternalUrl('https://example.com') // true (if on different domain)
|
|
124
|
+
* isExternalUrl('/about') // false
|
|
125
|
+
*/
|
|
126
|
+
export function isExternalUrl(url, currentOrigin = null) {
|
|
127
|
+
if (!url || typeof url !== 'string') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Relative URLs are not external
|
|
132
|
+
if (url.startsWith('/') || url.startsWith('.') || url.startsWith('#')) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// mailto:, tel:, etc. are not external in the HTTP sense
|
|
137
|
+
if (/^(mailto|tel|sms):/i.test(url)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If no origin provided and we're in a browser, use window.location
|
|
142
|
+
if (!currentOrigin && typeof window !== 'undefined') {
|
|
143
|
+
currentOrigin = window.location.origin;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If still no origin, we can't determine (assume external for safety)
|
|
147
|
+
if (!currentOrigin) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const urlObj = new URL(url, currentOrigin);
|
|
153
|
+
return urlObj.origin !== currentOrigin;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Invalid URL, treat as external for safety
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format a URL for display: strip the protocol and any trailing slash.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} url - URL to format
|
|
164
|
+
* @returns {string} Display-friendly URL (empty string for falsy/non-string)
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* formatUrlForDisplay('https://example.com/') // 'example.com'
|
|
168
|
+
* formatUrlForDisplay('http://example.com/blog') // 'example.com/blog'
|
|
169
|
+
*/
|
|
170
|
+
export function formatUrlForDisplay(url) {
|
|
171
|
+
if (!url || typeof url !== 'string') {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '').replace(/\/+$/, '');
|
|
176
|
+
}
|