UNPKG

@octopusdeploy/design-system-components

Version:
183 lines (182 loc) • 7.42 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.renderDescription = exports.descriptionText = exports.descriptionWithLink = exports.descriptionCode = exports.descriptionLink = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const css_1 = require("@emotion/css"); const design_system_tokens_1 = require("@octopusdeploy/design-system-tokens"); const React = __importStar(require("react")); /** * Validates if a URL is safe to use in links * @param url - The URL to validate * @returns true if the URL uses a safe protocol or is a relative path, false otherwise */ const isValidUrl = (url) => { // Block dangerous protocols immediately if (url.startsWith("javascript:") || url.startsWith("data:") || url.startsWith("vbscript:")) { return false; } // Allow relative URLs (paths that don't start with a protocol) if (!url.includes("://")) { return true; } // For absolute URLs, validate the protocol try { const parsed = new URL(url); return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol); } catch { return false; } }; /** * Helper function to create a link object for use in description templates * @param text - The link text to display * @param url - The URL to link to (supports both absolute and relative URLs) * @param newTab - Whether to open the link in a new tab (defaults to false) * @returns HelperLink object for valid URLs, empty string for invalid URLs * @example * // Relative link (opens in same tab) * descriptionLink("Visit our docs", "/docs/getting-started") * * // Absolute link (opens in same tab) * descriptionLink("Visit our docs", "https://docs.example.com") * * // External link that opens in new tab * descriptionLink("External resource", "https://external.com", true) * * // Invalid/dangerous URLs return empty string * descriptionLink("Bad link", "javascript:alert('xss')") // returns "" */ const descriptionLink = (text, url, newTab = false) => { if (!isValidUrl(url)) { return ""; } return { type: "link", text, url, newTab, }; }; exports.descriptionLink = descriptionLink; /** * Helper function to create a code object for use in description templates * @param text - The code text to display * @returns HelperCode object * @example * // Inline code snippet * code("propName") * * // Code with special characters * code("useState()") */ const descriptionCode = (text) => ({ type: "code", text, }); exports.descriptionCode = descriptionCode; /** * For backwards compatibility * @deprecated Use `link` instead */ exports.descriptionWithLink = exports.descriptionLink; /** * Tagged template literal function for creating rich descriptions with embedded links and code. * Allows natural template literal syntax with embedded link() and code() calls. * @param strings - Template literal strings * @param values - Interpolated values (including link and code objects) * @returns DescriptionContent array * @example * // Using template literal syntax with links * descriptionText`Visit our ${descriptionLink("documentation", "/docs")} for more info.` * * // With code snippets * descriptionText`Set the ${code("isVisible")} prop to control visibility.` */ const descriptionText = (strings, ...values) => { const isValidValue = (value) => typeof value === "string" || (value && typeof value === "object" && (value.type === "link" || value.type === "code")); const isNonEmptyString = (item) => typeof item !== "string" || item !== ""; return strings .flatMap((str, index) => [str, ...(values[index] ? [values[index]] : [])]) .filter(isValidValue) .filter(isNonEmptyString); }; exports.descriptionText = descriptionText; /** * Renders a description array containing strings, links, and code snippets * @param content - Array of strings, HelperLink, and HelperCode objects * @returns JSX element containing the rendered content */ const renderDescription = (content) => { const renderItem = (item, index) => { if (typeof item === "string") { return (0, jsx_runtime_1.jsx)(React.Fragment, { children: item }, index); } if (item.type === "link") { // Double-check URL safety during rendering as an additional security layer if (!isValidUrl(item.url)) { // Unsafe URL detected - render as plain text instead of link for security return (0, jsx_runtime_1.jsx)("span", { children: item.text }, index); } return ((0, jsx_runtime_1.jsx)("a", { href: item.url, rel: "noopener noreferrer", className: linkStyles, target: item.newTab ? "_blank" : undefined, children: item.text }, index)); } if (item.type === "code") { return ((0, jsx_runtime_1.jsx)("code", { className: codeStyles, children: item.text }, index)); } return (0, jsx_runtime_1.jsx)(React.Fragment, {}, index); }; return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: content.map(renderItem) }); }; exports.renderDescription = renderDescription; const linkStyles = (0, css_1.css)({ color: design_system_tokens_1.themeTokens.color.text.link.default, textDecoration: "underline", "&:hover": { textDecoration: "none", color: design_system_tokens_1.themeTokens.color.text.link.hover, }, "&:focus-visible": { boxShadow: design_system_tokens_1.themeTokens.shadow.focused, }, }); const codeStyles = (0, css_1.css)({ font: design_system_tokens_1.text.code.regular.medium, backgroundColor: design_system_tokens_1.themeTokens.color.background.tertiary, border: `1px solid ${design_system_tokens_1.themeTokens.color.border.secondary}`, borderRadius: design_system_tokens_1.borderRadius.extraSmall, padding: `${design_system_tokens_1.space[2]} ${design_system_tokens_1.space[4]}`, color: design_system_tokens_1.themeTokens.color.text.primary, });