#!/usr/bin/env Rscript
if(!"source:tools/utils.R" %in% search()) envir::attach_source("tools/utils.R")
# if(!"source:tools/translate-tools.R" %in% search()) envir::attach_source("tools/translate-tools.R")
## Deferred:
# TODO: k_fft() should really accept and return complex tensors too.
# TODO: activation_resolve() or activation_get() as alias of keras.activation.get() ?
# TODO: the 2-translated.Rmd should include a chunk w/ the function def (for easy seeing while editing)
# with chunk options (include = FALSE)
# TODO: add PR for purrr::rate_throttle("3 per minute")
## Rejected:
# TODO: k_array() should take a 'shape' argument
## Implemented but Questioning:
# TODO: maybe move ops to `op_*` instead of `k_*` ?
if(FALSE) {
Sys.glob(c("R/autogen*.R", "vignettes-src/*.Rmd")) %>%
walk(\(f) {
readLines(f) %>%
str_replace_all("(?<![a-zA-Z0-9])k_", "op_") %>%
writeLines(f)
})
f <- Sys.glob(".tether/man/k_*")
f2 <- sub("/k_", "/op_", f, fixed = TRUE)
fs::file_move(f, f2)
}
if(FALSE){
f <- Sys.glob(c("R/autogen*.R"))
f2 <- sub("/autogen-", "/keras-", f, fixed = TRUE)
fs::file_move(f, f2)
}
fs::dir_walk("man-src", type = "directory", recurse = TRUE,
function(d) {
if(!length(list.files(d, all.files = TRUE, no.. = TRUE))) # empty directory
unlink(d)
})
## In progress
## Waiting to be processed:
# TODO: remove k_amax() and friends, they're redundant w/ k_max(), which already
# takes an axis arg. Only there for numpy api compatability, which
# doesn't matter to us.
#
# TODO: layer_feature_space should take a formula, and dispatch to the features as required.
# ~ scale(foo) * bar
# TODO: train_on_batch and related methods should be autogen'd and exported. Or maybe we curate those,
# and don't export them? (I.e., have the few people that need them access methods via model$train_on_batch())
# TODO: Clean up @family tags (partially autogen, derive from module - then use that to autogenerate _pkgdown.yml)
# TODO: self$model$stop_training <- TRUE should work. Need to avoid propogating `$<-` past first.
# TODO: new_callback_class() should wrap callables to make epoch/batch n 1 based,
# make logs persistent (e.g., wrap the user callable w/ `logs$update(<callback_return>`))
# TODO: in reticulate, change subclassed dict autoconversion back to off:
# so that keras$utils$get_custom_objects()$clear() works.
# TODO: get_custom_objects() needs thinking
#
# TODO: many of the applications can share a man page, e.g., application_convnext_{large...}
# TODO: r_name autogen: move "set" to tail, so have config_floatx(), config_floatx_set()
# TODO: revisit history - also mentions in docs (e.g., in callback_model_checkpoint())
# TODO: BackupAndRestore is broken, doesn't respect current epoch. file/fix upstream.
# ~/github/keras-team/keras/keras/callbacks/backup_and_restore_callback.py
# TODO: reticulate, support NO_COLOR (or similar) to disable the link wrapper around `py_last_error()` hint.
# TODO: # fix `fit()` not returning `history` correctly
# TODO: global search for "axis" in doc text, update to 1 based where appropriate.
#
# TODO: in reticulate: virtualenv_starter(): check for --enable-shared
# TODO: fix py_func(), for r_to_py.R6ClassGenerator
# can't use __signature__ anymore in keras_core...
## TODO: "keras.applications.convnext" is a module, filtered out has good stuff
# TODO: initializer families:
# <class 'keras.initializers.constant_initializers.Zeros'>
# <class 'keras.initializers.random_initializers.RandomUniform'>
# TODO: next: losses, metrics, saving, guides/vignettes
#
# TODO: global search replace in man-src/*.Rmd "([^ ])=([^ ])" "\\1 = \\2"
# TODO: bidirectional, time_distributed -- need special caseing
#
# TODO: note in docs for k_logical_and (and friends) that these are dispatched
# to from & != and so on.
#
# TODO: k_arange: should it default to produce floats?
# TODO: keras.Function ?? keras.Variable ?? keras.name_scope ??
#
# TODO: remove k_random_binomial() ??
#
# TODO: layer_feature_space() needs massaging.
#
# TODO: to_categorical():
# - handle factor/character https://github.com/rstudio/keras/issues/1055
# - make it 1 based?
#
# TODO: param descriptions - make it more robust to changes upstream
# autoinject "see description" without needing it in the yml.
# yml is only for explicit overrides
#
# TODO: implement dim() S3 generic.
#
# TODO: remove @import methods ??
#
# TODO: add @import reticulate ??
#
# TODO: remove any tensorflow imports / DESCRIPTION deps
#
# TODO: trimws @returns
#
# TODO: k_istft k_irfft example is wrong, investigate
#
# TODO: rename: k_image_pad_images -> k_image_pad
#
# TODO: this should work: k_convert_to_tensor(c(1, 3, 2, 0), "int32")
#
# TODO: as_0_based_index() utility: as_integer(x + 1)
#
# TODO: k_array(<r_array>) should default to float32, not float64
#
# TODO: k_array(<r_int>) why int64, and not int32?
#
# TODO: revisit docs for k_scatter_update and k_scatter, remove python sliceisms
# TODO: fix k_vectorized_map() arg rename kludge
#
# TODO: layer_category_encoding()(count_weights) call arg example not working
#
# TODO: revisit k_vectorized_map() man page
# The source of truth for the current translation should be...?
# - the autogened file R/autogen-*.R, or
# - man-src/*/2-translated.Rmd
#
# TODO: `axis` arg in merging layers has to wrong transformer, should be `as_axis()`, is `as_integer()`
# TODO: global doc search for None/True/False/[Tt]uple/[Dd]ict(ionary|\\b) (Almost DONE except for Tuples)
#
# TODO: layer_feature_space() needs many helpers for float_normalized() and friends
# output_mode = 'dict' should be 'named list' ?
#
# TODO: feature_space saving errors
#
# TODO: layer_lambda docs: bring back section on serialization and limitations after
# fixing serialization.
#
# TODO: a layer_hashed_crossing() instance should have a customized wrapper that
# splices in the args, so instead of layer(list(a, b)), you can do layer(a, b)
# also, the example should maybe be nicer, with a named list/dict being passed,
# instead of a tuple
#
# TODO: this shouldn't error (empty last arg should be no-op):
# if(FALSE) {
# keras$utils$FeatureSpace$cross(
# feature_names=c("string_values", "int_values"),
# crossing_dim=32,
# output_mode="int",
# )
# }
#
# TODO: layer_torch_module_wrapper raises an error - aybe incompatible torch version?a
#
# TODO: config_{enable,disable,is_enabled}_traceback_filtering have identical docstrings,
# should all be the same page.
#
# TODO: @family tags should be manually managed, not dynamically generated.
# perhaps in a yml file
#
# TODO: refactor so 'endpoint' can be a tuple like "keras.losses.Hinge,keras.losses.hinge"
# and 0-upstream.md is a concatenation of multiple endpoints, separated
# by a delimiter so we can do
# read_file("0-upstream.md") |>
# strsplit(str_c(strrep("~", 80), "\n")) |>
# lapply(format_upstream)
memoise::forget(mk_export)
get_translations <- function() {
dirs <- fs::dir_ls("man-src/", regexp = "\\.Rmd$") |>
sort() %>%
# .[grep("^k_", basename(.))] %>%
# .[grep("^layer_", basename(.))] %>%
set_names(basename) %>%
# keep(\(dir) read_file(path(dir, "2-translated.Rmd")) |>
keep(\(dir) read_file(path(dir)) |>
str_detect("```python")) |>
(\(x) { message("remaining: ", length(x)); x})() |>
# (\(x) {walk(x, message); x})()
head(10) |>
purrr::walk(\(dir) {
og <- read_file(dir)
# og <- read_file(dir/"2-translated.Rmd")
new <- og %>%
str_split_lines() %>%
str_replace_all("```python", "```{r}") %>%
str_replace_all(fixed("keras.ops."), "k_") %>%
str_replace_all(fixed("ops."), "k_") %>%
str_replace_all(fixed("= np.array"), "<- k_array") %>%
str_replace_all(fixed("np.array"), "k_array") %>%
str_replace_all(fixed(").reshape("), "|> k_reshape(c(") %>%
str_replace_all(fixed("None"), "NULL") %>%
str_replace_all(fixed("k_convert_to_tensor(["), "k_convert_to_tensor(c(") %>%
str_replace_all(fixed("k_array(["), "k_array(c(") %>%
str_replace_all(fixed("])"), "))") %>%
str_replace_all(fixed("np.random.rand"), "random_uniform(c") %>%
str_replace_all("^([a-z_0-9A-Z]+) =", "\\1 <-") %>%
str_replace_all("None", "NULL") %>%
str_replace_all("\\bdict(ionary)?", "named list") %>%
str_replace_all("\\bDict(ionary)?", "Named list") %>%
str_replace_all("tuple", "list") %>%
str_replace_all("Tuple", "List") %>%
str_replace_all("True", "TRUE") %>%
str_replace_all("False", "FALSE") %>%
str_replace_all(fixed("np.random.random(("), "random_uniform(c(") %>%
# str_replace_all("([0-9])(\\.0?\\b)", "\\1") %>%
str_replace_all(fixed("list/list"), "list") %>%
str_replace_all(fixed("list/list", ignore_case = TRUE), "List") %>%
str_replace_all(fixed("keras.layers."), "layer_") %>%
str_replace_all(fixed("layers."), "layer_") %>%
str_flatten_lines() %>%
identity()
# str_replace_all(fixed("k_convert_to_tensor(["), "k_array(c(") %>%
# new |> write_lines(dir/"2-translated.Rmd")
new |> write_lines(dir)
file.edit(dir)
# file.edit(dir/"2-translated.Rmd")
# stop()
return()
withr::local_dir(dir)
message("Translating: ", basename(dir))
new <- og |> get_translated_roxygen()
# message("cost: ")
new |> write_lines("2-translated.Rmd")
# write_rds(new, "completion.rds")
})
x <- system("git diff --name-only", intern = TRUE) %>%
grep("2-translated.Rmd$", ., value = TRUE)
x %>%
double_quote() %>%
str_flatten(",\n ") %>%
str_c(" ", .) %>%
c("file.edit(", ., ")") %>%
str_flatten_lines() %>%
message()
# message(sprintf("file.edit(%s)", shQuote(dirs))
# file.edit({str_flatten(x, ', ')})}")
}
if(FALSE) {
get_translations()
list.files("man-src", "2-translated\\.Rmd$", recursive = TRUE,
full.names = TRUE) %>%
walk(\(f) {
x <- read_lines(f)
while(x[1] == "---") {
x <- x[-(1:3)]
}
x <- str_trim(str_flatten_lines(
"---",
'knit: ({source(here::here("tools/knit.R")); knit_man_src})',
"---",
x))
write_lines(x, f)
})
withr::with_dir("man-src", {
list.dirs(full.names = TRUE) %>%
as_fs_path() %>%
walk(\(d) {
if (!file_exists(d / "2-translated.Rmd"))
return()
# browser()
link_create(path(d, "2-translated.Rmd"),
path(d, ext = "Rmd"))
})
})
}
# start by regenerating patch files
endpoints <- list_endpoints(skip = c(
# to be processed / done
"keras.saving",
"keras.backend",
"keras.dtensor",
"keras.mixed_precision",
"keras.models",
"keras.export",
"keras.experimental",
"keras.applications",
"keras.legacy",
"keras.distribution", # multi-host multi-device training
"keras.protobuf", # ???
"keras.datasets", # datasets unchanged, no need to autogen
"keras.preprocessing.text", # deprecated
"keras.estimator", # deprecated
"keras.optimizers.legacy",
"keras.src" # internal
)) %>%
c(list_endpoints("keras.applications", max_depth = 1)) %>%
# filter out top level non module symbols for now
grep("keras.([^.]+)$", ., value = TRUE, invert = TRUE) %>%
unique()
endpoints %<>% filter_out_endpoint_aliases()
## filter out some endpoints that need special handling
endpoints %<>% setdiff(c %(% {
"keras.layers.Layer" # only for subclassing
"keras.optimizers.Optimizer" # only for subclassing
"keras.regularizers.Regularizer" # only for subclassing
"keras.constraints.Constraint" # only for subclassing
"keras.initializers.Initializer" # only for subclassing
"keras.callbacks.Callback" # only for subclassing
"keras.losses.Loss" # only for subclassing
"keras.metrics.Metric" # only for subclassing
"keras.optimizers.schedules.LearningRateSchedule" # only for subclassing
"keras.utils.PyDataset" # parallel processing in R not possible this way
"keras.utils.Sequence" # tfdatasets is ~100x better anyway.
"keras.utils.plot_model" # S3 method plot()
# TODO: revisit custom_object_scope()
"keras.utils.custom_object_scope" # need custom work to resolve py_names -
# manually wrapped to `with_custom_object_scope()`
"keras.metrics.Accuracy" # weird,
# only class handle, no fn handle - weird alias
# for binary_accuracy, but without any threshold casting.
# kind of confusing - the keras.metrices.<type>*_accuracy
# endpoints are much preferable.
"keras.utils.Progbar" # needs thinking
"keras.layers.Wrapper" # needs thinking
"keras.layers.InputLayer" # use Input instead
"keras.layers.InputSpec" # ??
# "keras.layers.Bidirectional" # ??
"keras.callbacks.CallbackList" # just an abstract list
"keras.callbacks.History" # always added to fit() by default
"keras.optimizers.LegacyOptimizerWarning"
"keras.ops.absolute" # alias dup of abs.
})
endpoints <-
unlist(list(
str_c("keras.Model.", names(keras$Model))
))
exports <- endpoints |>
purrr::set_names() |>
lapply(mk_export)
df <- exports |>
lapply(\(e) {
unclass(e) |> map_if(\(attr) !is_scalar_atomic(attr), list) |>
as_tibble_row()
}) |>
list_rbind() |>
select(r_name, endpoint, type, module, everything())
df$dump %>%
str_flatten("\n\n") %>%
write_lines("R/keras-Model-methods.R")
df <- df |>
mutate(
man_src_dir = path("man-src", r_name),
endpoint_sans_name = str_extract(endpoint, "keras\\.(.*)\\.[^.]+$", 1))
if(!all(dir_exists(df$man_src_dir))) {
df |>
filter(!dir_exists(man_src_dir)) |>
rowwise() |>
mutate(init_man_src_dir = {
# browser()
man_src_dir |>
dir_create() |>
withr::with_dir({
write_lines(format_man_src_0(endpoint), "0-upstream.md")
write_lines(roxygen, "1-formatted.md")
write_lines(roxygen, "2-translated.Rmd")
NULL
})
})
}
if(FALSE) {
df %>%
filter(r_name |> startsWith("constraint_")) %>%
rowwise() %>%
mutate(dump2 = str_flatten_lines(
str_c("# ", endpoint),
str_c("#' ", read_lines(path(man_src_dir, "2-translated.Rmd"))),
str_c("#' ", glue("@tether {endpoint}")),
str_c(r_name, " <-"),
deparse(r_fn)
)) %>%
ungroup() %>%
{
write_lines(str_flatten(.$dump2, "\n\n\n"),
'R/autogen2-constraints.R')
}
}
df <- df |>
arrange(endpoint_sans_name, module, r_name) |>
mutate(file = if_else(endpoint_sans_name == "layers",
{
module |>
str_replace("^keras(_core)?\\.(src\\.)?", "") |>
str_replace(paste0(endpoint_sans_name, "\\."), "") |>
str_replace("^([^.]+).*", paste0(endpoint_sans_name, "-\\1.R"))
},
str_c(endpoint_sans_name %>% str_replace_all(fixed("."), "-"), ".R"))
)
unlink(Sys.glob("R/autogen-*.R"))
get_translated_lines <- function(r_name) {
x <- readLines(fs::path("man-src", r_name, "2-translated.Rmd"))
if(x[1] == "---" && x[3] == '---')
x <- x[-(1:3)]
x <- str_trim(x, "right")
}
stop("Edit files in R/*.R directly and then cmd+shift+d / devtools::document() to regenerate
R/*.R files. Docs/fns in R/*.R is the new source of truth, and tools/make.R is just for
generating initial wrappers for new symbols")
# https://roxygen2.r-lib.org/articles/rd-formatting.html#code-chunks
# knit_print.python.builtin.object <- # strip env addressr
df |>
group_by(file) |>
dplyr::group_walk(\(df, grp) {
txt <- df |>
rowwise() |>
mutate(final_dump = str_flatten_lines(
# glue(r"--("{fs::path('man-src', r_name, ext = 'Rmd')}" # |>file.edit() # or cmd+click to edit man page)--"),
# glue(r"--("{fs::path(man_src_dir, "0-upstream.md")}" # view the upstream doc)--"),
# glue(r"--(#' @eval readLines("{fs::path(man_src_dir, "3-rendered.md")}") )--"),
glue(r"--(#' {get_translated_lines(r_name)})--"),
glue(r"--(#' @tether {endpoint})--"),
str_c(r_name, " <- "),
deparse(r_fn),
""
)) |>
_$final_dump
txt <- str_flatten(
c("## Autogenerated. Do not modify manually.", txt),
"\n\n\n")
txt <- txt |>
str_split_lines() |>
str_remove_non_ascii() |>
str_trim("right") |>
str_flatten_lines() |>
str_trim()
txt <- txt %>%
str_flatten_and_compact_lines(roxygen = TRUE) %>%
# handle empty @description TODO: handle this earlier, in dump.
gsub("#' @description\n#'\n#' @", "#' @", ., fixed = TRUE)
file <- paste0("R/autogen-", grp$file)
writeLines(txt, file)
})
if(FALSE) {
x <- "abc\n@param foo baralkjasdf\n@param bar asdfklajfsa\n"
xx <- str_split_1(x, fixed("\n@"))
xx[startsWith(xx, "param ")] %<>%
str_match_all("param ([^ ]+)(.*)") %>%
map_chr(\(x) {
stopifnot(nrow(x) == 1)
name <- x[,2]
desc <- glue::trim(str_trim(x[,3]))
sprintf("param %s\n%s", name, desc)
})
x2 <- xx %>%
str_flatten("\n@") %>%
str_replace_all("[\n]+@param", "\n\n@param") %>%
str_replace_all("[\n]+@param", "\n\n@param")
cat(x2)
xx <- str_replace_all(x, "@param ([^ ]+)(.*)@", function(x){ browser()})
# xx <- str_match_all(x, regex("(.*)@param ([^ ]+)(.*)@", multiline = TRUE, dotall = T)) |> _[[1L]] #|> as.list()
xx <- str_locate_all(x, regex("@param ([^ ]+)(.*)@", multiline = TRUE, dotall = T))
xx
names(x) <- c("b", "name", "desc")
x$desc %<>% glue::trim()
# x <-
}
Sys.glob(c("man-src/*/2-*.Rmd")) %>%
walk(\(f) {
x <- read_lines(f)
xx <- x |>
# str_replace_all( "([^ ]+)\\$shape\\b", "shape(\\1)") |>
str_trim("right")
write_lines(xx, f)
})
# dir_ls("man-src", glob = "1-*.md", recurse = TRUE) %>%
#
# Sys.glob(c("man-src/*/1-*.md", "man-src/*/2-*.Rmd")) %>%
# walk(\(f) {
# x <- read_file(f)
#
# xx <- str_split_1(x, fixed("\n@"))
# ip <- startsWith(xx, "param ")
# xx[ip] <- xx[ip] %>%
# map_chr(\(x) {
# m <- str_match_all(x, regex("param ([^[:space:]]+)(.*)",
# dotall = TRUE, multiline = TRUE))[[1]]
# stopifnot(nrow(m) == 1)
# # browser()
# name <- m[,2]
# desc <- glue::trim(str_trim(m[,3]))
# sprintf("param %s\n%s\n", name, desc)
# })
#
# x2 <- xx %>%
# str_flatten("\n@") %>%
# str_replace_all("[\n]+@param", "\n\n@param") %>%
# str_replace_all("[\n]+@param", "\n\n@param")
#
# write_file(x2, f)
# })
# man_src_pull_upstream_updates(write_out_only_formatted = TRUE)
# stop("FINITO")
man_src_pull_upstream_updates()
devtools::load_all() # TODO: render should be w/ an installed package and in a fresh r session w/ only `library(keras3)`
man_src_render_translated()
envir::import_from(knitr, knit_print)
registerS3method("knit_print", "python.builtin.object", function(x, ...) {
# browser()
# utils::str(x)
x <- capture.output(print(x))
x <- trimws(x, "right")
# strip object addresses; no noisy diff
x <- sub(" at 0x[0-9A-Fa-f]{9}>$", ">", x, perl = TRUE)
# remove reticulate hint from exceptions
x <- x[!grepl(r"{## .*rstudio:run:reticulate::py_last_error\(\).*}", x)]
x <- x[!grepl(r"{## .*reticulate::py_last_error\(\).*}", x)]
writeLines(x)
})
process_chunk_output <- function(x, options) {
# TKutils::str_vars(knitr::opts_knit$get("out.format"))
message("process_chunk_output:")
str(x)
writeLines(x)
cat("---\n")
# utils::str(x)
x_in <- x
x <- x |> strsplit("\n") |> unlist() #|> trimws("right")
x <- trimws(x, "right")
# strip object addresses; no noisy diff
x <- sub(" at 0x[0-9A-Fa-f]{9}>$", ">", x, perl = TRUE)
# remove reticulate hint from exceptions
x <- x[!grepl(r"{## .*rstudio:run:reticulate::py_last_error\(\).*}", x)]
x <- x[!grepl(r"{## .*reticulate::py_last_error\(\).*}", x)]
x <- paste0(x, collapse = "\n")
if(x_in |> endsWith("\n") &&
!x |> endsWith("\n"))
x <- paste0(x, "\n")
x
}
# we delay setting the output hook `knit_hooks$set(output = )` because
# if we set it too early, knitr doesn't set `render_markdown()` hooks.
# so we set a chunk option, which triggers setting the output hook
# one after knitr is already setup and knitting.
knitr::opts_hooks$set(
keras.roxy.post_process_output = function(options) {
message("Running option hook")
str(options)
# this is a self destructing option, run once before the first
# chunk in a roxy block is evaluated
options$keras.roxy.post_process <- NULL
knitr::opts_chunk$set(keras.roxy.post_process = NULL)
# make output reproducible
# `evalenv` is created once per block, but knit() is called once per chunk
# so we use this to detect if we're in the first chunk of a block and run setup
if(is.null(roxygen2::roxy_meta_get("evalenv")$.__ran_block_init__)) {
keras$utils$clear_session()
set.seed(1L)
keras$utils$set_random_seed(1L)
assign(x = ".__ran_block_init__",
envir = roxygen2::roxy_meta_get("evalenv"),
value = TRUE)
}
local({
og_output_hook <- knitr::knit_hooks$get("output")
if(isTRUE(attr(og_output_hook, "keras.roxy.post_process", TRUE))) {
message("Bailing early, not setting output hook")
print(og_output_hook)
return()
}
message("Setting output hook")
knitr::knit_hooks$set(output = structure(function(x, options) {
x <- process_chunk_output(x, options)
og_output_hook(x, options)
}, "keras.roxy.post_process" = TRUE))
})
options
}
)
og_knit <- knitr::knit
unlockBinding("knit", asNamespace("knitr"))
knitr <- asNamespace("knitr")
knitr$knit <- function(input, output = NULL, tangle = FALSE, text = NULL,
quiet = FALSE, envir = parent.frame(), encoding = "UTF-8") {
message("~~~~")
message("Entering knit(), text = ")
writeLines(c(text))
ret <- og_knit(input, output, tangle, text,
quiet, envir, encoding)
message("Exiting knit(), ret =")
writeLines(ret)
message("_____")
ret
}
options(warn = 1)
# trace(knitr::knit, quote())
devtools::document(roclets = c('rd', 'namespace', "roxytether::tether_roclet"))
# devtools::document(roclets = c('rd', 'namespace'))
envir::attach_source("tools/utils.R")
trimws_file("man/*.Rd")
# ok, so knit() is called once per chunk
# but the evalenv is created once per block
#
# we want to call clear_session() once per block
# so we need a way to detect from a chunk if we're in a block
# stop()
if(interactive()) local({
rx <- callr::r_bg(\() remotes::install_local(force = TRUE, upgrade = "never"))
later::later(\() cat(rx$read_all_output()), delay = 17)
}) else {
remotes::install_local(force = TRUE)
rcmdcheck::rcmdcheck()
}
# remotes::update_packages(upgrade = "always")
# pkgdown::build_site()
# stop("DONE", call. = FALSE)
message("DONE!")
if(FALSE) {
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.