Fixes for Enso Font in GUI2 (#8508)

- Fixes issue reported in Discord.

# Important Notes
None
This commit is contained in:
somebody1234 2023-12-14 08:17:12 +10:00 committed by GitHub
parent af50d32553
commit f5c3713f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 767 additions and 241 deletions

1
app/gui2/.gitignore vendored
View File

@ -29,3 +29,4 @@ src/util/iconList.json
src/util/iconName.ts
src/stores/visualization/metadata.json
public/font-*/
src/assets/font-*.css

View File

@ -11,12 +11,6 @@
crossorigin
href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css"
/>
<link
rel="stylesheet"
type="text/css"
crossorigin
href="https://fonts.cdnfonts.com/css/dejavu-sans-mono"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Enso GUI</title>
</head>

View File

@ -1,9 +1,22 @@
/** @file THIS SCRIPT IS PROVIDED ONLY FOR CONVENIENCE.
* The sources of truth are at `build/build/src/project/gui2.rs` and
* `build/build/src/ide/web/fonts.rs`. */
import * as fsSync from 'node:fs'
import * as fs from 'node:fs/promises'
import * as http from 'node:http'
import * as https from 'node:https'
import * as process from 'node:process'
import tar from 'tar'
import bz2 from 'unbzip2-stream'
if (process.env.CI === '1') process.exit(0)
const WARNING_MESSAGE =
'⚠️⚠️⚠️ Please use the buildscript (`./run`) to download fonts instead. ⚠️⚠️⚠️'
let warningMessageAlreadyShown = false
let exitCode = 0
const ENSO_FONT_URL = 'https://github.com/enso-org/font/releases/download/1.0/enso-font-1.0.tar.gz'
const MPLUS1_FONT_URL =
'https://github.com/coz-m/MPLUS_FONTS/raw/71d438c798d063cc6fdae8d2864bc48f2d3d06ad/fonts/ttf/MPLUS1%5Bwght%5D.ttf'
@ -12,11 +25,21 @@ const DEJAVU_SANS_MONO_FONT_URL =
/** @param {string | https.RequestOptions | URL} options
* @param {((res: import('node:http').IncomingMessage) => void) | undefined} [callback] */
function httpsGet(options, callback) {
return https.get(options, (response) => {
function get(options, callback) {
const protocol =
typeof options === 'string' ? new URL(options).protocol : options.protocol ?? 'https:'
/** @type {{ get: typeof http['get'] }} */
let httpModule = https
switch (protocol) {
case 'http:': {
httpModule = http
break
}
}
return httpModule.get(options, (response) => {
const location = response.headers.location
if (location) {
httpsGet(
get(
typeof options === 'string' || options instanceof URL
? location
: { ...options, ...new URL(location) },
@ -28,51 +51,161 @@ function httpsGet(options, callback) {
})
}
console.info('Downloading Enso font...')
await fs.rm('./public/font-enso/', { recursive: true, force: true })
await fs.mkdir('./public/font-enso/', { recursive: true })
await new Promise((resolve, reject) => {
httpsGet(ENSO_FONT_URL, (response) => {
response.pipe(
tar.extract({
cwd: './public/font-enso/',
strip: 1,
filter(path) {
// Reject files starting with `.`.
return !/[\\/][.]/.test(path)
},
}),
)
response.on('end', resolve)
response.on('error', reject)
})
})
console.info('Downloading M PLUS 1 font...')
await fs.rm('./public/font-mplus1/', { recursive: true, force: true })
await fs.mkdir('./public/font-mplus1/', { recursive: true })
await new Promise((resolve, reject) => {
httpsGet(MPLUS1_FONT_URL, (response) => {
response.pipe(fsSync.createWriteStream('./public/font-mplus1/MPLUS1.ttf'))
response.on('end', resolve)
response.on('error', reject)
})
})
console.info('Downloading DejaVu Sans Mono font...')
await fs.rm('./public/font-dejavu/', { recursive: true, force: true })
await fs.mkdir('./public/font-dejavu/', { recursive: true })
await new Promise((resolve, reject) => {
httpsGet(DEJAVU_SANS_MONO_FONT_URL, (response) => {
response.pipe(bz2()).pipe(
tar.extract({
cwd: './public/font-dejavu/',
strip: 2,
filter(path) {
return /[\\/]DejaVuSansMono/.test(path) || !/Oblique[.]ttf$/.test(path)
},
}),
)
response.on('end', resolve)
response.on('error', reject)
})
})
/** @param {unknown} error */
function errorCode(error) {
return typeof error === 'object' &&
error != null &&
'code' in error &&
typeof error.code === 'string'
? error.code
: undefined
}
/** @param {unknown} error */
function isFileNotFoundError(error) {
return errorCode(error) === 'ENOENT'
}
const ENSO_FONT_VARIANTS = [
{ variant: 'Thin', weight: 100 },
{ variant: 'ExtraLight', weight: 200 },
{ variant: 'Light', weight: 300 },
{ variant: 'Regular', weight: 400 },
{ variant: 'Medium', weight: 500 },
{ variant: 'SemiBold', weight: 600 },
{ variant: 'Bold', weight: 700 },
{ variant: 'ExtraBold', weight: 800 },
{ variant: 'Black', weight: 900 },
].map((variant) => ({ font: 'Enso', ...variant }))
const DEJAVU_FONT_VARIANTS = [
{ variant: 'DejaVuSansMono', weight: 400 },
{ variant: 'DejaVuSansMono-Bold', weight: 700 },
].map((variant) => ({ font: 'DejaVu Sans Mono', ...variant }))
try {
await fs.access(`./src/assets/font-enso.css`)
for (const { variant } of ENSO_FONT_VARIANTS) {
await fs.access(`./public/font-enso/Enso-${variant}.ttf`)
}
console.info('Enso font already downloaded, skipping...')
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error('Unexpected error occurred when checking for Enso font:')
console.error(error)
exitCode = 1
} else {
if (!warningMessageAlreadyShown) console.warn(WARNING_MESSAGE)
warningMessageAlreadyShown = true
console.info('Downloading Enso font...')
await fs.rm('./public/font-enso/', { recursive: true, force: true })
await fs.mkdir('./public/font-enso/', { recursive: true })
await new Promise((resolve, reject) => {
get(ENSO_FONT_URL, (response) => {
response.pipe(
tar.extract({
cwd: './public/font-enso/',
strip: 1,
filter(path) {
// Reject files starting with `.`.
return !/[\\/][.]/.test(path)
},
}),
)
response.on('end', resolve)
response.on('error', reject)
})
})
/** @type {string[]} */
let css = []
for (const { font, variant, weight } of ENSO_FONT_VARIANTS) {
css.push(`\
@font-face {
font-family: '${font}';
src: url('/font-enso/Enso-${variant}.ttf');
font-weight: ${weight};
}
`)
}
await fs.writeFile('./src/assets/font-enso.css', css.join('\n'))
}
}
try {
await fs.access(`./src/assets/font-mplus1.css`)
await fs.access(`./public/font-mplus1/MPLUS1[wght].ttf`)
console.info('M PLUS 1 font already downloaded, skipping...')
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error('Unexpected error occurred when checking for M PLUS 1 font:')
console.error(error)
exitCode = 1
} else {
if (!warningMessageAlreadyShown) console.warn(WARNING_MESSAGE)
warningMessageAlreadyShown = true
console.info('Downloading M PLUS 1 font...')
await fs.rm('./public/font-mplus1/', { recursive: true, force: true })
await fs.mkdir('./public/font-mplus1/', { recursive: true })
await new Promise((resolve, reject) => {
get(MPLUS1_FONT_URL, (response) => {
response.pipe(fsSync.createWriteStream('./public/font-mplus1/MPLUS1[wght].ttf'))
response.on('end', resolve)
response.on('error', reject)
})
})
const css = `\
@font-face {
font-family: 'M PLUS 1';
src: url('/font-mplus1/MPLUS1[wght].ttf');
}
`
await fs.writeFile('./src/assets/font-mplus1.css', css)
}
}
try {
await fs.access(`./src/assets/font-dejavu.css`)
for (const variant of ['', '-Bold']) {
await fs.access(`./public/font-dejavu/DejaVuSansMono${variant}.ttf`)
}
console.info('DejaVu Sans Mono font already downloaded, skipping...')
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error('Unexpected error occurred when checking for DejaVu Sans Mono font:')
console.error(error)
exitCode = 1
} else {
if (!warningMessageAlreadyShown) console.warn(WARNING_MESSAGE)
warningMessageAlreadyShown = true
console.info('Downloading DejaVu Sans Mono font...')
await fs.rm('./public/font-dejavu/', { recursive: true, force: true })
await fs.mkdir('./public/font-dejavu/', { recursive: true })
await new Promise((resolve, reject) => {
get(DEJAVU_SANS_MONO_FONT_URL, (response) => {
response.pipe(bz2()).pipe(
tar.extract({
cwd: './public/font-dejavu/',
strip: 2,
filter(path) {
return /[\\/]DejaVuSansMono/.test(path) && !/Oblique[.]ttf$/.test(path)
},
}),
)
response.on('end', resolve)
response.on('error', reject)
})
})
/** @type {string[]} */
let css = []
for (const { font, variant, weight } of DEJAVU_FONT_VARIANTS) {
css.push(`\
@font-face {
font-family: '${font}';
src: url('/font-dejavu/${variant}.ttf');
font-weight: ${weight};
}
`)
}
await fs.writeFile('./src/assets/font-dejavu.css', css.join('\n'))
}
}
console.info('Done.')
if (exitCode !== 0) process.exit(exitCode)

View File

@ -1,11 +0,0 @@
@font-face {
font-family: 'DejaVu Sans Mono';
src: url('/font-dejavu/DejaVuSansMono.ttf');
font-weight: 400;
}
@font-face {
font-family: 'DejaVu Sans Mono';
src: url('/font-dejavu/DejaVuSansMono-Bold.ttf');
font-weight: 700;
}

View File

@ -1,53 +0,0 @@
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Thin.ttf');
font-weight: 100;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-ExtraLight.ttf');
font-weight: 200;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Light.ttf');
font-weight: 300;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-SemiBold.ttf');
font-weight: 600;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Bold.ttf');
font-weight: 700;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-ExtraBold.ttf');
font-weight: 800;
}
@font-face {
font-family: 'Enso';
src: url('/font-enso/Enso-Black.ttf');
font-weight: 900;
}

View File

@ -1,4 +0,0 @@
@font-face {
font-family: 'M PLUS 1';
src: url('/font-mplus1/MPLUS1.ttf');
}

View File

@ -247,6 +247,10 @@ const editorStyle = computed(() => {
}
}
:deep(.ͼ1 .cm-scroller) {
font-family: var(--font-mono);
}
.resize-handle {
position: absolute;
top: -3px;

View File

@ -167,7 +167,7 @@ function handleBreadcrumbClick(index: number) {
--enso-docs-text-color: rbga(0, 0, 0, 0.6);
--enso-docs-tag-background-color: #dcd8d8;
--enso-docs-code-background-color: #dddcde;
font-family: var(--font-code);
font-family: var(--font-sans);
font-size: 11.5px;
line-height: 160%;
color: var(--enso-docs-text-color);

View File

@ -1,4 +1,4 @@
@import url('../src/assets/font-enso-code.css');
@import url('../src/assets/font-enso.css');
.histoire-story-viewer .__histoire-render-story > [data-v-app] {
height: 100%;

View File

@ -17,6 +17,15 @@
gui/:
gui2/: # The new, Vue-based GUI.
dist/:
public/:
font-enso/:
font-mplus1/:
font-dejavu/:
src/:
assets/:
font-enso.css:
font-mplus1.css:
font-dejavu.css:
ide-desktop/:
lib/:
client/:

View File

@ -25,6 +25,8 @@ use tracing::Span;
// === Export ===
// ==============
pub mod dejavu_font;
pub mod enso_font;
pub mod fonts;
pub mod google_font;

View File

@ -0,0 +1,134 @@
//! Definitions for DejaVu fonts, and functions for downloading and installing them.
use crate::prelude::*;
use enso_font as font;
use enso_font::NonVariableDefinition;
use enso_font::NonVariableFaceHeader;
use ide_ci::cache::Cache;
// =================
// === Constants ===
// =================
pub const PACKAGE_URL: &str = "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.zip";
const FONT_FAMILY: &str = "DejaVu Sans Mono";
const FILE_PREFIX: &str = "DejaVu";
const FILE_SANS_MONO_PREFIX: &str = "SansMono";
const SANS_MONO_FONT_FAMILY_FONTS: &[(&str, font::Weight)] =
&[("-Bold", font::Weight::Bold), ("", font::Weight::Normal)];
// ===================
// === DejaVu Font ===
// ===================
/// Internal helper function to download the DejaVu Sans Mono font. Exposed via thin wrapper
/// functions.
async fn install_sans_mono_internal(
cache: &Cache,
octocrab: &Octocrab,
output_path: impl AsRef<Path>,
css_output_info: Option<(&str, impl AsRef<Path>)>,
) -> Result {
let output_path = output_path.as_ref();
let font = font();
let faces = faces();
let font = crate::ide::web::fonts::filter_font(&font, &faces);
let package = download(cache, octocrab).await?;
let get_font_files = extract_fonts(&font, package, output_path);
let make_css_file = crate::ide::web::fonts::write_css_file_if_required(
FONT_FAMILY,
&font,
&faces,
css_output_info,
);
try_join!(get_font_files, make_css_file)?;
Ok(())
}
/// Install DejaVu Sans Mono, without an auto-generated CSS file.
pub async fn install_sans_mono(
cache: &Cache,
octocrab: &Octocrab,
output_path: impl AsRef<Path>,
) -> Result {
install_sans_mono_internal(cache, octocrab, output_path, None::<(&str, &str)>).await
}
/// Install DejaVu Sans Mono, including an auto-generated CSS file.
pub async fn install_sans_mono_with_css(
cache: &Cache,
octocrab: &Octocrab,
css_basepath: &str,
output_path: impl AsRef<Path>,
css_output_path: impl AsRef<Path>,
) -> Result {
install_sans_mono_internal(cache, octocrab, output_path, Some((css_basepath, css_output_path)))
.await
}
/// The DejaVu Sans Mono Font.
pub fn font() -> NonVariableDefinition {
SANS_MONO_FONT_FAMILY_FONTS
.iter()
.map(|(name, weight)| {
let file = format!("{FILE_PREFIX}{FILE_SANS_MONO_PREFIX}{name}.ttf");
let header = NonVariableFaceHeader {
weight: *weight,
width: font::Width::Normal,
style: font::Style::Normal,
};
(header, file)
})
.collect()
}
/// All font faces contained in this font.
pub fn faces() -> [NonVariableFaceHeader; 2] {
[NonVariableFaceHeader { weight: font::Weight::Normal, ..default() }, NonVariableFaceHeader {
weight: font::Weight::Bold,
..default()
}]
}
/// Extract the fonts from the given archive file, and write them in the given directory.
pub async fn extract_fonts(
fonts: &NonVariableDefinition,
package: impl AsRef<Path>,
out_dir: impl AsRef<Path>,
) -> Result {
let mut archive = ide_ci::archive::zip::open(&package)?;
crate::ide::web::fonts::extract_fonts(&mut archive, fonts, package, out_dir, &mut |path| {
let mut iter = path.iter();
for _ in iter.by_ref().take(2) {}
Box::from(iter.as_str())
})
.await
}
/// Download the DejaVu Font package, with caching and GitHub authentication.
pub async fn download(cache: &Cache, octocrab: &Octocrab) -> Result<Box<Path>> {
Ok(cache
.get(ide_ci::cache::download::DownloadFile {
client: octocrab.client.clone(),
key: ide_ci::cache::download::Key {
url: PACKAGE_URL.parse().unwrap(),
additional_headers: reqwest::header::HeaderMap::from_iter([(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static(
mime::APPLICATION_OCTET_STREAM.as_ref(),
),
)]),
},
})
.await?
.into_boxed_path())
}

View File

@ -0,0 +1,113 @@
//! Definitions for Enso Font, and functions for downloading and installing them.
use crate::prelude::*;
use enso_enso_font::ttf;
use enso_font::NonVariableFaceHeader;
use ide_ci::cache::Cache;
// =================
// === Constants ===
// =================
const FONT_FAMILY: &str = "Enso";
// =================
// === Enso Font ===
// =================
pub async fn install_for_html(
cache: &Cache,
octocrab: &Octocrab,
output_path: impl AsRef<Path>,
) -> Result {
let output_path = output_path.as_ref();
let html_fonts: HashMap<_, _> = [
(NonVariableFaceHeader { weight: ttf::Weight::Normal, ..default() }, "Regular"),
(NonVariableFaceHeader { weight: ttf::Weight::ExtraBold, ..default() }, "Bold"),
]
.into_iter()
.collect();
let html_font_definitions = enso_enso_font::font()
.variations()
.filter(|v| html_fonts.contains_key(&v.header))
.collect();
let get_font_files = async {
let package = download(cache, octocrab).await?;
enso_enso_font::extract_fonts(&html_font_definitions, package, output_path).await
};
let make_css_file = async {
let mut css = String::new();
let url = ".";
for (header, variant) in html_fonts {
use std::fmt::Write;
let def = html_font_definitions.get(header);
let def = def.ok_or_else(|| {
anyhow!(
"Required font not found in Enso Font package. \
Expected a font matching: {header:?}."
)
})?;
let file = &def.file;
// Note that this cannot use `generate_css_file`, as it specifies a different font
// family for each variant.
writeln!(&mut css, "@font-face {{")?;
writeln!(&mut css, " font-family: '{FONT_FAMILY}{variant}';")?;
writeln!(&mut css, " src: url('{url}/{file}');")?;
writeln!(&mut css, " font-weight: normal;")?;
writeln!(&mut css, " font-style: normal;")?;
writeln!(&mut css, "}}")?;
}
let css_path = output_path.join("ensoFont.css");
ide_ci::fs::tokio::write(css_path, css).await?;
Ok(())
};
try_join!(get_font_files, make_css_file)?;
Ok(())
}
pub async fn install_with_css(
cache: &Cache,
octocrab: &Octocrab,
css_basepath: &str,
output_path: impl AsRef<Path>,
css_output_path: impl AsRef<Path>,
) -> Result {
let output_path = output_path.as_ref();
let font = enso_enso_font::font();
let faces = enso_enso_font::faces();
let font = crate::ide::web::fonts::filter_font(&font, &faces);
let package = download(cache, octocrab).await?;
let get_font_files = enso_enso_font::extract_fonts(&font, package, output_path);
let css_output_info = Some((css_basepath, css_output_path));
let make_css_file = crate::ide::web::fonts::write_css_file_if_required(
FONT_FAMILY,
&font,
&faces,
css_output_info,
);
try_join!(get_font_files, make_css_file)?;
Ok(())
}
/// Download the Enso Font package, with caching and GitHub authentication.
pub async fn download(cache: &Cache, octocrab: &Octocrab) -> Result<Box<Path>> {
Ok(cache
.get(ide_ci::cache::download::DownloadFile {
client: octocrab.client.clone(),
key: ide_ci::cache::download::Key {
url: enso_enso_font::PACKAGE_URL.parse().unwrap(),
additional_headers: reqwest::header::HeaderMap::from_iter([(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static(
mime::APPLICATION_OCTET_STREAM.as_ref(),
),
)]),
},
})
.await?
.into_boxed_path())
}

View File

@ -1,9 +1,8 @@
use crate::prelude::*;
use crate::ide::web::google_font;
use enso_enso_font::ttf;
use enso_font::NonVariableDefinition;
use enso_font::NonVariableFaceHeader;
use ide_ci::archive::extract_files::ExtractFiles;
use ide_ci::cache::Cache;
@ -18,87 +17,176 @@ pub async fn install_html_fonts(
output_path: impl AsRef<Path>,
) -> Result {
let output_path = output_path.as_ref();
google_font::download_google_font(cache, octocrab, "mplus1", output_path).await?;
install_enso_font_for_html(cache, octocrab, output_path).await?;
crate::ide::web::google_font::install(cache, octocrab, "mplus1", output_path).await?;
crate::ide::web::enso_font::install_for_html(cache, octocrab, output_path).await?;
Ok(())
}
pub async fn install_enso_font_for_html(
cache: &Cache,
octocrab: &Octocrab,
output_path: impl AsRef<Path>,
) -> Result {
let output_path = output_path.as_ref();
let html_fonts: HashMap<_, _> = [
(NonVariableFaceHeader { weight: ttf::Weight::Normal, ..default() }, "Regular"),
(NonVariableFaceHeader { weight: ttf::Weight::ExtraBold, ..default() }, "Bold"),
]
.into_iter()
.collect();
let html_font_definitions = enso_enso_font::enso_font()
.variations()
.filter(|v| html_fonts.contains_key(&v.header))
.collect();
let get_font_files = async {
let package = get_enso_font_package_(cache, octocrab).await?;
enso_enso_font::extract_fonts(&html_font_definitions, package, output_path).await
};
let make_css_file = async {
let mut css = String::new();
let family = "Enso";
let url = ".";
for (header, variant) in html_fonts {
use std::fmt::Write;
let def = html_font_definitions.get(header);
let def = def.ok_or_else(|| {
anyhow!(
"Required font not found in Enso Font package. \
Expected a font matching: {header:?}."
)
})?;
let file = &def.file;
writeln!(&mut css, "@font-face {{")?;
writeln!(&mut css, " font-family: '{family}{variant}';")?;
writeln!(&mut css, " src: url('{url}/{file}');")?;
writeln!(&mut css, " font-weight: normal;")?;
writeln!(&mut css, " font-style: normal;")?;
writeln!(&mut css, "}}")?;
/// A CSS font style that is displayed as a CSS [`@font-face`] [`font-style`] value.
///
/// [`@font-face`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
/// [`font-style`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-style
#[derive(Debug, Display, Copy, Clone)]
pub enum FontStyle {
#[display(fmt = "normal")]
Normal,
#[display(fmt = "italic")]
Italic,
#[display(fmt = "oblique")]
Oblique,
/// Angle is in degrees, between -90 and 90.
#[display(fmt = "oblique {_0}deg")]
ObliqueWithAngle(f64),
/// Angles are in degrees, between -90 and 90.
#[display(fmt = "oblique {_0}deg {_1}deg")]
ObliqueWithAngleRange(f64, f64),
}
/// A CSS font face that is displayed as a CSS [`@font-face`] declaration.
///
/// [`@font-face`]: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
#[derive(Debug, Clone)]
pub struct FontFace<'a> {
family: Cow<'a, str>,
path: Cow<'a, str>,
weight: Option<u16>,
style: Option<FontStyle>,
}
impl Display for FontFace<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let family = &self.family;
let path = &self.path;
writeln!(f, "@font-face {{")?;
writeln!(f, " font-family: '{family}';")?;
writeln!(f, " src: url('{path}');")?;
if let Some(weight) = self.weight {
writeln!(f, " font-weight: {weight};")?;
}
let css_path = output_path.join("ensoFont.css");
ide_ci::fs::tokio::write(css_path, css).await?;
if let Some(style) = self.style {
writeln!(f, " font-style: {style};")?;
}
writeln!(f, "}}")?;
Ok(())
};
try_join!(get_font_files, make_css_file)?;
}
}
/// Generate a CSS file containing the given font files. Does not include font-weight, so use this
/// only if the font weights are not known - prefer [`generate_css_file`] in all other cases.
pub fn generate_css_file_from_paths<AsRefStr>(
basepath: &str,
family: &str,
paths: impl Iterator<Item = AsRefStr>,
) -> Result<String>
where
AsRefStr: AsRef<str>,
{
let mut css = String::new();
for path in paths {
use std::fmt::Write;
let path = format!("{basepath}/{}", path.as_ref());
let font_face = FontFace {
family: Cow::Borrowed(family),
path: Cow::Borrowed(path.as_str()),
weight: None,
style: None,
};
writeln!(&mut css, "{font_face}")?;
}
Ok(css)
}
/// Generate a CSS file containing the given font family, including only the given font variations.
pub fn generate_css_file<'a>(
basepath: &str,
family: &str,
definitions: &NonVariableDefinition,
fonts: impl Iterator<Item = &'a NonVariableFaceHeader>,
) -> Result<String> {
let mut css = String::new();
for header in fonts {
use std::fmt::Write;
let def = definitions.get(*header);
let def = def.ok_or_else(|| {
anyhow!(
"Required font not found in {family} Font package. \
Expected a font matching: {header:?}."
)
})?;
let path = format!("{basepath}/{}", def.file);
let weight = def.header.weight.to_number();
let font_face = FontFace {
family: Cow::Borrowed(family),
path: Cow::Borrowed(path.as_str()),
weight: Some(weight),
style: None,
};
writeln!(&mut css, "{font_face}")?;
}
Ok(css)
}
// ===================
// === Filter Font ===
// ===================
pub fn filter_font(
font: &NonVariableDefinition,
faces: &[NonVariableFaceHeader],
) -> NonVariableDefinition {
font.variations().filter(|v| faces.contains(&v.header)).collect()
}
// =====================
// === Make CSS File ===
// =====================
pub async fn write_css_file_if_required(
font_family: &str,
font: &NonVariableDefinition,
faces: &[NonVariableFaceHeader],
css_output_info: Option<(&str, impl AsRef<Path>)>,
) -> Result {
if let Some((css_basepath, css_output_path)) = css_output_info {
let contents = generate_css_file(css_basepath, font_family, font, faces.iter())?;
ide_ci::fs::tokio::write(css_output_path, contents).await?;
Ok(())
} else {
Ok(())
}
}
// =====================
// === Extract Fonts ===
// =====================
/// Extract the fonts from the given archive file, and write them in the given directory.
#[context("Failed to extract fonts from archive {}", package.as_ref().display())]
pub async fn extract_fonts(
archive: impl ExtractFiles,
fonts: &NonVariableDefinition,
package: impl AsRef<Path>,
out_dir: impl AsRef<Path>,
normalize_path: &mut impl FnMut(&Path) -> Box<str>,
) -> Result {
ide_ci::fs::tokio::create_dir_if_missing(out_dir.as_ref()).await?;
let mut files_expected: HashSet<_> = fonts.files().collect();
archive
.extract_files(|path_in_archive| {
let stripped_path = normalize_path(path_in_archive);
if files_expected.remove(stripped_path.as_ref()) {
Some(out_dir.as_ref().join(stripped_path.as_ref()))
} else {
None
}
})
.await?;
ensure!(files_expected.is_empty(), "Required fonts not found in archive: {files_expected:?}.");
Ok(())
}
// =================
// === Enso Font ===
// =================
/// Download the Enso Font package, with caching and GitHub authentication.
pub async fn get_enso_font_package() -> Result<Box<Path>> {
let cache = Cache::new_default().await?;
let octocrab = ide_ci::github::setup_octocrab().await?;
get_enso_font_package_(&cache, &octocrab).await
}
async fn get_enso_font_package_(cache: &Cache, octocrab: &Octocrab) -> Result<Box<Path>> {
Ok(cache
.get(ide_ci::cache::download::DownloadFile {
client: octocrab.client.clone(),
key: ide_ci::cache::download::Key {
url: enso_enso_font::PACKAGE_URL.parse().unwrap(),
additional_headers: reqwest::header::HeaderMap::from_iter([(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static(
mime::APPLICATION_OCTET_STREAM.as_ref(),
),
)]),
},
})
.await?
.into_boxed_path())
}

View File

@ -1,4 +1,4 @@
//! Downloading Google Fonts.
//! Functions for downloading and installing Google fonts.
use crate::prelude::*;
@ -15,18 +15,18 @@ use octocrab::models::repos;
// =================
/// Google Fonts repository.
pub const GOOGLE_FONTS_REPOSITORY: RepoRef = RepoRef { owner: "google", name: "fonts" };
pub const REPOSITORY: RepoRef = RepoRef { owner: "google", name: "fonts" };
/// Path to the directory on the Google Fonts repository where we get the fonts from.
///
/// The directory name denotes the license of the fonts. In our case this is SIL OPEN FONT LICENSE
/// Version 1.1, commonly known as OFL.
pub const GOOGLE_FONT_DIRECTORY: &str = "ofl";
pub const DIRECTORY: &str = "ofl";
/// We keep dependency to a fixed commit, so we can safely cache it.
///
/// There are no known reasons not to bump this.
pub const GOOGLE_FONT_SHA1: &str = "ea893a43af7c5ab5ccee189fc2720788d99887ed";
pub const COMMIT_SHA1: &str = "ea893a43af7c5ab5ccee189fc2720788d99887ed";
// ==============
@ -51,7 +51,7 @@ impl Family {
&self,
handle: github::repo::Handle<impl IsRepo>,
) -> Result<Vec<repos::Content>> {
let path = format!("{GOOGLE_FONT_DIRECTORY}/{}", self.name);
let path = format!("{DIRECTORY}/{}", self.name);
let files = handle.repos().get_content().r#ref(&self.r#ref).path(path).send().await?;
Ok(files.items.into_iter().filter(|file| file.name.ends_with(".ttf")).collect())
}
@ -137,17 +137,15 @@ impl Storable for DownloadFont {
// === Entry Point ===
// ===================
pub async fn download_google_font(
/// Install a Google font, without an auto-generated CSS file.
pub async fn install(
cache: &Cache,
octocrab: &Octocrab,
family: &str,
output_path: impl AsRef<Path>,
) -> Result<Vec<PathBuf>> {
let family = Family {
repo: GOOGLE_FONTS_REPOSITORY.into(),
r#ref: GOOGLE_FONT_SHA1.into(),
name: family.into(),
};
let family =
Family { repo: REPOSITORY.into(), r#ref: COMMIT_SHA1.into(), name: family.into() };
let font = DownloadFont { family, octocrab: octocrab.clone() };
let cached_fonts = cache.get(font).await?;
let copy_futures =
@ -156,6 +154,26 @@ pub async fn download_google_font(
Ok(result)
}
/// Install a Google font, including an auto-generated CSS file.
pub async fn install_with_css(
cache: &Cache,
octocrab: &Octocrab,
family: &str,
css_family: &str,
css_basepath: &str,
output_path: impl AsRef<Path>,
css_output_path: impl AsRef<Path>,
) -> Result<Vec<PathBuf>> {
let paths = install(cache, octocrab, family, output_path).await?;
let css = crate::ide::web::fonts::generate_css_file_from_paths(
css_basepath,
css_family,
paths.iter().flat_map(|path| path.try_file_name().map(|name| name.as_str())),
)?;
ide_ci::fs::tokio::write(css_output_path, css).await?;
Ok(paths)
}
// =============
// === Tests ===
// =============
@ -171,7 +189,7 @@ mod tests {
let path = r"C:\temp\google_fonts2";
let octocrab = ide_ci::github::setup_octocrab().await?;
let cache = Cache::new_default().await?;
let aaa = download_google_font(&cache, &octocrab, "mplus1", path).await?;
let aaa = install(&cache, &octocrab, "mplus1", path).await?;
dbg!(aaa);
Ok(())
}

View File

@ -138,6 +138,32 @@ impl IsTarget for Gui2 {
let WithDestination { inner: _, destination } = job;
async move {
let repo_root = &context.repo_root;
crate::ide::web::google_font::install_with_css(
&context.cache,
&context.octocrab,
"mplus1",
"M PLUS 1",
"/font-mplus1",
&repo_root.app.gui_2.public.font_mplus_1,
&repo_root.app.gui_2.src.assets.font_mplus_1_css,
)
.await?;
crate::ide::web::dejavu_font::install_sans_mono_with_css(
&context.cache,
&context.octocrab,
"/font-dejavu",
&repo_root.app.gui_2.public.font_dejavu,
&repo_root.app.gui_2.src.assets.font_dejavu_css,
)
.await?;
crate::ide::web::enso_font::install_with_css(
&context.cache,
&context.octocrab,
"/font-enso",
&repo_root.app.gui_2.public.font_enso,
&repo_root.app.gui_2.src.assets.font_enso_css,
)
.await?;
crate::web::install(repo_root).await?;
script(repo_root, Scripts::Build)?.run_ok().await?;
ide_ci::fs::mirror_directory(

View File

@ -12,6 +12,7 @@ use tracing::Span;
// === Export ===
// ==============
pub mod extract_files;
pub mod tar;
pub mod zip;

View File

@ -0,0 +1,11 @@
use crate::prelude::*;
pub trait ExtractFiles {
/// The given function will be called with the path of each file within the archive. For each
/// input path, if it returns a path the file will be extracted to the returned path.
///
/// IMPORTANT: If the function uses its input path to generate an output path, care must be
/// taken that the output path is not in an unexpected location, especially if coming from an
/// untrusted archive.
async fn extract_files(self, filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result;
}

View File

@ -1,5 +1,6 @@
use crate::prelude::*;
use crate::archive::extract_files::ExtractFiles;
use flate2::read::GzDecoder;
use std::fs::File;
@ -64,17 +65,6 @@ impl Archive {
Ok(())
}
/// The given function will be called with the path of each file within the archive. For each
/// input path, if it returns a path the file will be extracted to the returned path.
///
/// IMPORTANT: If the function uses its input path to generate an output path, care must be
/// taken that the output path is not in an unexpected location, especially if coming from an
/// untrusted archive.
pub async fn extract_files(self, filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result {
let job = move || self.extract_files_sync(filter);
tokio::task::block_in_place(job)
}
/// Extract all files from the specified subtree in the archive, placing them in the specified
/// output directory.
pub async fn extract_subtree(
@ -106,3 +96,10 @@ impl Archive {
})
}
}
impl ExtractFiles for Archive {
async fn extract_files(self, filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result {
let job = move || self.extract_files_sync(filter);
tokio::task::block_in_place(job)
}
}

View File

@ -1,5 +1,6 @@
use crate::prelude::*;
use crate::archive::extract_files::ExtractFiles;
use anyhow::Context;
use std::io::Cursor;
use zip::read::ZipFile;
@ -68,3 +69,43 @@ pub fn extract_subtree(
}
Ok(())
}
/// Synchronous version of [`extract_files`].
#[context("Failed to extract files from ZIP archive")]
pub fn extract_files_sync(
archive: &mut ZipArchive<std::fs::File>,
mut filter: impl FnMut(&Path) -> Option<PathBuf>,
) -> Result {
for i in 0..archive.len() {
let mut entry = archive.by_index(i).with_context(|| "Error getting ZIP archive entry")?;
let path_in_archive = entry
.enclosed_name()
.with_context(|| "Could not get file path of ZIP archive entry")?;
if let Some(output_path) = filter(path_in_archive) {
let entry_type = if entry.is_dir() { "directory" } else { "file" };
let make_message = |prefix, path: &Path| {
format!(
"{} {:?} entry: {} => {}",
prefix,
entry_type,
path.display(),
output_path.display()
)
};
trace!("{}", make_message("Extracting", path_in_archive));
let mut output = std::fs::File::create(&output_path)
.with_context(|| make_message("Could not extract file", path_in_archive))?;
std::io::copy(&mut entry, &mut output)
.with_context(|| format!("Could not copy file to {}", output_path.display()))?;
}
}
Ok(())
}
impl ExtractFiles for &mut ZipArchive<std::fs::File> {
async fn extract_files(self, filter: impl FnMut(&Path) -> Option<PathBuf>) -> Result {
let job = move || extract_files_sync(self, filter);
tokio::task::block_in_place(job)
}
}

View File

@ -24,6 +24,7 @@
#![feature(pin_macro)]
#![feature(result_option_inspect)]
#![feature(extend_one)]
#![feature(async_fn_in_trait)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
@ -35,6 +36,7 @@
#![warn(trivial_numeric_casts)]
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
#![allow(incomplete_features)]
// ==============

View File

@ -3,6 +3,7 @@
// === Features ===
#![feature(let_chains)]
#![feature(default_free_fn)]
// === Standard Linter Configuration ===
#![deny(non_ascii_idents)]
#![warn(unsafe_code)]
@ -36,9 +37,9 @@ pub use owned_ttf_parser as ttf;
// =================
/// The name of the Enso font family.
pub const ENSO_FONT_FAMILY_NAME: &str = "enso";
pub const FONT_FAMILY: &str = "enso";
const ENSO_FONT_FAMILY_FONTS: &[(&str, ttf::Weight)] = &[
const FONTS: &[(&str, ttf::Weight)] = &[
("Black", ttf::Weight::Black),
("Bold", ttf::Weight::Bold),
("ExtraBold", ttf::Weight::ExtraBold),
@ -67,9 +68,9 @@ pub mod feature {
// === Enso Font ===
// =================
/// Returns the Enso Font.
pub fn enso_font() -> NonVariableDefinition {
ENSO_FONT_FAMILY_FONTS
/// The Enso Font.
pub fn font() -> NonVariableDefinition {
FONTS
.iter()
.map(|(name, weight)| {
let file = format!("Enso-{name}.ttf");
@ -83,13 +84,30 @@ pub fn enso_font() -> NonVariableDefinition {
.collect()
}
/// All font faces contained in this font.
pub fn faces() -> [NonVariableFaceHeader; 9] {
[
NonVariableFaceHeader { weight: ttf::Weight::Thin, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::ExtraLight, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::Light, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::Normal, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::Medium, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::SemiBold, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::Bold, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::ExtraBold, ..default() },
NonVariableFaceHeader { weight: ttf::Weight::Black, ..default() },
]
}
/// Extract the fonts from the given archive file, and write them in the given directory.
#[context("Failed to extract fonts from archive: {}", package.as_ref().display())]
#[context("Failed to extract fonts from archive {}", package.as_ref().display())]
pub async fn extract_fonts(
fonts: &NonVariableDefinition,
package: impl AsRef<Path>,
out_dir: impl AsRef<Path>,
) -> Result {
use ide_ci::archive::extract_files::ExtractFiles;
ide_ci::fs::tokio::create_dir_if_missing(out_dir.as_ref()).await?;
let mut files_expected: HashSet<_> = fonts.files().collect();
ide_ci::archive::tar::Archive::open_tar_gz(&package)
.await?

View File

@ -119,9 +119,11 @@ impl CodeGenerator {
// =================
pub async fn load_enso_font(out_dir: impl AsRef<Path>, code_gen: &mut CodeGenerator) -> Result {
let family_name = enso_enso_font::ENSO_FONT_FAMILY_NAME;
let font_family = enso_enso_font::enso_font();
let package = enso_build::ide::web::fonts::get_enso_font_package().await?;
let family_name = enso_enso_font::FONT_FAMILY;
let font_family = enso_enso_font::font();
let cache = ide_ci::cache::Cache::new_default().await?;
let octocrab = ide_ci::github::setup_octocrab().await?;
let package = enso_build::ide::web::enso_font::download(&cache, &octocrab).await?;
enso_enso_font::extract_fonts(&font_family, package, &out_dir).await?;
code_gen.add_non_variable_font_definition(family_name, &font_family);
for file in font_family.files() {
@ -143,8 +145,6 @@ mod google_fonts {
use super::*;
use crate::CodeGenerator;
use enso_build::ide::web::google_font::download_google_font;
#[derive(Debug)]
pub struct FaceDefinition {
file_name: String,
@ -175,7 +175,9 @@ mod google_fonts {
) -> Result<Vec<DownloadedFile>> {
let octocrab = ide_ci::github::setup_octocrab().await?;
let cache = ide_ci::cache::Cache::new_default().await?;
let result = download_google_font(&cache, &octocrab, name.as_ref(), out_dir).await?;
let result =
enso_build::ide::web::google_font::install(&cache, &octocrab, name.as_ref(), out_dir)
.await?;
result
.into_iter()
.map(|font| Ok(DownloadedFile { name: font.try_file_name()?.as_str().into() }))