diff --git a/dart/.gitignore b/dart/.gitignore new file mode 100644 index 00000000..3cceda55 --- /dev/null +++ b/dart/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/dart/analysis_options.yaml b/dart/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/dart/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/dart/example/minify_html_example.dart b/dart/example/minify_html_example.dart new file mode 100644 index 00000000..e39e38a3 --- /dev/null +++ b/dart/example/minify_html_example.dart @@ -0,0 +1,5 @@ +import 'package:minify_html/minify_html.dart'; + +void main(List args) { + final minified = minifyHtml("

Hello, world!

"); +} diff --git a/dart/lib/minify_html.dart b/dart/lib/minify_html.dart new file mode 100644 index 00000000..99d12d1e --- /dev/null +++ b/dart/lib/minify_html.dart @@ -0,0 +1,3 @@ +library minify_html; + +export 'src/minify_html.dart'; diff --git a/dart/lib/src/bindings.dart b/dart/lib/src/bindings.dart new file mode 100644 index 00000000..5d5bab3f --- /dev/null +++ b/dart/lib/src/bindings.dart @@ -0,0 +1,71 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:minify_html/src/locator.dart'; + +typedef minify_html_native = Int32 Function( + Pointer input, + Uint32 length, + Bool do_not_minify_doctype, + Bool ensure_spec_compliant_unquoted_attribute_values, + Bool keep_closing_tags, + Bool keep_html_and_head_opening_tags, + Bool keep_spaces_between_attributes, + Bool keep_comments, + Bool minify_css, + Bool minify_css_level_1, + Bool minify_css_level_2, + Bool minify_css_level_3, + Bool minify_js, + Bool remove_bangs, + Bool remove_processing_instructions, +); + +typedef get_last_result_native = Pointer Function(); + +typedef clear_last_result_native = Void Function(); + +class MinifyHtmlBindings { + late DynamicLibrary _library; + + late int Function( + Pointer input, + int length, + bool do_not_minify_doctype, + bool ensure_spec_compliant_unquoted_attribute_values, + bool keep_closing_tags, + bool keep_html_and_head_opening_tags, + bool keep_spaces_between_attributes, + bool keep_comments, + bool minify_css, + bool minify_css_level_1, + bool minify_css_level_2, + bool minify_css_level_3, + bool minify_js, + bool remove_bangs, + bool remove_processing_instructions, + ) minifyHtml; + + late Pointer Function() getLastResult; + + late void Function() clearLastResult; + + MinifyHtmlBindings() { + _library = loadDynamicLibrary(); + + minifyHtml = _library + .lookup>("minify_html") + .asFunction(); + getLastResult = _library + .lookup>("get_last_result") + .asFunction(); + clearLastResult = _library + .lookup>("clear_last_result") + .asFunction(); + } +} + +MinifyHtmlBindings? _cachedBindings; +MinifyHtmlBindings get bindings => _cachedBindings ??= MinifyHtmlBindings(); diff --git a/dart/lib/src/locator.dart b/dart/lib/src/locator.dart new file mode 100644 index 00000000..5341ad3a --- /dev/null +++ b/dart/lib/src/locator.dart @@ -0,0 +1,76 @@ +import 'dart:ffi'; +import 'dart:io'; + +/// Attempts to locate the MinifyHtml dynamic library. +/// +/// Throws [MinifyHtmlLocatorError] if the dynamic library could not be found. +DynamicLibrary loadDynamicLibrary() { + if (Platform.isIOS) { + return DynamicLibrary.process(); + } else if (Platform.isMacOS) { + return _locateOrError(appleLib); + } else if (Platform.isWindows) { + return _locate(windowsLib) ?? DynamicLibrary.executable(); + } else if (Platform.isLinux) { + return _locateOrError(linuxLib); + } else if (Platform.isAndroid) { + return DynamicLibrary.open(linuxLib); + } else if (Platform.isFuchsia) { + throw MinifyHtmlLocatorError( + 'MinifyHtml is currently not supported on Fuchsia.', + ); + } else { + throw MinifyHtmlLocatorError( + 'MinifyHtml is currently not supported on this platform.', + ); + } +} + +/// This error is thrown when the dynamic library could not be found. +class MinifyHtmlLocatorError extends Error { + final String message; + + MinifyHtmlLocatorError( + this.message, + ); + + @override + String toString() => 'MinifyHtmlLocatorError: $message'; +} + +/// The command that can be used to set up this package. +const invocationString = 'dart run minifyhtml:setup'; + +/// The expected name of the MinifyHtml library when compiled for Apple devices. +const appleLib = 'libminifyhtml.dylib'; + +/// The expected name of the MinifyHtml library when compiled for Linux devices. +const linuxLib = 'libminifyhtml.so'; + +/// The expected name of the MinifyHtml library when compiled for Windows devices. +const windowsLib = 'minifyhtml.dll'; + +const _minifyhtmlToolDir = '.dart_tool/minifyhtml/'; + +DynamicLibrary? _locate(String libName) { + if (FileSystemEntity.isFileSync(libName)) { + return DynamicLibrary.open(libName); + } + + final toolLib = + Directory.current.uri.resolve("$_minifyhtmlToolDir$libName").toFilePath(); + if (FileSystemEntity.isFileSync(toolLib)) { + return DynamicLibrary.open(toolLib); + } + + return null; +} + +DynamicLibrary _locateOrError(String libName) { + final value = _locate(libName); + if (value != null) { + return value; + } else { + throw MinifyHtmlLocatorError('MinifyHtml library not found'); + } +} diff --git a/dart/lib/src/minify_html.dart b/dart/lib/src/minify_html.dart new file mode 100644 index 00000000..1e1c1a98 --- /dev/null +++ b/dart/lib/src/minify_html.dart @@ -0,0 +1,37 @@ +import 'package:ffi/ffi.dart'; +import 'package:minify_html/src/bindings.dart'; +import 'package:minify_html/src/models.dart'; +import 'package:minify_html/src/resource.dart'; + +String minifyHtml(String source, [Cfg? cfg]) { + cfg = cfg ??= Cfg(); + + final srcC = Utf8Resource(source); + + try { + final length = bindings.minifyHtml( + srcC.unsafe(), + srcC.length, + cfg.doNotMinifyDoctype, + cfg.ensureSpecCompliantUnquotedAttributeValues, + cfg.keepClosingTags, + cfg.keepHtmlAndHeadOpeningTags, + cfg.keepSpacesBetweenAttributes, + cfg.keepComments, + cfg.minifyCss, + cfg.minifyCssLevel1, + cfg.minifyCssLevel2, + cfg.minifyCssLevel3, + cfg.minifyJs, + cfg.removeBangs, + cfg.removeProcessingInstructions, + ); + + final value = bindings.getLastResult().toDartString(length: length); + bindings.clearLastResult(); + + return value; + } finally { + srcC.free(); + } +} diff --git a/dart/lib/src/models.dart b/dart/lib/src/models.dart new file mode 100644 index 00000000..4cbd1154 --- /dev/null +++ b/dart/lib/src/models.dart @@ -0,0 +1,38 @@ +class Cfg { + const Cfg({ + this.doNotMinifyDoctype = false, + this.ensureSpecCompliantUnquotedAttributeValues = false, + this.keepClosingTags = false, + this.keepHtmlAndHeadOpeningTags = false, + this.keepSpacesBetweenAttributes = false, + this.keepComments = false, + this.minifyCss = false, + this.minifyCssLevel1 = false, + this.minifyCssLevel2 = false, + this.minifyCssLevel3 = false, + this.minifyJs = false, + this.removeBangs = false, + this.removeProcessingInstructions = false, + }); + + const Cfg.specCompliant() + : this( + doNotMinifyDoctype: true, + ensureSpecCompliantUnquotedAttributeValues: true, + keepSpacesBetweenAttributes: true, + ); + + final bool doNotMinifyDoctype; + final bool ensureSpecCompliantUnquotedAttributeValues; + final bool keepClosingTags; + final bool keepHtmlAndHeadOpeningTags; + final bool keepSpacesBetweenAttributes; + final bool keepComments; + final bool minifyCss; + final bool minifyCssLevel1; + final bool minifyCssLevel2; + final bool minifyCssLevel3; + final bool minifyJs; + final bool removeBangs; + final bool removeProcessingInstructions; +} diff --git a/dart/lib/src/resource.dart b/dart/lib/src/resource.dart new file mode 100644 index 00000000..9beadefb --- /dev/null +++ b/dart/lib/src/resource.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +final DynamicLibrary stdlib = DynamicLibrary.process(); +final posixFree = stdlib.lookup>("free"); + +class Utf8Resource implements Finalizable { + static final NativeFinalizer _finalizer = NativeFinalizer(posixFree); + + /// [_cString] must never escape [Utf8Resource], otherwise the + /// [_finalizer] will run prematurely. + late final Pointer _cString; + late final int length; + + Utf8Resource(String source) { + final units = utf8.encode(source); + length = units.length; + + final Pointer result = malloc(units.length); + final Uint8List nativeString = result.asTypedList(units.length); + nativeString.setAll(0, units); + _cString = result.cast(); + + _finalizer.attach(this, _cString.cast(), detach: this); + } + + void free() { + _finalizer.detach(this); + calloc.free(_cString); + } + + /// Ensure this [Utf8Resource] stays in scope longer than the inner resource. + Pointer unsafe() => _cString; +} diff --git a/dart/native/.gitignore b/dart/native/.gitignore new file mode 100644 index 00000000..869df07d --- /dev/null +++ b/dart/native/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/dart/native/Cargo.toml b/dart/native/Cargo.toml new file mode 100644 index 00000000..4296b243 --- /dev/null +++ b/dart/native/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "minify-html-native" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "minifyhtml" +crate-type = ["cdylib"] + +[dependencies] +minify-html = { version = "0.10.8", path = "../../rust/main" } diff --git a/dart/native/src/lib.rs b/dart/native/src/lib.rs new file mode 100644 index 00000000..b6bcafe6 --- /dev/null +++ b/dart/native/src/lib.rs @@ -0,0 +1,63 @@ +use std::{cell::RefCell, ptr, slice}; + +use minify_html::Cfg; + +thread_local! { + static LAST_RESULT: RefCell>> = RefCell::new(None); +} + +#[no_mangle] +pub extern "C" fn minify_html( + input: *const u8, + length: usize, + do_not_minify_doctype: bool, + ensure_spec_compliant_unquoted_attribute_values: bool, + keep_closing_tags: bool, + keep_html_and_head_opening_tags: bool, + keep_spaces_between_attributes: bool, + keep_comments: bool, + minify_css: bool, + minify_css_level_1: bool, + minify_css_level_2: bool, + minify_css_level_3: bool, + minify_js: bool, + remove_bangs: bool, + remove_processing_instructions: bool, +) -> usize { + let src = unsafe { slice::from_raw_parts(input, length) }; + + let cfg = Cfg { + do_not_minify_doctype, + ensure_spec_compliant_unquoted_attribute_values, + keep_closing_tags, + keep_html_and_head_opening_tags, + keep_spaces_between_attributes, + keep_comments, + minify_css, + minify_css_level_1, + minify_css_level_2, + minify_css_level_3, + minify_js, + remove_bangs, + remove_processing_instructions, + }; + + let result = minify_html::minify(src, &cfg); + let len = result.len(); + + LAST_RESULT.with(|v| *v.borrow_mut() = Some(result)); + len +} + +#[no_mangle] +pub extern "C" fn get_last_result() -> *const u8 { + LAST_RESULT.with(|prev| match prev.borrow().as_ref() { + Some(bytes) => bytes.as_ptr(), + None => return ptr::null(), + }) +} + +#[no_mangle] +pub extern "C" fn clear_last_result() { + LAST_RESULT.with(|value| *value.borrow_mut() = None) +} diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml new file mode 100644 index 00000000..b134379e --- /dev/null +++ b/dart/pubspec.yaml @@ -0,0 +1,14 @@ +name: minify_html +description: Extremely fast and smart HTML + JS + CSS minifier +version: 0.10.8 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: '>=2.19.2 <3.0.0' + +dependencies: + ffi: ^2.0.1 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.21.0 diff --git a/dart/test/minify_html_test.dart b/dart/test/minify_html_test.dart new file mode 100644 index 00000000..0238678f --- /dev/null +++ b/dart/test/minify_html_test.dart @@ -0,0 +1,11 @@ +import 'package:minify_html/minify_html.dart'; +import 'package:test/test.dart'; + +void main() { + test("should minify html", () { + expect( + minifyHtml("

Hello World!

"), + "

Hello World!", + ); + }); +}