Use fira code in editor (#695)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2020-12-14 23:58:46 +01:00 committed by GitHub
parent b3288a6666
commit 8ce344512c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 311 additions and 113 deletions

View file

@ -56,6 +56,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- Code blocks with 'vega-lite' as language are rendered as [vega-lite diagrams](https://vega.github.io/vega-lite/examples/).
- Markdown files can be imported into an existing note directly from the editor.
- The table button in the toolbar opens an overlay where the user can choose the number of columns and rows
- A toggle in the editor preferences for turning ligatures on and off.
### Changed
@ -70,6 +71,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- Use KaTeX instead of MathJax. ([Why?](https://community.codimd.org/t/frequently-asked-questions/190))
- The dark-mode is also applied to the read-only-view and can be toggled from there.
- Access tokens for the CLI and 3rd-party-clients can be managed in the user profile.
- Change editor font to "Fira Code"
---

View file

@ -49,9 +49,9 @@
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.2.1",
"fast-deep-equal": "3.1.3",
"firacode": "5.2.0",
"flowchart.js": "1.15.0",
"fontsource-source-sans-pro": "3.1.5",
"fontsource-source-code-pro": "3.1.5",
"fork-awesome": "1.1.7",
"highlight.js": "10.4.1",
"i18next": "19.8.4",

View file

@ -389,11 +389,29 @@
},
"preferences": {
"title": "Preferences",
"theme": "Editor theme",
"keyMap": "Keymap",
"indentWithTabs": "Tab character",
"indentUnit": "Tab size (when using spaces)",
"spellcheck": "Spell checking"
"theme": {
"label": "Editor theme",
"one-dark": "Dark",
"neat": "Light"
},
"keyMap": {
"label": "Keymap",
"sublime": "Sublime",
"emacs": "Emacs",
"vim": "Vim"
},
"indentWithTabs": {
"label": "Tab character",
"on": "Tabs",
"off": "Spaces"
},
"indentUnit": "Number of spaces per tab",
"spellcheck": {
"label": "Spell checking"
},
"ligatures": {
"label": "Show ligatures"
}
}
},
"embeddings": {
@ -413,6 +431,8 @@
}
},
"common": {
"yes": "Yes",
"no": "No",
"import": "Import",
"export": "Export",
"refresh": "Refresh",

View file

@ -11,11 +11,23 @@
@import '../../../../node_modules/codemirror/theme/neat';
@import './one-dark';
@import 'hints';
@import '../../../../node_modules/firacode/distr/fira_code.css';
.CodeMirror {
font-family: "Source Code Pro", "Twemoji Mozilla", Consolas, monaco, monospace;
font-family: "Fira Code", "Twemoji Mozilla", Consolas, monaco, monospace;
letter-spacing: 0.025em;
line-height: 1.25;
font-size: 18px;
height: 100%;
}
.no-ligatures .CodeMirror {
//These two properties must be set separately because otherwise node-scss breaks.
.CodeMirror-line, .CodeMirror-line-like {
font-feature-settings: inherit;
}
.CodeMirror-line, .CodeMirror-line-like {
font-variant-ligatures: none;
}
}

View file

@ -69,6 +69,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
const [editor, setEditor] = useState<Editor>()
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
const editorPreferences = useSelector((state: ApplicationState) => state.editorConfig.preferences, equal)
const ligaturesEnabled = useSelector((state: ApplicationState) => state.editorConfig.ligatures, equal)
const lastScrollPosition = useRef<number>()
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
@ -163,7 +164,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
editor={editor}
/>
<ControlledCodeMirror
className="overflow-hidden w-100 flex-fill"
className={`overflow-hidden w-100 flex-fill ${ligaturesEnabled ? '' : 'no-ligatures'}`}
value={content}
options={codeMirrorOptions}
onChange={onChange}

View file

@ -0,0 +1,45 @@
/*
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorConfiguration } from 'codemirror'
import equal from "fast-deep-equal"
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
export interface EditorPreferenceBooleanProps {
property: EditorPreferenceProperty
}
export const EditorPreferenceBooleanProperty: React.FC<EditorPreferenceBooleanProps> = ({ property }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
const { t } = useTranslation()
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: boolean = event.target.value === 'true'
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
const i18nPrefix = `editor.modal.preferences.${property}`
return (
<EditorPreferenceInput onChange={selectItem} property={property} type={EditorPreferenceInputType.SELECT} value={preference}>
<option value={'true'}>
{t(`${i18nPrefix}.on`)}
</option>
<option value={'false'}>
{t(`${i18nPrefix}.off`)}
</option>
</EditorPreferenceInput>
)
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
export enum EditorPreferenceInputType {
SELECT,
BOOLEAN,
NUMBER
}
export interface EditorPreferenceInputProps {
property: string
type: EditorPreferenceInputType
onChange: React.ChangeEventHandler<HTMLSelectElement>
value?: string | number | string[]
}
export const EditorPreferenceInput: React.FC<EditorPreferenceInputProps> = ({ property, type, onChange, value, children }) => {
useTranslation()
return (
<Form.Group controlId={`editor-pref-${property}`}>
<Form.Label>
<Trans i18nKey={`editor.modal.preferences.${property}${type===EditorPreferenceInputType.NUMBER ? '' : '.label'}`}/>
</Form.Label>
<Form.Control
as={type === EditorPreferenceInputType.NUMBER ? 'input' : 'select'}
size='sm'
value={value}
onChange={onChange}
type={type === EditorPreferenceInputType.NUMBER ? 'number' : ''}>
{children}
</Form.Control>
</Form.Group>
)
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { setEditorLigatures } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
export const EditorPreferenceLigaturesSelect: React.FC = () => {
const ligaturesEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.ligatures).toString())
const saveLigatures = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const ligaturesActivated: boolean = event.target.value === 'true'
setEditorLigatures(ligaturesActivated)
}, [])
const { t } = useTranslation()
return (
<EditorPreferenceInput onChange={saveLigatures} value={ligaturesEnabled} property={"ligatures"}
type={EditorPreferenceInputType.BOOLEAN}>
<option value='true'>{t(`common.yes`)}</option>
<option value='false'>{t(`common.no`)}</option>
</EditorPreferenceInput>
)
}

View file

@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorConfiguration } from 'codemirror'
import equal from "fast-deep-equal"
import React, { ChangeEvent, useCallback } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
export interface EditorPreferenceNumberProps {
property: EditorPreferenceProperty
}
export const EditorPreferenceNumberProperty: React.FC<EditorPreferenceNumberProps> = ({ property }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: number = Number.parseInt(event.target.value)
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
return (
<EditorPreferenceInput onChange={selectItem} property={property} type={EditorPreferenceInputType.NUMBER} value={preference}/>
)
}

View file

@ -0,0 +1,13 @@
/*
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
export enum EditorPreferenceProperty {
KEYMAP = 'keyMap',
THEME = 'theme',
INDENT_WITH_TABS = 'indentWithTabs',
INDENT_UNIT = 'indentUnit',
SPELL_CHECK = 'spellcheck'
}

View file

@ -0,0 +1,45 @@
/*
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorConfiguration } from 'codemirror'
import equal from "fast-deep-equal"
import React, { ChangeEvent, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { mergeEditorPreferences } from '../../../../../redux/editor/methods'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceProperty } from './editor-preference-property'
export interface EditorPreferenceSelectPropertyProps {
property: EditorPreferenceProperty
selections: string[]
}
export const EditorPreferenceSelectProperty: React.FC<EditorPreferenceSelectPropertyProps> = ({ property, selections }) => {
const preference = useSelector((state: ApplicationState) => state.editorConfig.preferences[property]?.toString() || '', equal)
const { t } = useTranslation()
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
const selectedItem: string = event.target.value
mergeEditorPreferences({
[property]: selectedItem
} as EditorConfiguration)
}, [property])
const i18nPrefix = `editor.modal.preferences.${property}`
return (
<EditorPreferenceInput onChange={selectItem} property={property} type={EditorPreferenceInputType.SELECT} value={preference}>
{selections.map(selection =>
<option key={selection} value={selection}>
{t(`${i18nPrefix}.${selection}`) }
</option>)}
</EditorPreferenceInput>
)
}

View file

@ -1,60 +0,0 @@
/*
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorConfiguration } from 'codemirror'
import React, { ChangeEvent, useCallback, useState } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
export enum EditorPreferenceProperty {
KEYMAP = 'keyMap',
THEME = 'theme',
INDENT_WITH_TABS = 'indentWithTabs',
INDENT_UNIT = 'indentUnit',
SPELL_CHECK= 'spellcheck'
}
export interface EditorPreferenceSelectProps {
onChange: (config: EditorConfiguration) => void
preferences: EditorConfiguration
property: EditorPreferenceProperty
}
export const EditorPreferenceSelect: React.FC<EditorPreferenceSelectProps> = ({ property, onChange, preferences, children }) => {
useTranslation()
const [selected, setSelected] = useState(preferences[property])
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
let selectedItem: string | boolean | number = event.target.value
if (property === EditorPreferenceProperty.INDENT_UNIT) {
selectedItem = parseInt(selectedItem)
}
setSelected(selectedItem)
if (property === EditorPreferenceProperty.INDENT_WITH_TABS) {
selectedItem = selectedItem === 'true'
}
onChange({
...preferences,
[property]: selectedItem
})
}, [preferences, property, setSelected, onChange])
return (
<Form.Group controlId={`editor-pref-${property}`}>
<Form.Label>
<Trans i18nKey={`editor.modal.preferences.${property}`}/>
</Form.Label>
<Form.Control
as={property === EditorPreferenceProperty.INDENT_UNIT ? 'input' : 'select'}
size='sm'
value={selected as string | number}
onChange={selectItem}
type={property === EditorPreferenceProperty.INDENT_UNIT ? 'number' : ''}>
{ children }
</Form.Control>
</Form.Group>
)
}

View file

@ -4,26 +4,26 @@ SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
*/
import { EditorConfiguration } from 'codemirror'
import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useState } from 'react'
import equal from "fast-deep-equal"
import React, { Fragment, useState } from 'react'
import { Button, Form, ListGroup } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../../../redux'
import { setEditorPreferences } from '../../../../../redux/editor/methods'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
import { CommonModal } from '../../../../common/modals/common-modal'
import { EditorPreferenceProperty, EditorPreferenceSelect } from './editor-preference-select'
import { ShowIf } from '../../../../common/show-if/show-if'
import { EditorPreferenceBooleanProperty } from './editor-preference-boolean-property'
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
import { EditorPreferenceLigaturesSelect } from './editor-preference-ligatures-select'
import { EditorPreferenceNumberProperty } from './editor-preference-number-property'
import { EditorPreferenceProperty } from "./editor-preference-property"
import { EditorPreferenceSelectProperty } from "./editor-preference-select-property"
export const EditorPreferences: React.FC = () => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const preferences = useSelector((state: ApplicationState) => state.editorConfig.preferences, equal)
const sendPreferences = useCallback((newPreferences: EditorConfiguration) => {
setEditorPreferences(newPreferences)
}, [])
const indentWithTabs = useSelector((state: ApplicationState) => state.editorConfig.preferences.indentWithTabs ?? false, equal)
return (
<Fragment>
@ -39,32 +39,27 @@ export const EditorPreferences: React.FC = () => {
<Form>
<ListGroup>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.THEME}>
<option value='one-dark'>Dark</option>
<option value='neat'>Light</option>
</EditorPreferenceSelect>
<EditorPreferenceSelectProperty property={EditorPreferenceProperty.THEME} selections={['one-dark', 'neat']}/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.KEYMAP}>
<option value='sublime'>Sublime</option>
<option value='emacs'>Emacs</option>
<option value='vim'>Vim</option>
</EditorPreferenceSelect>
<EditorPreferenceSelectProperty property={EditorPreferenceProperty.KEYMAP} selections={['sublime', 'emacs', 'vim']}/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.INDENT_WITH_TABS}>
<option value='false'>Spaces</option>
<option value='true'>Tab</option>
</EditorPreferenceSelect>
<EditorPreferenceBooleanProperty property={EditorPreferenceProperty.INDENT_WITH_TABS}/>
</ListGroup.Item>
<ShowIf condition={!indentWithTabs}>
<ListGroup.Item>
<EditorPreferenceNumberProperty property={EditorPreferenceProperty.INDENT_UNIT}/>
</ListGroup.Item>
</ShowIf>
<ListGroup.Item>
<EditorPreferenceLigaturesSelect/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.INDENT_UNIT}/>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={() => alert('This feature is not yet implemented.')} preferences={preferences} property={EditorPreferenceProperty.SPELL_CHECK}>
<option value='off'>off</option>
<EditorPreferenceInput onChange={() => alert('This feature is not yet implemented.')} property={EditorPreferenceProperty.SPELL_CHECK} type={EditorPreferenceInputType.SELECT}>
<option value='off'>Off</option>
<option value='en'>English</option>
</EditorPreferenceSelect>
</EditorPreferenceInput>
</ListGroup.Item>
</ListGroup>
</Form>

View file

@ -37,7 +37,7 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
'line-color': darkModeActivated ? '#ffffff' : '#000000',
'element-color': darkModeActivated ? '#ffffff' : '#000000',
'font-color': darkModeActivated ? '#ffffff' : '#000000',
'font-family': 'Source Code Pro, "Twemoji Mozilla", monospace'
'font-family': 'Source Sans Pro, "Twemoji Mozilla", monospace'
})
setError(false)
} catch (error) {

View file

@ -11,6 +11,7 @@ import {
EditorConfig,
EditorConfigActionType,
SetEditorConfigAction,
SetEditorLigaturesAction,
SetEditorPreferencesAction,
SetEditorSyncScrollAction
} from './types'
@ -52,12 +53,18 @@ export const setEditorSyncScroll = (syncScroll: boolean): void => {
store.dispatch(action)
}
export const setEditorPreferences = (preferences: EditorConfiguration): void => {
export const setEditorLigatures = (ligatures: boolean): void => {
const action: SetEditorLigaturesAction = {
type: EditorConfigActionType.SET_LIGATURES,
ligatures: ligatures
}
store.dispatch(action);
}
export const mergeEditorPreferences = (preferences: EditorConfiguration): void => {
const action: SetEditorPreferencesAction = {
type: EditorConfigActionType.SET_EDITOR_PREFERENCES,
preferences: {
...preferences
}
type: EditorConfigActionType.MERGE_EDITOR_PREFERENCES,
preferences: preferences
}
store.dispatch(action)
}

View file

@ -12,12 +12,14 @@ import {
EditorConfigActions,
EditorConfigActionType,
SetEditorConfigAction,
SetEditorLigaturesAction,
SetEditorPreferencesAction,
SetEditorSyncScrollAction
} from './types'
const initialState: EditorConfig = {
editorMode: EditorMode.BOTH,
ligatures: true,
syncScroll: true,
preferences: {
theme: 'one-dark',
@ -28,7 +30,7 @@ const initialState: EditorConfig = {
}
const getInitialState = (): EditorConfig => {
return loadFromLocalStorage() ?? initialState
return { ...initialState, ...loadFromLocalStorage() }
}
export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (state: EditorConfig = getInitialState(), action: EditorConfigActions) => {
@ -48,10 +50,19 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
}
saveToLocalStorage(newState)
return newState
case EditorConfigActionType.SET_EDITOR_PREFERENCES:
case EditorConfigActionType.SET_LIGATURES:
newState = {
...state,
preferences: (action as SetEditorPreferencesAction).preferences
ligatures: (action as SetEditorLigaturesAction).ligatures
}
saveToLocalStorage(newState)
return newState
case EditorConfigActionType.MERGE_EDITOR_PREFERENCES:
newState = {
...state,
preferences: {
...state.preferences, ...(action as SetEditorPreferencesAction).preferences
}
}
saveToLocalStorage(newState)
return newState

View file

@ -11,12 +11,14 @@ import { EditorMode } from '../../components/editor/app-bar/editor-view-mode'
export enum EditorConfigActionType {
SET_EDITOR_VIEW_MODE = 'editor/mode/set',
SET_SYNC_SCROLL = 'editor/syncScroll/set',
SET_EDITOR_PREFERENCES = 'editor/preferences/set'
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
SET_LIGATURES = 'editor/preferences/setLigatures'
}
export interface EditorConfig {
editorMode: EditorMode;
syncScroll: boolean;
ligatures: boolean
preferences: EditorConfiguration
}
@ -28,6 +30,10 @@ export interface SetEditorSyncScrollAction extends EditorConfigActions {
syncScroll: boolean
}
export interface SetEditorLigaturesAction extends EditorConfigActions {
ligatures: boolean
}
export interface SetEditorConfigAction extends EditorConfigActions {
mode: EditorMode
}

View file

@ -8,7 +8,6 @@
@import "../../node_modules/bootstrap/scss/bootstrap";
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
@import "~fontsource-source-sans-pro/index.css";
@import "~fontsource-source-code-pro/index.css";
@import "fonts/twemoji/twemoji";
@import '../../node_modules/fork-awesome/css/fork-awesome.min';

View file

@ -6869,6 +6869,11 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
firacode@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/firacode/-/firacode-5.2.0.tgz#2eab5b4f59e3197d5e84f15a25485a88f0cd3180"
integrity sha512-Q1SO7vibzYcT+KaohGK0ypGS58zLtO2o2UuiLvngTEhBbHQoZmZ4DAiugj0MNNpeM4jG9gM9NyusVDM3MxYP7A==
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -6908,11 +6913,6 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
fontsource-source-code-pro@3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/fontsource-source-code-pro/-/fontsource-source-code-pro-3.1.5.tgz#0f000f06dfa7220f62c64f572e5d9ee1fd087a8e"
integrity sha512-eoYyrTaNcgrWc62w0pImfualDr2JE4SKpeGlEqV+Y34tHYbuD5jTIkzx3GrDH4eDw9KDS5wQdlnSVOj+SVy0Zw==
fontsource-source-sans-pro@3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/fontsource-source-sans-pro/-/fontsource-source-sans-pro-3.1.5.tgz#575edceadf0e68603c1c74b087f4423738cce2f9"