0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 09:53:01 +00:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Trevor Buckner
7fc03bb5a7 Merge branch 'master' into PreventInterruptedBrewJump 2025-10-06 00:07:43 -04:00
Trevor Buckner
ccf11661de Merge pull request #4461 from naturalcrit/LintEverythingOct2025
Lint everything
2025-10-06 00:07:29 -04:00
Trevor Buckner
811f274968 Move badly-placed scrollingTimeout that was doing nothing
When a user clicks brewJump or sourceJump, we disallow new jumps until the current scroll operation has finished for 150 ms. Unfortunately the timer being checked was always undefined, so you could keep clicking the jump button and get the brewRenderer or editor to keep bouncing around without finishing the jump action.

This just moves the scrollingTimeout up outside of the listener function so it isn't being reset to undefined every loop.
2025-10-06 00:06:34 -04:00
Trevor Buckner
63bebe1efd Lint everything
Catching up on a bunch of linting so random changes stop showing up on PRs when the linter is run.
2025-10-06 00:02:24 -04:00
Trevor Buckner
22e26d635a Merge pull request #4460 from naturalcrit/cleanupLocalStorageKeysTest
Clean up localStorageMap code
2025-10-05 23:28:34 -04:00
12 changed files with 68 additions and 68 deletions

View File

@@ -325,10 +325,10 @@ const Editor = createClass({
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const checkIfScrollComplete = ()=>{ let scrollingTimeout;
let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete); brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
@@ -369,8 +369,8 @@ const Editor = createClass({
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
const checkIfScrollComplete = ()=>{ let scrollingTimeout;
let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;

View File

@@ -18,7 +18,7 @@ module.exports = {
try { try {
Boolean(new URL(value)); Boolean(new URL(value));
return null; return null;
} catch (e) { } catch {
return 'Must be a valid URL'; return 'Must be a valid URL';
} }
} }

View File

@@ -2,9 +2,9 @@ require('./error-navitem.less');
const React = require('react'); const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const ErrorNavItem = ({error = '', clearError})=>{ const ErrorNavItem = ({ error = '', clearError })=>{
const response = error.response; const response = error.response;
const errorCode = error.code const errorCode = error.code;
const status = response?.status; const status = response?.status;
const HBErrorCode = response?.body?.HBErrorCode; const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message; const message = response?.body?.message;
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
errMsg += `\`\`\`\n${error.stack}\n`; errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg); console.log(errMsg);
} catch (e){} } catch {}
if(status === 409) { if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>

View File

@@ -34,8 +34,8 @@ const NewBrew = ()=>{
} }
const type = file.name.split('.').pop().toLowerCase(); const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file); console.log(file);

View File

@@ -2,22 +2,22 @@ import React from 'react';
import dedent from 'dedent-tabs'; import dedent from 'dedent-tabs';
import Nav from 'naturalcrit/nav/nav.jsx'; import Nav from 'naturalcrit/nav/nav.jsx';
const getShareId = (brew)=>( const getShareId = (brew)=>(
brew.googleId && !brew.stubbed brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId ? brew.googleId + brew.shareId
: brew.shareId : brew.shareId
); );
const getRedditLink = (brew)=>{ const getRedditLink = (brew)=>{
const text = dedent` const text = dedent`
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`; **[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`; return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
}; };
export default ({brew}) => ( export default ({ brew })=>(
<Nav.dropdown> <Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'> <Nav.item color='teal' icon='fas fa-share-alt'>
share share

View File

@@ -123,16 +123,16 @@ const EditPage = (props)=>{
editorRef.current?.update(); editorRef.current?.update();
}; };
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme') if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback //If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);

View File

@@ -39,8 +39,8 @@ const HomePage =(props)=>{
props = { props = {
brew : DEFAULT_BREW, brew : DEFAULT_BREW,
ver : '0.0.0', ver : '0.0.0',
...props ...props
}; };
const [currentBrew , setCurrentBrew] = useState(props.brew); const [currentBrew , setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined); const [error , setError] = useState(undefined);
@@ -71,7 +71,7 @@ const HomePage =(props)=>{
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
return () => { return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
}; };
}, []); }, []);
@@ -100,16 +100,16 @@ const HomePage =(props)=>{
editorRef.current.update(); editorRef.current.update();
}; };
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme') if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback //If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -218,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' /> Create your own <i className='fas fa-magic' />
</a> </a>
</div> </div>
) );
}; };
module.exports = HomePage; module.exports = HomePage;

View File

@@ -36,9 +36,9 @@ const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true; const useLocalStorage = true;
const neverSaved = true; const neverSaved = true;
const NewPage = (props) => { const NewPage = (props)=>{
props = { props = {
brew: DEFAULT_BREW, brew : DEFAULT_BREW,
...props ...props
}; };
@@ -57,7 +57,7 @@ const NewPage = (props) => {
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(() => { useEffect(()=>{
loadBrew(); loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
@@ -73,7 +73,7 @@ const NewPage = (props) => {
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
return () => { return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
}; };
}, []); }, []);
@@ -118,16 +118,16 @@ const NewPage = (props) => {
editorRef.current.update(); editorRef.current.update();
}; };
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme') if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback //If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -141,10 +141,10 @@ const NewPage = (props) => {
} }
}; };
const save = async () => { const save = async ()=>{
setIsSaving(true); setIsSaving(true);
let updatedBrew = { ...currentBrew }; const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew); splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
@@ -153,13 +153,13 @@ const NewPage = (props) => {
const res = await request const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew) .send(updatedBrew)
.catch((err) => { .catch((err)=>{
setIsSaving(false); setIsSaving(false);
setError(err); setError(err);
}); });
setIsSaving(false) setIsSaving(false);
if (!res) return; if(!res) return;
const savedBrew = res.body; const savedBrew = res.body;
@@ -209,7 +209,7 @@ const NewPage = (props) => {
setIsSaving(false); setIsSaving(false);
}; };
const renderNavbar = () => ( const renderNavbar = ()=>(
<Navbar> <Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> <Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
@@ -230,7 +230,7 @@ const NewPage = (props) => {
); );
return ( return (
<div className='newPage sitePage'> <div className='newPage sitePage'>
{renderNavbar()} {renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={handleSplitMove}>

View File

@@ -8,9 +8,9 @@ import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch'; import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata, import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js'; brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js'; import checkClientVersion from './middleware/check-client-version.js';
@@ -377,14 +377,14 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
if(patchedResult != brewFromClient.text) if(patchedResult != brewFromClient.text)
throw("Patches did not apply cleanly, text mismatch detected"); throw ('Patches did not apply cleanly, text mismatch detected');
// brew.text = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) { } catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', { console.error('Failed to apply patches:', {
//patches : brewFromClient.patches, //patches : brewFromClient.patches,
brewId : brewFromClient.editId || 'unknown', brewId : brewFromClient.editId || 'unknown',
error : err error : err
}); });
// While running in parallel, don't throw the error upstream. // While running in parallel, don't throw the error upstream.
// throw err; // rethrow to preserve the 500 behavior // throw err; // rethrow to preserve the 500 behavior

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = []; const mpAsSnippets = [];
// Snippets from Themes first. // Snippets from Themes first.
if(themeBundleSnippets) { if(themeBundleSnippets) {
for (let themes of themeBundleSnippets) { for (const themes of themeBundleSnippets) {
if(typeof themes !== 'string') { if(typeof themes !== 'string') {
const userSnippets = []; const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1); const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
if(typeof yamlObj == 'string') return yamlObj; if(typeof yamlObj == 'string') return yamlObj;
let snippetsText = ''; let snippetsText = '';
for (let snippet of yamlObj) { for (const snippet of yamlObj) {
for (let subSnippet of snippet.subsnippets) { for (const subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
} }
} }
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{ .catch((err)=>{
setError(err) setError(err);
}); });
if(!res) { if(!res) {
setThemeBundle({}); setThemeBundle({});
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
setError(null); setError(null);
}; };
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
const clientText = clientTextRaw?.normalize('NFC') || ''; const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || ''; const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8'); const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8'); const serverBuffer = Buffer.from(serverText, 'utf8');
if (clientBuffer.equals(serverBuffer)) { if(clientBuffer.equals(serverBuffer)) {
console.log(`${label} text matches byte-for-byte.`); console.log(`${label} text matches byte-for-byte.`);
return; return;
} }
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Byte-level diff // Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) { for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
if (clientBuffer[i] !== serverBuffer[i]) { if(clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`); console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break; break;
} }
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Char-level diff // Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) { for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if (clientText[i] !== serverText[i]) { if(clientText[i] !== serverText[i]) {
console.log(`Char mismatch at index ${i}:`); console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`); console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`); console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
break; break;
} }
} }
} };
export { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,

View File

@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
try { try {
return mathParser.evaluate(replacedLabel); return mathParser.evaluate(replacedLabel);
} catch (error) { } catch {
return undefined; // Return undefined if invalid math result return undefined; // Return undefined if invalid math result
} }
} }
@@ -680,7 +680,7 @@ const tableTerminators = [
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use(MarkedDefinitionLists()); Marked.use(MarkedDefinitionLists());
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock); Marked.use(mustacheInjectBlock);
Marked.use(MarkedAlignedParagraphs()); Marked.use(MarkedAlignedParagraphs());
Marked.use(MarkedSubSuperText()); Marked.use(MarkedSubSuperText());

View File

@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href)) prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '') .replace(nonWordAndColonTest, '')
.toLowerCase(); .toLowerCase();
} catch (e) { } catch {
return null; return null;
} }
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
} }
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch (e) { } catch {
return null; return null;
} }
return href; return href;