diff --git a/.binder/environment.yml b/.binder/environment.yml index 76c5fa50..d1d75e6e 100644 --- a/.binder/environment.yml +++ b/.binder/environment.yml @@ -28,6 +28,7 @@ dependencies: - geckodriver - robotframework >=6 - robotframework-jupyterlibrary >=0.4.1 + - robotframework-pabot - robotframework-robocop >=2.6.0 - robotframework-tidy >=3.3.1 # demo toys diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 16cbb150..17fdf24b 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -9,7 +9,6 @@ labels: maintenance - [ ] ensure the CHANGELOG is up-to-date - [ ] move the new release to the top of the stack - [ ] validate on binder -- [ ] validate on ReadTheDocs - [ ] wait for a successful build of `main` - [ ] download the `dist` archive and unpack somewhere (maybe a fresh `dist`) - [ ] create a new release through the GitHub UI @@ -20,7 +19,7 @@ labels: maintenance cd dist twine upload *.tar.gz *.whl npm login - for tarball in deathbeds-jupyterlab-fonts-*.tar.gz; do + for tarball in deathbeds-jupyterlab-font*.tar.gz; do npm publish .tgz done npm logout diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73c23f31..a7b5d05b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: - name: upload (dist) uses: actions/upload-artifact@v3 with: - name: jupyterlab-fonts ${{ github.run_number }} dist + name: jupyterlab-fonts-${{ github.run_number }}-dist path: ./dist test: @@ -142,15 +142,10 @@ jobs: matrix.python-version }} path: ./build/${{ matrix.os }}-${{ matrix.python-version }}.conda.lock - - name: uninstall node - shell: bash -l {0} - run: | - mamba uninstall -y nodejs - - name: download (dist) uses: actions/download-artifact@v3 with: - name: jupyterlab-fonts ${{ github.run_number }} dist + name: jupyterlab-fonts-${{ github.run_number }}-dist path: ./dist - name: clean @@ -173,10 +168,10 @@ jobs: shell: bash -l {0} run: doit test || doit test || doit test - - name: upload (atest) + - name: upload (reports) if: always() uses: actions/upload-artifact@v3 with: name: | - jupyterlab-fonts ${{ github.run_number }} atest ${{ runner.os }} ${{ matrix.python-version }} - path: ./build/atest + jupyterlab-fonts-${{ github.run_number }}-reports-${{ runner.os }}-f${{ matrix.python-version }} + path: ./build/reports diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbc7572..d877c6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog -## v2.1.0 (unreleased) +## v2.1.1 (unreleased) + +- fix some errors when disposing notebooks +- normalize generated CSS +- allow for dereferencing local asset `url()`s for `@import`, etc. + +## v2.1.0 - Improve notebook-level `@import`, `@font-face`, etc. - adds `data-jpf-cell-id` and `data-jpf-cell-tags` to notebook cell elements diff --git a/atest/Cells.robot b/atest/Cells.robot new file mode 100644 index 00000000..1736f514 --- /dev/null +++ b/atest/Cells.robot @@ -0,0 +1,51 @@ +*** Settings *** +Documentation Cell-level styling works + +Library JupyterLibrary +Resource ./_keywords.resource + +Suite Setup Set Attempt Screenshot Directory cells +Suite Teardown Reset JupyterLab And Close With Coverage +Test Teardown Clean Up After Cell Test + + +*** Variables *** +${NS} @deathbeds/jupyterlab-fonts +${META_EMPTY} {} +${META_RED} {"tags":["red"], "${NS}": {"styles": {"background-color": "red"}}} +${META_IMPORT} {"${NS}": {"styles": {"@import": "url('./style.css')"}}} +${RED_TAG} css:[${DATA_TAGS}=",red,"] +${ID_TAG} css:[${DATA_CELL_ID}] + + +*** Test Cases *** +Cell Styling + Launch A New JupyterLab Document + Wait Until JupyterLab Kernel Is Idle + Stylesheet Should Not Contain background-color: red + Wait Until Page Contains Element ${ID_TAG} + Wait Until Page Does Not Contain Element ${RED_TAG} + Set Cell Metadata ${META_RED} 1 00-red.png + Wait Until Page Contains Element ${RED_TAG} + Stylesheet Should Contain background-color: red + Set Cell Metadata ${META_EMPTY} 1 01-empty.png + Wait Until Page Does Not Contain Element ${RED_TAG} + Stylesheet Should Not Contain background-color: red + Capture Page Screenshot 02-end.png + +Importing + ${nbdir} = Get Jupyter Directory + ${nburl} = Get Jupyter Server URL + Create File ${nbdir}${/}style.css body { background-color: green; } + Launch A New JupyterLab Document + Wait Until JupyterLab Kernel Is Idle + Set Cell Metadata ${META_IMPORT} 1 10-green.png + Stylesheet Should Contain @import url('${nburl}files/./style.css') + Capture Page Screenshot 11-end.png + + +*** Keywords *** +Clean Up After Cell Test + Execute JupyterLab Command Close All Tabs + ${nbdir} = Get Jupyter Directory + Remove File ${nbdir}${/}Untitled.ipynb diff --git a/atest/Editor.robot b/atest/Editor.robot deleted file mode 100644 index 99839a33..00000000 --- a/atest/Editor.robot +++ /dev/null @@ -1,170 +0,0 @@ -*** Settings *** -Documentation The font editor allows changing fonts in notebooks - -Library JupyterLibrary -Library BuiltIn -Library OperatingSystem -Resource ./_keywords.resource -Resource ./_coverage.resource - - -*** Variables *** -${ED} css:.jp-FontsEditor -${DOCK} //div[@id='jp-main-dock-panel'] -${TAB} li[contains(@class, 'lm-TabBar-tab')] -${ICON_FONT} *[@data-icon = 'fonts:fonts'] -${ICON_LICENSE} *[@data-icon = 'fonts:license'] -${ICON_SETTINGS} *[@data-icon = 'ui-components:settings'] -${ICON_NOTEBOOK} *[@data-icon = 'ui-components:notebook'] -${ICON_CLOSE} div[contains(@class, 'lm-TabBar-tabCloseIcon')] -${BUTTON} .jp-FontsEditor-button -${SETTING_ITEM} //div[contains(@class, 'jp-PluginList')]//div -${SETTINGS_RAW_CM} .jp-SettingsRawEditor-user .CodeMirror - - -*** Test Cases *** -Global Font Editor - [Documentation] Customize Global fonts with the Font Editor - [Template] Use the font editor to configure fonts - [Setup] Open the Global Font Editor - Global Code Anonymous Pro Bold - Global Code Anonymous Pro Regular - Global Code DejaVu Sans Mono - Global Code DejaVu Sans Mono Bold - Global Code Fira Code Bold - Global Code Fira Code Light - Global Code Fira Code Medium - Global Code Fira Code Regular - Global Content Anonymous Pro Bold - Global Content Anonymous Pro Regular - Global Content DejaVu Sans Mono - Global Content DejaVu Sans Mono Bold - Global Content Fira Code Bold - Global Content Fira Code Light - Global Content Fira Code Medium - Global Content Fira Code Regular - [Teardown] Close the Font Editor Global - -Notebook Font Editor - [Documentation] Customize Notebook fonts with the Font Editor - [Template] Use the font editor to configure fonts - [Setup] Open the Notebook Font Editor - Notebook Code - - Notebook Code Anonymous Pro Bold - Notebook Code Anonymous Pro Regular - Notebook Code DejaVu Sans Mono - Notebook Code DejaVu Sans Mono Bold - Notebook Code Fira Code Bold - Notebook Code Fira Code Light - Notebook Code Fira Code Medium - Notebook Code Fira Code Regular - Notebook Content - - Notebook Content Anonymous Pro Bold - Notebook Content Anonymous Pro Regular - Notebook Content DejaVu Sans Mono - Notebook Content DejaVu Sans Mono Bold - Notebook Content Fira Code Bold - Notebook Content Fira Code Light - Notebook Content Fira Code Medium - Notebook Content Fira Code Regular - [Teardown] Close the Font Editor Untitled - -Global Enable/Disable - [Documentation] Test enabling and disabling custom fonts - [Setup] Open the Global Font Editor - Set Screenshot Directory ${OUTPUT_DIR}${/}global_editor - Use the Global Font Editor to disable custom fonts - Use the Global Font Editor to enable custom fonts - [Teardown] Close the Font Editor Global - - -*** Keywords *** -Prepare to test a font editor - [Documentation] Open a notebook and settings - Execute JupyterLab Command Close All Tabs - Make a Font Test Notebook - -Open Advanced Settings to Validate Fonts - [Documentation] use advanced settings to validate changes - Execute JupyterLab Command Advanced JSON Settings Editor - ${settings} = Set Variable ${DOCK}//${TAB}//${ICON_SETTINGS}/../.. - ${fonts} = Set Variable ${SETTING_ITEM}//${ICON_FONT} - Wait Until Page Contains Element ${fonts} - Click Element ${fonts} - Drag And Drop By Offset ${settings} 0 600 - Click Element css:.jp-Notebook .CodeMirror - -Open the Global Font Editor - [Documentation] Use the JupyterLab Menu to open the global font editor - Prepare to test a font editor - Open With JupyterLab Menu Settings Fonts Global Fonts... - Open Advanced Settings to Validate Fonts - -Open the Notebook Font Editor - [Documentation] Use the Notebook button bar to open the notebook font editor - Prepare to test a font editor - Click Element css:.jp-Toolbar-item [data-icon\='fonts:fonts'] - Open Advanced Settings to Validate Fonts - -Close the Font Editor - [Documentation] Close the Notebook Font Editor by closing the tab - [Arguments] ${kind}=Global - Close JupyterLab Dock Panel Tab ${kind} - Execute JupyterLab Command Close All Tabs - ${dir} = Get Jupyter Directory - Remove File ${dir}${/}Untitled.ipynb - Capture Page Coverage - -Close the License Viewer - [Documentation] Close the Font License Viewer by closing the tab - Click Element ${DOCK}//${TAB}//${ICON_LICENSE}/../../${ICON_CLOSE} - -Use the font editor to configure fonts - [Documentation] Presently, change a dropdown in the font editor - [Arguments] ${scope} ${kind} ${font} - Set Screenshot Directory ${OUTPUT_DIR}${/}editor${/}${scope}${/}${kind}${/}${font} - Change a Font Dropdown ${scope} ${kind} Font ${font} 0 - IF "${scope}" == "Global" Check font license is visible in Editor - Change a Font Dropdown ${scope} ${kind} Size - 0 - FOR ${size} IN RANGE 12 20 4 - Change a Font Dropdown ${scope} ${kind} Size ${size}px ${size} - IF "${scope}" == "Global" Settings Should Contain ${font} - END - Change a Font Dropdown ${scope} ${kind} Font - 99 - -Change a Font Dropdown - [Documentation] Update a particular typography value - [Arguments] ${scope} ${kind} ${aspect} ${value} ${idx}=0 - ${sel} = Set Variable ${ED} section[title="${kind}"] select[title="${aspect}"] - Select From List By Label ${sel} ${value} - Capture Page Screenshot ${aspect}_${idx}.png - IF "${scope}" == "Global" Settings Should Contain ${value} - -Check font license is visible in Editor - [Documentation] Verify that the licenses are loaded - Click Element css:.jp-FontsEditor-field ${BUTTON} - Wait Until Page Contains Element css:.jp-LicenseViewer pre timeout=20s - Capture Page Screenshot Font_1_license.png - Close the License Viewer - -Use the Global font editor to ${what} custom fonts - [Documentation] Presently, change a checkbox in the font editor - Set Screenshot Directory ${OUTPUT_DIR}${/}editor${/}Global${/}${what} - ${input} = Set Variable ${ED}-enable input - Capture Page Screenshot 00_before.png - IF "${what}"=="enable" Select Checkbox ${input} - IF "${what}"=="disable" Unselect Checkbox ${input} - Capture Page Screenshot 01_after.png - -Settings Should Contain - [Documentation] Check the settings for a string - [Arguments] ${value} - ${settings} = Get Editor Content ${SETTINGS_RAW_CM} - IF "${value}" != '-' Should Contain ${settings} ${value} - -Get Editor Content - [Documentation] Get CodeMirror content - [Arguments] ${sel} - ${js} = Set Variable return document.querySelector(`${sel}`).CodeMirror.getValue() - ${content} = Execute JavaScript ${js} - RETURN ${content} diff --git a/atest/Global.robot b/atest/Global.robot new file mode 100644 index 00000000..0863b93d --- /dev/null +++ b/atest/Global.robot @@ -0,0 +1,28 @@ +*** Settings *** +Documentation The font editor allows changing fonts in notebooks + +Resource ./_keywords.resource + + +*** Test Cases *** +Global Font Editor + [Documentation] Customize Global fonts with the Font Editor + [Template] Use the font editor to configure fonts + [Setup] Open the Global Font Editor + Global Code Anonymous Pro Bold + Global Code Anonymous Pro Regular + Global Code DejaVu Sans Mono + Global Code DejaVu Sans Mono Bold + Global Code Fira Code Bold + Global Code Fira Code Light + Global Code Fira Code Medium + Global Code Fira Code Regular + Global Content Anonymous Pro Bold + Global Content Anonymous Pro Regular + Global Content DejaVu Sans Mono + Global Content DejaVu Sans Mono Bold + Global Content Fira Code Bold + Global Content Fira Code Light + Global Content Fira Code Medium + Global Content Fira Code Regular + [Teardown] Close the Font Editor Global diff --git a/atest/Menus.robot b/atest/Menus.robot index 35c65350..e0364eef 100644 --- a/atest/Menus.robot +++ b/atest/Menus.robot @@ -1,10 +1,7 @@ *** Settings *** Documentation Test whether the JupyterLab Fonts Menu performs as advertised. -Library JupyterLibrary -Library BuiltIn Resource ./_keywords.resource -Resource ./_coverage.resource Suite Setup Prepare Menu Test @@ -45,18 +42,3 @@ Customize code font with the JupyterLab Menu Content Size Default Content Size Content Size 20px Content Size Default Content Size - - -*** Keywords *** -Prepare Menu Test - Set Screenshot Directory ${OUTPUT_DIR}${/}menus - Execute JupyterLab Command Close All Tabs - Make a Font Test Notebook - -Use the Menu to configure Font - [Documentation] Set a font value in the JupyterLab Fonts Menu - [Arguments] ${kind} ${aspect} ${setting} - Wait Until Keyword Succeeds 5x 0.25s - ... Open With JupyterLab Menu Settings Fonts ${kind} ${aspect} ${setting} - Capture Page Screenshot ${kind}-${aspect}-${setting}.png - Capture Page Coverage diff --git a/atest/Notebook.robot b/atest/Notebook.robot new file mode 100644 index 00000000..b88d6489 --- /dev/null +++ b/atest/Notebook.robot @@ -0,0 +1,30 @@ +*** Settings *** +Documentation The font editor allows changing fonts in notebooks + +Resource ./_keywords.resource + + +*** Test Cases *** +Notebook Font Editor + [Documentation] Customize Notebook fonts with the Font Editor + [Template] Use the font editor to configure fonts + [Setup] Open the Notebook Font Editor + Notebook Code - + Notebook Code Anonymous Pro Bold + Notebook Code Anonymous Pro Regular + Notebook Code DejaVu Sans Mono + Notebook Code DejaVu Sans Mono Bold + Notebook Code Fira Code Bold + Notebook Code Fira Code Light + Notebook Code Fira Code Medium + Notebook Code Fira Code Regular + Notebook Content - + Notebook Content Anonymous Pro Bold + Notebook Content Anonymous Pro Regular + Notebook Content DejaVu Sans Mono + Notebook Content DejaVu Sans Mono Bold + Notebook Content Fira Code Bold + Notebook Content Fira Code Light + Notebook Content Fira Code Medium + Notebook Content Fira Code Regular + [Teardown] Close the Font Editor Untitled diff --git a/atest/__init__.robot b/atest/__init__.robot index 2e3cd9db..562f62d0 100644 --- a/atest/__init__.robot +++ b/atest/__init__.robot @@ -1,20 +1,39 @@ *** Settings *** Documentation Test interactive typography in JupyterLab +Library OperatingSystem Library JupyterLibrary +Library uuid Suite Setup Prepare for testing fonts Suite Teardown Clean up after testing fonts +Force Tags py:${py} os:${os} attempt:${attempt} + + +*** Variables *** +${LOG_DIR} ${OUTPUT_DIR}${/}logs + *** Keywords *** Prepare for testing fonts - ${py} = Evaluate __import__("sys").version.split(" ")[0] - ${platform} = Evaluate __import__("platform").system() - Set Global Variable ${PY} ${py} - Set Global Variable ${PLATFORM} ${platform} - Set Tags py:${PY} os:${platform} - Wait for New Jupyter Server to be Ready stdout=${OUTPUT DIR}${/}lab.log + ${port} = Get Unused Port + ${base_url} = Set Variable /@rf/ + ${token} = UUID4 + Create Directory ${LOG_DIR} + Wait for New Jupyter Server to be Ready + ... jupyter-lab + ... ${port} + ... ${base_url} + ... ${NONE} # notebook_dir + ... ${token.__str__()} + ... --config\=${ROOT}${/}atest${/}fixtures${/}jupyter_config.json + ... --no-browser + ... --debug + ... --port\=${port} + ... --NotebookApp.token\='${token.__str__()}' + ... --NotebookApp.base_url\='${base_url}' + ... stdout=${LOG_DIR}${/}lab.log Open JupyterLab Set Window Size 1366 768 Disable JupyterLab Modal Command Palette diff --git a/atest/_coverage.resource b/atest/_coverage.resource deleted file mode 100644 index 0f76114a..00000000 --- a/atest/_coverage.resource +++ /dev/null @@ -1,30 +0,0 @@ -*** Settings *** -Documentation Keywords for working with browser coverage data - -Library OperatingSystem -Library JupyterLibrary -Library uuid - - -*** Keywords *** -Get Next Coverage File - [Documentation] Get a random filename. - ${uuid} = UUID1 - RETURN ${uuid.__str__()} - -Capture Page Coverage - [Documentation] Fetch coverage data from the browser. - [Arguments] ${name}=${EMPTY} - IF not '''${name}''' - ${name} = Get Next Coverage File - END - ${cov_json} = Execute Javascript - ... return window.__coverage__ && JSON.stringify(window.__coverage__, null, 2) - IF ${cov_json} - Create File ${ROBOCOV}${/}${name}.json ${cov_json} - END - -Reset JupyterLab And Close With Coverage - [Documentation] Close JupyterLab after gathering coverage. - Capture Page Coverage - Reset JupyterLab And Close diff --git a/atest/_keywords.resource b/atest/_keywords.resource index ed741764..afc91a54 100644 --- a/atest/_keywords.resource +++ b/atest/_keywords.resource @@ -1,3 +1,13 @@ +*** Settings *** +Documentation Keywords for testing jupyterlab-fonts + +Resource ./_variables.resource +Library uuid +Library BuiltIn +Library OperatingSystem +Library JupyterLibrary + + *** Keywords *** Make a Font Test Notebook ${kernel} = Get Element Attribute css:.jp-LauncherCard-label[title^='Python 3'] title @@ -6,3 +16,171 @@ Make a Font Test Notebook ... from IPython.display import Markdown ... display(*[Markdown(f"{'#' * i} Hello world") for i in range(6)]) Maybe Close JupyterLab Sidebar + +Close the Font Editor + [Documentation] Close the Notebook Font Editor by closing the tab + [Arguments] ${kind}=Global + Close JupyterLab Dock Panel Tab ${kind} + Execute JupyterLab Command Close All Tabs + ${dir} = Get Jupyter Directory + Remove File ${dir}${/}Untitled.ipynb + Capture Page Coverage + +Close the License Viewer + [Documentation] Close the Font License Viewer by closing the tab + Click Element ${DOCK}//${TAB}//${ICON_LICENSE}/../../${ICON_CLOSE} + +Use the font editor to configure fonts + [Documentation] Presently, change a dropdown in the font editor + [Arguments] ${scope} ${kind} ${font} + Set Attempt Screenshot Directory editor${/}${scope}${/}${kind}${/}${font} + Change a Font Dropdown ${scope} ${kind} Font ${font} 0 + IF "${scope}" == "Global" Check font license is visible in Editor + Change a Font Dropdown ${scope} ${kind} Size - 0 + FOR ${size} IN RANGE 12 20 4 + Change a Font Dropdown ${scope} ${kind} Size ${size}px ${size} + IF "${scope}" == "Global" Settings Should Contain ${font} + END + Change a Font Dropdown ${scope} ${kind} Font - 99 + +Change a Font Dropdown + [Documentation] Update a particular typography value + [Arguments] ${scope} ${kind} ${aspect} ${value} ${idx}=0 + ${sel} = Set Variable ${ED} section[title="${kind}"] select[title="${aspect}"] + Select From List By Label ${sel} ${value} + Capture Page Screenshot ${aspect}_${idx}.png + IF "${scope}" == "Global" Settings Should Contain ${value} + +Check font license is visible in Editor + [Documentation] Verify that the licenses are loaded + Click Element css:.jp-FontsEditor-field ${BUTTON} + Wait Until Page Contains Element css:.jp-LicenseViewer pre timeout=20s + Capture Page Screenshot Font_1_license.png + Close the License Viewer + +Use the Global font editor to ${what} custom fonts + [Documentation] Presently, change a checkbox in the font editor + Set Attempt Screenshot Directory editor${/}Global${/}${what} + ${input} = Set Variable ${ED}-enable input + Capture Page Screenshot 00_before.png + IF "${what}"=="enable" Select Checkbox ${input} + IF "${what}"=="disable" Unselect Checkbox ${input} + Capture Page Screenshot 01_after.png + +Settings Should Contain + [Documentation] Check the settings for a string + [Arguments] ${value} + ${settings} = Get Editor Content ${SETTINGS_RAW_CM} + IF "${value}" != '-' Should Contain ${settings} ${value} + +Get Editor Content + [Documentation] Get CodeMirror content + [Arguments] ${sel} + ${js} = Set Variable return document.querySelector(`${sel}`).CodeMirror.getValue() + ${content} = Execute JavaScript ${js} + RETURN ${content} + +Prepare to test a font editor + [Documentation] Open a notebook and settings + Execute JupyterLab Command Close All Tabs + Make a Font Test Notebook + +Open Advanced Settings to Validate Fonts + [Documentation] use advanced settings to validate changes + Execute JupyterLab Command Advanced JSON Settings Editor + ${settings} = Set Variable ${DOCK}//${TAB}//${ICON_SETTINGS}/../.. + ${fonts} = Set Variable ${SETTING_ITEM}//${ICON_FONT} + Wait Until Page Contains Element ${fonts} + Click Element ${fonts} + Drag And Drop By Offset ${settings} 0 600 + Click Element css:.jp-Notebook .CodeMirror + +Open the Notebook Font Editor + [Documentation] Use the Notebook button bar to open the notebook font editor + Prepare to test a font editor + Click Element css:.jp-Toolbar-item [data-icon\='fonts:fonts'] + Open Advanced Settings to Validate Fonts + +Open the Global Font Editor + [Documentation] Use the JupyterLab Menu to open the global font editor + Prepare to test a font editor + Open With JupyterLab Menu Settings Fonts Global Fonts... + Open Advanced Settings to Validate Fonts + +Get Next Coverage File + [Documentation] Get a random filename. + ${uuid} = UUID1 + RETURN ${uuid.__str__()} + +Capture Page Coverage + [Documentation] Fetch coverage data from the browser. + [Arguments] ${name}=${EMPTY} + IF not '''${name}''' + ${name} = Get Next Coverage File + END + ${cov_json} = Execute Javascript + ... return window.__coverage__ && JSON.stringify(window.__coverage__, null, 2) + IF ${cov_json} + Create File ${ROBOCOV}${/}${name}.json ${cov_json} + END + +Reset JupyterLab And Close With Coverage + [Documentation] Close JupyterLab after gathering coverage. + Capture Page Coverage + Reset JupyterLab And Close + +Prepare Menu Test + Set Attempt Screenshot Directory menus + Execute JupyterLab Command Close All Tabs + Make a Font Test Notebook + +Use the Menu to configure Font + [Documentation] Set a font value in the JupyterLab Fonts Menu + [Arguments] ${kind} ${aspect} ${setting} + Wait Until Keyword Succeeds 5x 0.25s + ... Open With JupyterLab Menu Settings Fonts ${kind} ${aspect} ${setting} + Capture Page Screenshot ${kind}-${aspect}-${setting}.png + Capture Page Coverage + +Set Attempt Screenshot Directory + [Documentation] Set a screenshot directory that includes the attempt + [Arguments] ${path} + Set Screenshot Directory + ... ${OUTPUT_DIR}${/}screenshots${/}${OS.lower()[:2]}_${ATTEMPT}${/}${path} + +Set Cell Metadata + [Documentation] Use the Property Inspector to update the cell metadata + [Arguments] ${metadata} ${idx}=1 ${screenshot}=${EMPTY} + Maybe Open JupyterLab Sidebar Property Inspector + Maybe Open Cell Metadata JSON + ${sel} = Set Variable css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx}) + Click Element css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx}) + Set CodeMirror Value ${CSS_LAB_CELL_META_TOOL} .CodeMirror ${metadata} + ${commit} = Set Variable css:${CSS_LAB_CELL_META_TOOL} ${CSS_LAB_META_COMMIT} + Wait Until Page Contains Element ${commit} + Click Element ${commit} + IF ${screenshot.__len__()} Capture Page Screenshot ${screenshot} + +Maybe Open Cell Metadata JSON + [Documentation] Ensure the Cell Metadata viewer is open. + ${el} = Get WebElements css:${CSS_LAB_CELL_META_JSON_CM_HIDDEN} + + IF not ${el.__len__()} RETURN + + Click Element css:${CSS_LAB_ADVANCED_COLLAPSE} + Wait Until Page Does Not Contain Element css:${CSS_LAB_CELL_META_JSON_CM_HIDDEN} + +Stylesheet Should Contain + [Documentation] Check whether style "stuck", + [Arguments] ${text} ${mod}=${MOD_NOTEBOOK} ${inverse}=${FALSE} + ${css} = Get Element Attribute css:${SHEET}${mod} innerHTML + IF ${inverse} + Should Not Contain ${css} ${text} + ELSE + Should Contain ${css} ${text} + END + +Stylesheet Should Not Contain + [Documentation] Check whether style "stuck", + [Arguments] ${text} ${mod}=${MOD_NOTEBOOK} + Stylesheet Should Contain ${text} ${mod} inverse=${TRUE} diff --git a/atest/_variables.resource b/atest/_variables.resource new file mode 100644 index 00000000..f666d824 --- /dev/null +++ b/atest/_variables.resource @@ -0,0 +1,29 @@ +*** Settings *** +Documentation Variables for testing jupyterlab-fonts + + +*** Variables *** +${ED} css:.jp-FontsEditor +${DOCK} //div[@id='jp-main-dock-panel'] +${TAB} li[contains(@class, 'lm-TabBar-tab')] +${ICON_FONT} *[@data-icon = 'fonts:fonts'] +${ICON_LICENSE} *[@data-icon = 'fonts:license'] +${ICON_SETTINGS} *[@data-icon = 'ui-components:settings'] +${ICON_NOTEBOOK} *[@data-icon = 'ui-components:notebook'] +${ICON_CLOSE} div[contains(@class, 'lm-TabBar-tabCloseIcon')] +${BUTTON} .jp-FontsEditor-button +${SETTING_ITEM} //div[contains(@class, 'jp-PluginList')]//div +${SETTINGS_RAW_CM} .jp-SettingsRawEditor-user .CodeMirror +${SHEET} .jp-Fonts-Sheet +${MOD_GLOBAL} .jp-fonts-mod-global +${MOD_NOTEBOOK} .jp-fonts-mod-notebook +${DATA_TAGS} data-jpf-cell-tags +${DATA_CELL_ID} data-jpf-cell-id +# lab +${CSS_LAB_MOD_HIDDEN} .lm-mod-hidden +${CSS_LAB_CELL_META_JSON_CM} .jp-MetadataEditorTool .CodeMirror +${CSS_LAB_CELL_META_JSON_CM_HIDDEN} ${CSS_LAB_MOD_HIDDEN} ${CSS_LAB_CELL_META_JSON_CM} +${CSS_LAB_ADVANCED_COLLAPSE} .jp-NotebookTools .jp-Collapse-header +${CSS_LAB_CELL_META_TOOL} .jp-RankedPanel .jp-MetadataEditorTool:nth-child(1) +${CSS_LAB_NOTEBOOK_META_TOOL} .jp-RankedPanel .jp-MetadataEditorTool:nth-child(2) +${CSS_LAB_META_COMMIT} [data-icon="ui-components:check"] diff --git a/atest/fixtures/jupyter_config.json b/atest/fixtures/jupyter_config.json new file mode 100644 index 00000000..b2ef6be8 --- /dev/null +++ b/atest/fixtures/jupyter_config.json @@ -0,0 +1,14 @@ +{ + "LabApp": { + "log_level": "DEBUG", + "open_browser": false + }, + "ServerApp": { + "tornado_settings": { + "page_config_data": { + "buildCheck": false, + "buildAvailable": false + } + } + } +} diff --git a/dodo.py b/dodo.py index 5afe52a3..e1971f02 100644 --- a/dodo.py +++ b/dodo.py @@ -2,6 +2,7 @@ import hashlib import json import os +import platform import re import shutil import sys @@ -28,6 +29,13 @@ class C: ATEST_ARGS = json.loads(os.environ.get("ATEST_ARGS", "[]")) WITH_JS_COV = bool(json.loads(os.environ.get("WITH_JS_COV", "0"))) NYC = [*JLPM, "nyc", "report"] + PABOT_DEFAULTS = [ + "--artifactsinsubfolders", + "--artifacts", + "png,log,txt,svg,ipynb,json", + ] + PLATFORM = platform.system() + PY_VERSION = "{}.{}".format(sys.version_info[0], sys.version_info[1]) class P: @@ -62,7 +70,7 @@ class P: YARN_LOCK = ROOT / "yarn.lock" ESLINTRC = ROOT / ".eslintrc.js" - ALL_ROBOT = [*ATEST.rglob("*.robot")] + ALL_ROBOT = [*ATEST.rglob("*.robot"), *ATEST.rglob("*.resource")] ALL_SCHEMA = [*PACKAGES.glob("*/schema/*.json")] ALL_YAML = [*BINDER.glob("*.yml"), *GH.rglob("*.yml")] @@ -237,7 +245,7 @@ def task_build(): ext_pkg_jsons = [] if C.WITH_JS_COV: - file_dep = [P.YARN_INTEGRITY] + file_dep = [P.YARN_INTEGRITY, *B.ALL_CORE_SCHEMA] else: file_dep = [B.META_BUILDINFO] @@ -249,7 +257,9 @@ def task_build(): ext_pkg_jsons += [ext_pkg_json] scope_args = [*C.LERNA, "run", "--scope", name] if C.WITH_JS_COV: - actions = [[*scope_args, "labextension:build:cov"]] + actions = [ + [*scope_args, "labextension:build:cov"], + ] else: actions = [[*scope_args, "labextension:build"]] yield dict( @@ -391,9 +401,14 @@ def task_test(): (doit.tools.create_folder, [B.ATEST_OUT]), doit.action.CmdAction( [ - *C.PYM, - "robot", + "pabot", + *C.PABOT_DEFAULTS, + *(["--name", "🇦"]), + *(["--variable", f"ATTEMPT:{1}"]), + *(["--variable", f"OS:{C.PLATFORM}"]), + *(["--variable", f"PY:{C.PY_VERSION}"]), *(["--variable", f"ROBOCOV:{B.ROBOCOV}"]), + *(["--variable", f"ROOT:{P.ROOT}"]), *C.ATEST_ARGS, P.ATEST, ], @@ -421,6 +436,7 @@ def task_test(): yield dict( name="pytest", + task_dep=["setup:ext"], file_dep=[*P.ALL_PY_SRC], actions=[ [ diff --git a/packages/_meta/package.json b/packages/_meta/package.json index 973febc8..9fe85f6d 100644 --- a/packages/_meta/package.json +++ b/packages/_meta/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/meta-jupyterlab-fonts", - "version": "2.1.0", + "version": "2.1.1", "private": true, "scripts": { "build": "tsc -b", diff --git a/packages/jupyterlab-font-anonymous-pro/package.json b/packages/jupyterlab-font-anonymous-pro/package.json index 63f26854..e4b8e53a 100644 --- a/packages/jupyterlab-font-anonymous-pro/package.json +++ b/packages/jupyterlab-font-anonymous-pro/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/jupyterlab-font-anonymous-pro", - "version": "2.1.0", + "version": "2.1.1", "description": "Anonymous Pro Fonts for JupyterLab", "keywords": [ "fonts", @@ -31,7 +31,7 @@ "watch": "jupyter labextension watch --debug ." }, "dependencies": { - "@deathbeds/jupyterlab-fonts": "~2.1", + "@deathbeds/jupyterlab-fonts": "~2.1.1", "@jupyterlab/application": "3", "typeface-anonymous-pro": "^1.1.13" }, diff --git a/packages/jupyterlab-font-dejavu-sans-mono/package.json b/packages/jupyterlab-font-dejavu-sans-mono/package.json index 18389ee0..d018b88a 100644 --- a/packages/jupyterlab-font-dejavu-sans-mono/package.json +++ b/packages/jupyterlab-font-dejavu-sans-mono/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/jupyterlab-font-dejavu-sans-mono", - "version": "2.1.0", + "version": "2.1.1", "description": "Dejavu Sans Mono Fonts for JupyterLab", "keywords": [ "fonts", @@ -32,7 +32,7 @@ "watch": "jupyter labextension watch --debug ." }, "dependencies": { - "@deathbeds/jupyterlab-fonts": "~2.1", + "@deathbeds/jupyterlab-fonts": "~2.1.1", "@jupyterlab/application": "3" }, "devDependencies": { diff --git a/packages/jupyterlab-font-fira-code/package.json b/packages/jupyterlab-font-fira-code/package.json index b626aaa0..cb7f87b3 100644 --- a/packages/jupyterlab-font-fira-code/package.json +++ b/packages/jupyterlab-font-fira-code/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/jupyterlab-font-fira-code", - "version": "2.1.0", + "version": "2.1.1", "description": "Fira Code Fonts for JupyterLab", "keywords": [ "fonts", @@ -30,7 +30,7 @@ "watch": "jupyter labextension watch --debug ." }, "dependencies": { - "@deathbeds/jupyterlab-fonts": "~2.1", + "@deathbeds/jupyterlab-fonts": "~2.1.1", "@jupyterlab/application": "3", "firacode": "^6.2.0" }, diff --git a/packages/jupyterlab-fonts/package.json b/packages/jupyterlab-fonts/package.json index aeef4e15..6f16ff78 100644 --- a/packages/jupyterlab-fonts/package.json +++ b/packages/jupyterlab-fonts/package.json @@ -1,6 +1,6 @@ { "name": "@deathbeds/jupyterlab-fonts", - "version": "2.1.0", + "version": "2.1.1", "description": "Interactive Typography and Style for JupyterLab", "keywords": [ "fonts", diff --git a/packages/jupyterlab-fonts/src/button.ts b/packages/jupyterlab-fonts/src/button.ts index 89b8e7c7..a8e2a591 100644 --- a/packages/jupyterlab-fonts/src/button.ts +++ b/packages/jupyterlab-fonts/src/button.ts @@ -6,8 +6,7 @@ import { IDisposable, DisposableDelegate } from '@lumino/disposable'; import { ISignal, Signal } from '@lumino/signaling'; import { ICONS } from './icons'; - -import { PACKAGE_NAME, CONFIGURED_CLASS } from '.'; +import { PACKAGE_NAME, CONFIGURED_CLASS } from './tokens'; /** * A notebook widget extension that adds a button to the toolbar. @@ -40,14 +39,19 @@ export class NotebookFontsButton } }; - if (panel.model) { - panel.model.metadata.changed.connect(metaUpdated); - metaUpdated(panel.model.metadata); + const panelModel = panel.model; + + if (panelModel) { + panelModel.metadata.changed.connect(metaUpdated); + metaUpdated(panelModel.metadata); } panel.toolbar.insertItem(9, 'fonts', button); return new DisposableDelegate(() => { + if (panelModel) { + panelModel.metadata.changed.disconnect(metaUpdated); + } button.dispose(); }); } diff --git a/packages/jupyterlab-fonts/src/editor.ts b/packages/jupyterlab-fonts/src/editor.ts index 0da44b5d..31436066 100644 --- a/packages/jupyterlab-fonts/src/editor.ts +++ b/packages/jupyterlab-fonts/src/editor.ts @@ -5,7 +5,6 @@ import * as React from 'react'; import { FontManager } from './manager'; import * as SCHEMA from './schema'; - import { TextKind, TEXT_OPTIONS, @@ -14,7 +13,7 @@ import { TextProperty, IFontFaceOptions, PACKAGE_NAME, -} from '.'; +} from './tokens'; import '../style/editor.css'; diff --git a/packages/jupyterlab-fonts/src/index.ts b/packages/jupyterlab-fonts/src/index.ts index 4a210e9f..4dfdb792 100644 --- a/packages/jupyterlab-fonts/src/index.ts +++ b/packages/jupyterlab-fonts/src/index.ts @@ -1,174 +1,8 @@ -import { ICommandPalette } from '@jupyterlab/apputils'; -import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; -import { CommandRegistry } from '@lumino/commands'; -import { Token } from '@lumino/coreutils'; -import { ISignal } from '@lumino/signaling'; -import { Menu } from '@lumino/widgets'; - -import * as SCHEMA from './schema'; - -import '../style/index.css'; - -export type Scope = 'global' | 'notebook'; - -export enum TextKind { - code = 'code', - content = 'content', -} - -export const KIND_LABELS: { [key in TextKind]: string } = { - code: 'Code', - content: 'Content', -}; - -export enum FontFormat { - woff2 = 'woff2', - woff = 'woff', -} - -export type TFontMimeTypes = { [key in FontFormat]: string }; - -export const FONT_FORMATS = { - woff2: 'font/woff2', - woff: 'font/woff', -}; - -export type TextProperty = 'font-family' | 'font-size' | 'line-height'; - -export interface IFontCallback { - (): Promise; -} - -export interface IFontLicense { - name: string; - spdx: string; - text: () => Promise; - holders: string[]; -} - -export interface IFontFaceOptions { - name: string; - faces: IFontCallback; - license: IFontLicense; -} - -export const CMD = { - code: { - fontSize: 'code-font-size', - fontFamily: 'code-font-family', - lineHeight: 'code-line-height', - }, - content: { - fontSize: 'content-font-size', - fontFamily: 'content-font-family', - lineHeight: 'content-line-height', - }, - editFonts: 'font-editor:open', - customFonts: { - disable: 'custom-fonts:disable', - enable: 'custom-fonts:enable', - }, -}; - -export const ROOT = ':root'; - -export type ICSSVars = { - [key in TextKind]: { [key in TextProperty]: SCHEMA.ICSSOM }; -}; - -export const CSS: ICSSVars = { - code: { - 'font-family': '--jp-code-font-family', - 'font-size': '--jp-code-font-size', - 'line-height': '--jp-code-line-height', - }, - content: { - 'font-family': '--jp-content-font-family', - 'font-size': '--jp-content-font-size1', - 'line-height': '--jp-content-line-height', - }, -}; - -export namespace DOM { - export const sheet = 'jp-Fonts-Sheet'; - export const modGlobal = 'jp-fonts-mod-global'; - export const modNotebook = 'jp-fonts-mod-notebook'; - export const notebookPanel = 'jp-NotebookPanel'; - export const cell = 'jp-Cell'; -} - -export type ICSSTextOptions = { - [key in TextProperty]: (manager: IFontManager) => SCHEMA.ICSSOM[]; -}; - -export const TEXT_OPTIONS: ICSSTextOptions = { - 'font-size': (_m) => Array.from(Array(25).keys()).map((i) => `${i + 8}px`), - 'line-height': (_m) => Array.from(Array(8).keys()).map((i) => `${i * 0.25 + 1}`), - 'font-family': (m) => { - let names = Array.from(m.fonts.values()).reduce((memo, f) => { - return memo.concat(f.name); - }, [] as string[]); - names.sort((a, b) => a.localeCompare(b)); - return names; - }, -}; - -export type ICSSTextLabels = { [key in TextProperty]: string }; - -export const TEXT_LABELS: ICSSTextLabels = { - 'font-size': 'Size', - 'line-height': 'Line Height', - 'font-family': 'Font', -}; - -export const DEFAULT = { - code: { - fontSize: '13px', - lineHeight: '1', - fontFamily: '"Source Code Pro", monospace', - }, -}; - -export const PACKAGE_NAME: string = '@deathbeds/jupyterlab-fonts'; -export const CONFIGURED_CLASS = 'jp-fonts-configured'; - -export const IFontManager = new Token( - '@deathbeds/jupyterlab-fonts:IFontManager' -); - -export interface IFontManagerConstructor { - new ( - commands: CommandRegistry, - palette: ICommandPalette, - notebooks: INotebookTracker - ): IFontManager; -} - -export interface IFontManager { - ready: Promise; - registerFontFace(options: IFontFaceOptions): void; - licensePaneRequested: ISignal; - requestLicensePane(font: any): void; - fonts: Map; - stylesheets: HTMLStyleElement[]; - menu: Menu; - getVarName(property: TextProperty, options: ITextStyleOptions): SCHEMA.ICSSOM | null; - getTextStyle( - property: TextProperty, - options: ITextStyleOptions - ): SCHEMA.ICSSOM | null; - setTextStyle( - property: TextProperty, - value: SCHEMA.ICSSOM | null, - options: ITextStyleOptions - ): void; - dataURISrc(url: string, format: FontFormat): Promise; - setTransientNotebookStyle(panel: NotebookPanel, style: SCHEMA.ISettings | null): void; - getTransientNotebookStyle(panel: NotebookPanel): SCHEMA.ISettings | null; -} - -export interface ITextStyleOptions { - kind: TextKind; - scope?: Scope; - notebook?: NotebookPanel; -} +export * from './button'; +export * from './license'; +export * from './manager'; +export * from './plugin'; +export * from './schema'; +export * from './stylist'; +export * from './tokens'; +export * from './util'; diff --git a/packages/jupyterlab-fonts/src/license.tsx b/packages/jupyterlab-fonts/src/license.tsx index 9d577393..9a0bfd00 100644 --- a/packages/jupyterlab-fonts/src/license.tsx +++ b/packages/jupyterlab-fonts/src/license.tsx @@ -1,7 +1,7 @@ import { VDomModel, VDomRenderer } from '@jupyterlab/apputils'; import * as React from 'react'; -import { IFontFaceOptions } from '.'; +import { IFontFaceOptions } from './tokens'; import '../style/license.css'; diff --git a/packages/jupyterlab-fonts/src/manager.ts b/packages/jupyterlab-fonts/src/manager.ts index f95aaa22..130fbbda 100644 --- a/packages/jupyterlab-fonts/src/manager.ts +++ b/packages/jupyterlab-fonts/src/manager.ts @@ -8,8 +8,6 @@ import { Menu } from '@lumino/widgets'; import * as SCHEMA from './schema'; import { Stylist } from './stylist'; -import { dataURISrc } from './util'; - import { IFontManager, PACKAGE_NAME, @@ -23,7 +21,8 @@ import { TEXT_OPTIONS, FontFormat, IFontFaceOptions, -} from '.'; +} from './tokens'; +import { dataURISrc } from './util'; const ALL_PALETTE = 'Fonts'; @@ -277,25 +276,25 @@ export class FontManager implements IFontManager { }); } - private _registerNotebook(notebook: NotebookPanel) { - this._stylist.registerNotebook(notebook, true); - let watcher = this._notebookMetaWatcher(notebook); - if (notebook?.model) { - notebook.model.metadata.changed.connect(watcher); + private _registerNotebook(panel: NotebookPanel) { + this._stylist.registerNotebook(panel, true); + let watcher = this._notebookMetaWatcher(panel); + if (panel?.model) { + panel.model.metadata.changed.connect(watcher); } - notebook.disposed.connect(this._onNotebookDisposed); + panel.disposed.connect(this._onNotebookDisposed, this); watcher(); this.hack(); } - private _onNotebookDisposed(notebook: NotebookPanel) { - this._stylist.registerNotebook(notebook, false); + private _onNotebookDisposed(panel: NotebookPanel) { + this._stylist.registerNotebook(panel, false); } - private _notebookMetaWatcher(_notebook: NotebookPanel) { + private _notebookMetaWatcher(panel: NotebookPanel) { return () => { this._notebooks.forEach((notebook) => { - if (notebook.id !== notebook.id || !notebook.model) { + if (notebook.id !== panel.id || !notebook.model) { return; } const meta = notebook.model.metadata.get(PACKAGE_NAME) as SCHEMA.ISettings; diff --git a/packages/jupyterlab-fonts/src/plugin.ts b/packages/jupyterlab-fonts/src/plugin.ts index 325b3493..9492001b 100644 --- a/packages/jupyterlab-fonts/src/plugin.ts +++ b/packages/jupyterlab-fonts/src/plugin.ts @@ -9,8 +9,9 @@ import { FontEditor } from './editor'; import { ICONS } from './icons'; import { LicenseViewer } from './license'; import { FontManager } from './manager'; +import { IFontManager, PACKAGE_NAME, CMD, IFontFaceOptions } from './tokens'; -import { IFontManager, PACKAGE_NAME, CMD, IFontFaceOptions } from '.'; +import '../style/index.css'; const PLUGIN_ID = `${PACKAGE_NAME}:fonts`; diff --git a/packages/jupyterlab-fonts/src/stylist.ts b/packages/jupyterlab-fonts/src/stylist.ts index 63585493..0cb3a3a5 100644 --- a/packages/jupyterlab-fonts/src/stylist.ts +++ b/packages/jupyterlab-fonts/src/stylist.ts @@ -1,13 +1,17 @@ import { Cell, ICellModel } from '@jupyterlab/cells'; +import { PathExt, PageConfig, URLExt } from '@jupyterlab/coreutils'; import { Notebook, NotebookPanel } from '@jupyterlab/notebook'; import { JSONExt } from '@lumino/coreutils'; +import { Debouncer } from '@lumino/polling'; import { Signal } from '@lumino/signaling'; import * as JSS from 'jss'; import jssPresetDefault from 'jss-preset-default'; import * as SCHEMA from './schema'; +import { ROOT, IFontFaceOptions, DOM, PACKAGE_NAME } from './tokens'; -import { ROOT, IFontFaceOptions, DOM, PACKAGE_NAME } from '.'; +const RE_CSS_IMPORT = /^@import(.*$)/; +const RE_CSS_REL_URL = /url\(\s*['"]?(\.[^\)'"]+)['"]?\s*\)/g; export class Stylist { fonts = new Map(); @@ -18,11 +22,17 @@ export class Stylist { private _jss = JSS.create(jssPresetDefault()); private _fontCache = new Map(); private _cacheUpdated = new Signal(this); + private _cellStyleCache = new Map(); + private _notebookContentDebouncer: Debouncer; + private _notebookCellCount = new Map(); constructor() { this._globalStyles = document.createElement('style'); this._globalStyles.classList.add(DOM.sheet); this._globalStyles.classList.add(DOM.modGlobal); + this._notebookContentDebouncer = new Debouncer((notebook: Notebook) => { + this._onNotebookModelContentChanged(notebook); + }, 100); } get cacheUpdated() { return this._cacheUpdated; @@ -35,7 +45,7 @@ export class Stylist { sheet.classList.add(DOM.sheet); sheet.classList.add(DOM.modNotebook); panel.content.modelContentChanged.connect( - this._onNotebookModelContentChanged, + this._debouncedNotebookContentChanged, this ); panel.disposed.connect(this._onDisposed, this); @@ -46,8 +56,16 @@ export class Stylist { } } + private _debouncedNotebookContentChanged(notebook: Notebook) { + this._notebookContentDebouncer.invoke(notebook).catch(console.warn); + } + /** hoist cell metadata to data attributes */ private _onNotebookModelContentChanged(notebook: Notebook) { + const newCellCount = notebook.widgets.length; + const oldCellCount = this._notebookCellCount.get(notebook) || -1; + + let needsUpdate = newCellCount !== oldCellCount; for (const cell of notebook.widgets) { cell.node.dataset.jpfCellId = cell.model.id; let tags = [...((cell.model.metadata.get('tags') || []) as string[])].join(','); @@ -56,6 +74,23 @@ export class Stylist { } else { delete cell.node.dataset.jpfCellTags; } + + if (!needsUpdate) { + const meta = cell.model.metadata.get(PACKAGE_NAME) || JSONExt.emptyObject; + let cached = this._cellStyleCache.get(cell.model.id) || JSONExt.emptyObject; + if (!JSONExt.deepEqual(meta, cached)) { + needsUpdate = true; + } + this._cellStyleCache.set(cell.model.id, meta); + } + } + + if (needsUpdate) { + this.stylesheet( + notebook.model?.metadata.get(PACKAGE_NAME) as SCHEMA.ISettings, + notebook.parent as NotebookPanel + ); + this._notebookCellCount.set(notebook, newCellCount); } } @@ -65,10 +100,13 @@ export class Stylist { this._notebookStyles.delete(panel); panel.disposed.disconnect(this._onDisposed, this); panel.content.modelContentChanged.disconnect( - this._onNotebookModelContentChanged, + this._debouncedNotebookContentChanged, this ); } + if (this._notebookCellCount.has(panel.content)) { + this._notebookCellCount.delete(panel.content); + } } get stylesheets() { @@ -114,7 +152,7 @@ export class Stylist { if (transientMeta) { style = this._nbMetaToStyle(transientMeta, panel); jss = this._jss.createStyleSheet(style as any); - css = `${css}\n\n${jss.toString()}`; + css = `${css.trim()}\n${jss.toString()}`; } for (const cell of panel.content.widgets) { let cellMeta = @@ -122,10 +160,12 @@ export class Stylist { JSONExt.emptyObject; style = this._nbMetaToStyle(cellMeta, panel, cell); jss = this._jss.createStyleSheet(style as any); - css = `${css}\n\n${jss.toString()}`; + css = `${css.trim()}\n${jss.toString()}`; } } + css = this._normalizeCSS(css, panel); + if (sheet && sheet.textContent !== css) { sheet.textContent = css; } @@ -133,6 +173,34 @@ export class Stylist { this.hack(); } + private _normalizeCSS(css: string, panel?: NotebookPanel) { + const lines = css.split('\n'); + const finalLines: string[] = []; + const imports: string[] = []; + let localPath = panel?.context.localPath || null; + if (localPath) { + localPath = URLExt.join( + PageConfig.getBaseUrl(), + 'files', + PathExt.dirname(localPath) + ); + } + let line: string; + for (line of lines) { + if (localPath != null) { + line = line.replace(RE_CSS_REL_URL, `url('${localPath}/$1')`); + } + + let importMatch = line.match(RE_CSS_IMPORT); + if (importMatch) { + imports.push(line); + } else { + finalLines.push(line); + } + } + return [...imports, ...finalLines].join('\n'); + } + private _nbMetaToStyle( meta: SCHEMA.ISettings, panel: NotebookPanel, diff --git a/packages/jupyterlab-fonts/src/tokens.ts b/packages/jupyterlab-fonts/src/tokens.ts new file mode 100644 index 00000000..eed3cb65 --- /dev/null +++ b/packages/jupyterlab-fonts/src/tokens.ts @@ -0,0 +1,172 @@ +import { ICommandPalette } from '@jupyterlab/apputils'; +import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; +import { CommandRegistry } from '@lumino/commands'; +import { Token } from '@lumino/coreutils'; +import { ISignal } from '@lumino/signaling'; +import { Menu } from '@lumino/widgets'; + +import * as SCHEMA from './schema'; + +export type Scope = 'global' | 'notebook'; + +export enum TextKind { + code = 'code', + content = 'content', +} + +export const KIND_LABELS: { [key in TextKind]: string } = { + code: 'Code', + content: 'Content', +}; + +export enum FontFormat { + woff2 = 'woff2', + woff = 'woff', +} + +export type TFontMimeTypes = { [key in FontFormat]: string }; + +export const FONT_FORMATS = { + woff2: 'font/woff2', + woff: 'font/woff', +}; + +export type TextProperty = 'font-family' | 'font-size' | 'line-height'; + +export interface IFontCallback { + (): Promise; +} + +export interface IFontLicense { + name: string; + spdx: string; + text: () => Promise; + holders: string[]; +} + +export interface IFontFaceOptions { + name: string; + faces: IFontCallback; + license: IFontLicense; +} + +export const CMD = { + code: { + fontSize: 'code-font-size', + fontFamily: 'code-font-family', + lineHeight: 'code-line-height', + }, + content: { + fontSize: 'content-font-size', + fontFamily: 'content-font-family', + lineHeight: 'content-line-height', + }, + editFonts: 'font-editor:open', + customFonts: { + disable: 'custom-fonts:disable', + enable: 'custom-fonts:enable', + }, +}; + +export const ROOT = ':root'; + +export type ICSSVars = { + [key in TextKind]: { [key in TextProperty]: SCHEMA.ICSSOM }; +}; + +export const CSS: ICSSVars = { + code: { + 'font-family': '--jp-code-font-family', + 'font-size': '--jp-code-font-size', + 'line-height': '--jp-code-line-height', + }, + content: { + 'font-family': '--jp-content-font-family', + 'font-size': '--jp-content-font-size1', + 'line-height': '--jp-content-line-height', + }, +}; + +export namespace DOM { + export const sheet = 'jp-Fonts-Sheet'; + export const modGlobal = 'jp-fonts-mod-global'; + export const modNotebook = 'jp-fonts-mod-notebook'; + export const notebookPanel = 'jp-NotebookPanel'; + export const cell = 'jp-Cell'; +} + +export type ICSSTextOptions = { + [key in TextProperty]: (manager: IFontManager) => SCHEMA.ICSSOM[]; +}; + +export const TEXT_OPTIONS: ICSSTextOptions = { + 'font-size': (_m) => Array.from(Array(25).keys()).map((i) => `${i + 8}px`), + 'line-height': (_m) => Array.from(Array(8).keys()).map((i) => `${i * 0.25 + 1}`), + 'font-family': (m) => { + let names = Array.from(m.fonts.values()).reduce((memo, f) => { + return memo.concat(f.name); + }, [] as string[]); + names.sort((a, b) => a.localeCompare(b)); + return names; + }, +}; + +export type ICSSTextLabels = { [key in TextProperty]: string }; + +export const TEXT_LABELS: ICSSTextLabels = { + 'font-size': 'Size', + 'line-height': 'Line Height', + 'font-family': 'Font', +}; + +export const DEFAULT = { + code: { + fontSize: '13px', + lineHeight: '1', + fontFamily: '"Source Code Pro", monospace', + }, +}; + +export const PACKAGE_NAME: string = '@deathbeds/jupyterlab-fonts'; +export const CONFIGURED_CLASS = 'jp-fonts-configured'; + +export const IFontManager = new Token( + '@deathbeds/jupyterlab-fonts:IFontManager' +); + +export interface IFontManagerConstructor { + new ( + commands: CommandRegistry, + palette: ICommandPalette, + notebooks: INotebookTracker + ): IFontManager; +} + +export interface IFontManager { + ready: Promise; + registerFontFace(options: IFontFaceOptions): void; + licensePaneRequested: ISignal; + requestLicensePane(font: any): void; + fonts: Map; + stylesheets: HTMLStyleElement[]; + menu: Menu; + getVarName(property: TextProperty, options: ITextStyleOptions): SCHEMA.ICSSOM | null; + getTextStyle( + property: TextProperty, + options: ITextStyleOptions + ): SCHEMA.ICSSOM | null; + setTextStyle( + property: TextProperty, + value: SCHEMA.ICSSOM | null, + options: ITextStyleOptions + ): void; + dataURISrc(url: string, format: FontFormat): Promise; + setTransientNotebookStyle(panel: NotebookPanel, style: SCHEMA.ISettings | null): void; + getTransientNotebookStyle(panel: NotebookPanel): SCHEMA.ISettings | null; +} + +export interface ITextStyleOptions { + kind: TextKind; + scope?: Scope; + notebook?: NotebookPanel; +} diff --git a/packages/jupyterlab-fonts/src/util.ts b/packages/jupyterlab-fonts/src/util.ts index 91794ad7..9f1a9962 100644 --- a/packages/jupyterlab-fonts/src/util.ts +++ b/packages/jupyterlab-fonts/src/util.ts @@ -1,4 +1,4 @@ -import { FONT_FORMATS, FontFormat } from '.'; +import { FONT_FORMATS, FontFormat } from './tokens'; /* below from https://gist.github.com/viljamis/c4016ff88745a0846b94 */ const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; diff --git a/setup.cfg b/setup.cfg index 910ed398..9a3833a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,6 @@ ignore = E203 [tool:pytest] junit_family=xunit2 -script_launch_mode = subprocess addopts = -vv --tb long