#!/bin/bash # build.sh - gumx.cc static site generator # Licensed under the MIT License (see LICENSES/MIT) # Must be run directly, not sourced [[ "$(basename "${0}")" == "build.sh" ]] || { echo "Do not source build.sh"; exit 1; } set -euo pipefail # --------------------------------------------------------------------------- # Source mo mustache renderer # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ ! -f "${SCRIPT_DIR}/mo" ]]; then echo "[error] mo not found. Run: curl -sL https://raw.githubusercontent.com/tests-always-included/mo/master/mo -o mo && chmod +x mo" >&2 exit 1 fi # shellcheck source=mo . "${SCRIPT_DIR}/mo" # --------------------------------------------------------------------------- # Defaults # --------------------------------------------------------------------------- VERBOSE=false DRAFTS=false FORCE=false DO_CLEAN=false DO_PREP=false DO_SERVE=false LIST_COUNT=5 TMPDIR_BUILD="$(mktemp -d)" # --------------------------------------------------------------------------- # Cleanup trap # --------------------------------------------------------------------------- cleanup() { rm -rf "${TMPDIR_BUILD}" } trap cleanup EXIT INT TERM # --------------------------------------------------------------------------- # CLI parsing # --------------------------------------------------------------------------- usage() { cat <&2; usage; exit 1 ;; esac shift done # --------------------------------------------------------------------------- # --clean # --------------------------------------------------------------------------- if [[ "${DO_CLEAN}" == true ]]; then rm -rf output echo "[clean] output/ removed" exit 0 fi # --------------------------------------------------------------------------- # --prep (absorbed from prep.sh) # --------------------------------------------------------------------------- if [[ "${DO_PREP}" == true ]]; then echo "[prep] Optimizing untracked images..." for img in $(git ls-files -o images/ 2>/dev/null); do if identify "${img}" > /dev/null 2>&1; then mogrify -strip -resize 800x "${img}" echo "[prep:processed] ${img}" else echo "[prep:ignored] ${img}" fi done echo "[prep] Adding trailing newlines to .md files..." local _prep_list _prep_list="$(mktemp)" find . -name '*.md' -not -path './.git/*' -not -path './mo/*' -print0 > "${_prep_list}" while IFS= read -r -d '' f; do if [[ -s "${f}" ]]; then last_char="$(tail -c1 "${f}")" if [[ "${last_char}" != "" ]]; then printf '\n' >> "${f}" echo "[prep:newline] ${f}" fi fi done < "${_prep_list}" rm -f "${_prep_list}" fi # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { if [[ "${VERBOSE}" == true ]]; then echo "$*"; fi } # needs_rebuild src dst [template] # Returns 0 (true / do rebuild) if dst is missing or older than src/template # Returns 1 (false / skip) if dst is up-to-date and --force is not set needs_rebuild() { local src="$1" dst="$2" tmpl="${3:-}" [[ "${FORCE}" == true ]] && return 0 [[ ! -f "${dst}" ]] && return 0 [[ "${src}" -nt "${dst}" ]] && return 0 [[ -n "${tmpl}" && -f "${tmpl}" && "${tmpl}" -nt "${dst}" ]] && return 0 return 1 } # render_page template outfile # All variables exported before calling this function are available. render_page() { local template="$1" local outfile="$2" mkdir -p "$(dirname "${outfile}")" # Copy template into partials/ so mo resolves {{> partial}} relative to that dir, # then remove the copy. Avoids any /dev/stdin or /dev/fd dependency. local _tmpl_name="${template##templates/}" cp "${template}" "templates/partials/${_tmpl_name}" (cd templates/partials && mo "${_tmpl_name}") > "${outfile}" rm -f "templates/partials/${_tmpl_name}" } # build_breadcrumbs path -> sets BREADCRUMBS variable build_breadcrumbs() { local path="$1" BREADCRUMBS="" local components="${path}" while [[ "${components}" != "." ]]; do BREADCRUMBS=" / $(basename "${components}")${BREADCRUMBS}" components="$(dirname "${components}")" done } # get_frontmatter key file get_frontmatter() { local key="$1" file="$2" # lowdown extracts YAML frontmatter fields lowdown -X "${key}" "${file}" 2>/dev/null || true } # check_has_code content_html -> sets HAS_CODE ("true" or "") check_has_code() { local html="$1" if echo "${html}" | grep -qE '
 "${_find_list}"

    while IFS= read -r -d '' src; do
        local filename
        filename="$(basename "${src}")"
        local title date lang event header_extra
        title="$(get_frontmatter title "${src}")"
        date="$(get_frontmatter date "${src}")"
        lang="$(get_frontmatter lang "${src}")"
        event="$(get_frontmatter event "${src}")"

        # Derive URL path
        local rel="${src#${dir}/}"     # e.g. hello.md
        local slug="${rel%.md}"        # e.g. hello
        local url_path="/${dir}/${slug}"
        local out_dir="output/${dir}/${slug}"
        local out_html="${out_dir}/index.html"

        # Draft handling
        if [[ "${date_required}" == "true" && -z "${date}" ]]; then
            if [[ "${DRAFTS}" == true ]]; then
                title="[DRAFT] ${title}"
                printf -- '- [DRAFT] [%s](%s)\n' "${title}" "${url_path}" >> "${list_file}"
                echo "[draft] ${src}"
            else
                log "[skipped-draft] ${src}"
                continue
            fi
        else
            if [[ -n "${date}" ]]; then
                if [[ -n "${event}" ]]; then
                    printf -- '- [%s] [%s](%s) @ %s\n' "${date}" "${title}" "${url_path}" "${event}" >> "${list_file}"
                else
                    printf -- '- [%s] [%s](%s)\n' "${date}" "${title}" "${url_path}" >> "${list_file}"
                fi
            else
                printf -- '- [%s](%s)\n' "${title}" "${url_path}" >> "${list_file}"
            fi
        fi

        # Build the page
        header_extra="${date}"
        [[ -n "${event}" ]] && header_extra="${date}${date:+ — }${event}"

        local tmpl="templates/${template_type}.html"
        render_content_page "${src}" "${out_html}" "${tmpl}" "${header_extra}" "${lang:-en}"
        echo "[${template_type}] ${url_path}"

    done < "${_find_list}"

    # Build archive page
    local archive_md="${TMPDIR_BUILD}/${dir}-archive.md"
    printf '# %s\n\n' "${archive_title}" > "${archive_md}"
    sort -r "${list_file}" >> "${archive_md}"

    local archive_html="output/${dir}/index.html"
    export ARCHIVE_TITLE="${archive_title}"
    export PAGE_TITLE="${archive_title}"
    export CONTENT
    CONTENT="$(lowdown "${archive_md}")"
    export BREADCRUMBS=" / ${dir}"
    export HEADER_EXTRA=""
    export META_DATE=""
    export META_DESCRIPTION=""
    export HAS_CODE=""
    export LANG="en"
    export DIR=""
    export IS_HOME=""

    if needs_rebuild "${list_file}" "${archive_html}" "templates/archive.html"; then
        render_page "templates/archive.html" "${archive_html}"
        echo "[archive] ${archive_html}"
    else
        log "[skip] ${archive_html}"
    fi
}

# ---------------------------------------------------------------------------
# process_projects
# ---------------------------------------------------------------------------
process_projects() {
    local list_file="${TMPDIR_BUILD}/projects.list.md"
    : > "${list_file}"

    if [[ ! -d "projects" ]]; then
        echo "[warning] projects/ directory not found"
        return
    fi

    local _proj_list="${TMPDIR_BUILD}/projects.find.list"
    find "projects" -mindepth 1 -maxdepth 1 -name '*.md' -not -name '.*' -print0 | sort -z > "${_proj_list}"

    while IFS= read -r -d '' src; do
        local title url demo sources
        title="$(get_frontmatter title "${src}")"
        url="$(get_frontmatter url "${src}")"
        demo="$(get_frontmatter demo "${src}")"
        sources_raw="$(get_frontmatter sources "${src}")"

        local slug
        slug="$(basename "${src}" .md)"
        local url_path="/projects/${slug}"
        local out_html="output/projects/${slug}/index.html"
        local tmpl="templates/project.html"

        # Build sources HTML links from "name:url,name:url" pairs
        local sources_html=""
        if [[ -n "${sources_raw}" ]]; then
            IFS=',' read -ra pairs <<< "${sources_raw}"
            for pair in "${pairs[@]}"; do
                local sname surl
                sname="${pair%%:*}"
                surl="${pair#*:}"
                # Handle URLs with double-colon (http://)
                # The format is name:scheme://rest  — re-join after first ':'
                surl="${pair#${sname}:}"
                sources_html+="${sname} "
            done
            sources_html="${sources_html% }"
        fi

        # Archive entry
        printf -- '- [%s](%s)\n' "${title}" "${url_path}" >> "${list_file}"

        if ! needs_rebuild "${src}" "${out_html}" "${tmpl}"; then
            log "[skip] ${out_html}"
            continue
        fi

        local content
        content="$(lowdown "${src}")"
        check_has_code "${content}"

        build_breadcrumbs "projects/${slug}"

        export PAGE_TITLE="${title}"
        export CONTENT="${content}"
        export BREADCRUMBS
        export HEADER_EXTRA=""
        export META_DATE=""
        export META_DESCRIPTION=""
        export HAS_CODE
        export LANG="en"
        export DIR=""
        export IS_HOME=""
        export PROJECT_URL="${url}"
        export PROJECT_DEMO="${demo}"
        export PROJECT_SOURCES="${sources_html}"
        export ARCHIVE_TITLE=""

        render_page "${tmpl}" "${out_html}"
        echo "[project] ${url_path}"

    done < "${_proj_list}"

    # Archive
    local archive_md="${TMPDIR_BUILD}/projects-archive.md"
    printf '# projects\n\n' > "${archive_md}"
    cat "${list_file}" >> "${archive_md}"

    local archive_html="output/projects/index.html"
    export ARCHIVE_TITLE="projects"
    export PAGE_TITLE="projects"
    CONTENT="$(lowdown "${archive_md}")"
    export CONTENT
    export BREADCRUMBS=" / projects"
    export HEADER_EXTRA=""
    export META_DATE=""
    export META_DESCRIPTION=""
    export HAS_CODE=""
    export LANG="en"
    export DIR=""
    export IS_HOME=""
    export PROJECT_URL=""
    export PROJECT_DEMO=""
    export PROJECT_SOURCES=""

    if needs_rebuild "${list_file}" "${archive_html}" "templates/archive.html"; then
        render_page "templates/archive.html" "${archive_html}"
        echo "[archive] ${archive_html}"
    else
        log "[skip] ${archive_html}"
    fi
}

# ---------------------------------------------------------------------------
# process_slides
# ---------------------------------------------------------------------------
process_slides() {
    if [[ ! -d "slides" ]]; then
        log "[info] slides/ directory not found, skipping"
        return
    fi

    local list_file="${TMPDIR_BUILD}/slides.list.md"
    : > "${list_file}"

    local _slides_list="${TMPDIR_BUILD}/slides.find.list"
    find "slides" -mindepth 1 -maxdepth 1 -name '*.md' -not -name '.*' -print0 | sort -z > "${_slides_list}"

    while IFS= read -r -d '' src; do
        local title date theme
        title="$(get_frontmatter title "${src}")"
        date="$(get_frontmatter date "${src}")"
        theme="$(get_frontmatter theme "${src}")"
        [[ -z "${theme}" ]] && theme="black"

        local slug
        slug="$(basename "${src}" .md)"
        local url_path="/slides/${slug}"
        local out_html="output/slides/${slug}/index.html"
        local tmpl="templates/slides.html"

        if [[ -n "${date}" ]]; then
            printf -- '- [%s] [%s](%s)\n' "${date}" "${title}" "${url_path}" >> "${list_file}"
        else
            printf -- '- [%s](%s)\n' "${title}" "${url_path}" >> "${list_file}"
        fi

        if ! needs_rebuild "${src}" "${out_html}" "${tmpl}"; then
            log "[skip] ${out_html}"
            continue
        fi

        # Split markdown on '---' slide separators into 
blocks local slides_content slides_content="$(awk ' BEGIN { in_slide=0; buf="" } /^---$/ { if (in_slide) { print "
" buf="" } else { in_slide=1 } next } in_slide { buf = buf "\n" $0 } END { if (buf != "") { print "
" } } ' "${src}")" mkdir -p "output/slides/${slug}" export TITLE="${title}" export THEME="${theme}" export SLIDES_CONTENT="${slides_content}" cp "templates/slides.html" "templates/partials/slides.html" (cd templates/partials && mo "slides.html") > "${out_html}" rm -f "templates/partials/slides.html" echo "[slides] ${url_path}" done < "${_slides_list}" # Archive listing local archive_md="${TMPDIR_BUILD}/slides-archive.md" printf '# slides\n\n' > "${archive_md}" sort -r "${list_file}" >> "${archive_md}" local archive_html="output/slides/index.html" export ARCHIVE_TITLE="slides" export PAGE_TITLE="slides" CONTENT="$(lowdown "${archive_md}")" export CONTENT export BREADCRUMBS=" / slides" export HEADER_EXTRA="" export META_DATE="" export META_DESCRIPTION="" export HAS_CODE="" export LANG="en" export DIR="" export IS_HOME="" if needs_rebuild "${list_file}" "${archive_html}" "templates/archive.html"; then render_page "templates/archive.html" "${archive_html}" echo "[archive] ${archive_html}" else log "[skip] ${archive_html}" fi } # --------------------------------------------------------------------------- # process_garden # --------------------------------------------------------------------------- process_garden() { local dir="${1:-garden}" local garden_list="${TMPDIR_BUILD}/${dir}.garden.list.md" : > "${garden_list}" local _garden_find="${TMPDIR_BUILD}/${dir//\//_}.find.list" find "${dir}" -mindepth 1 -maxdepth 1 -not -name '.*' -print0 | sort -z > "${_garden_find}" while IFS= read -r -d '' node; do if [[ -d "${node}" ]]; then local subnode_title subnode_title="$(get_frontmatter title "${node}/index.md" 2>/dev/null || true)" [[ -z "${subnode_title}" ]] && subnode_title="$(basename "${node}")" printf -- '- [%s/](/%s)\n' "${subnode_title}" "${node}" >> "${garden_list}" process_garden "${node}" else if [[ "${node}" == "${node%.md}" ]]; then # Non-markdown asset — copy verbatim cp -a --parent "${node}" output/ elif [[ "$(basename "${node}")" != "index.md" ]]; then local title date lang title="$(get_frontmatter title "${node}")" [[ -z "${title}" ]] && title="$(basename "${node}" .md)" date="$(get_frontmatter date "${node}")" lang="$(get_frontmatter lang "${node}")" local rel="${node#${dir}/}" local slug="${rel%.md}" local url_path="/${dir}/${slug}" local out_html="output/${dir}/${slug}/index.html" local tmpl="templates/garden.html" render_content_page "${node}" "${out_html}" "${tmpl}" "${date}" "${lang:-en}" printf -- '- [%s](%s)\n' "${title}" "${url_path}" >> "${garden_list}" garden_count=$(( garden_count + 1 )) fi fi done < "${_garden_find}" # Build / update the index for this directory local garden_index_md="${TMPDIR_BUILD}/${dir//\//_}.index.md" local garden_title if [[ -f "${dir}/index.md" ]]; then garden_title="$(get_frontmatter title "${dir}/index.md")" # Append the listing to a copy of the index cp "${dir}/index.md" "${garden_index_md}" else garden_title="$(basename "${dir}")" printf '# %s\n\n' "${garden_title}" > "${garden_index_md}" fi printf '\n\n' >> "${garden_index_md}" cat "${garden_list}" >> "${garden_index_md}" local out_html="output/${dir}/index.html" local tmpl="templates/garden.html" if needs_rebuild "${garden_index_md}" "${out_html}" "${tmpl}"; then local content content="$(lowdown "${garden_index_md}")" check_has_code "${content}" build_breadcrumbs "${dir}" export PAGE_TITLE="${garden_title}" export CONTENT="${content}" export BREADCRUMBS export HEADER_EXTRA="" export META_DATE="" export META_DESCRIPTION="" export HAS_CODE export LANG="en" export DIR="" export IS_HOME="" export ARCHIVE_TITLE="" render_page "${tmpl}" "${out_html}" echo "[garden] output/${dir}/index.html" else log "[skip] output/${dir}/index.html" fi } # --------------------------------------------------------------------------- # generate_rss_feed # --------------------------------------------------------------------------- generate_rss_feed() { local list_file="${TMPDIR_BUILD}/blog.list.md" [[ ! -f "${list_file}" ]] && return local out="output/feed.xml" mkdir -p output { cat <<'RSSHEAD' gumx https://gumx.cc gumx blog en RSSHEAD # Top 20 entries, sorted newest first sort -r "${list_file}" | head -n 20 | while IFS= read -r line; do # Line format: - [DATE] [TITLE](URL) local date title url date="$(echo "${line}" | sed -n 's/.*\[\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\)\].*/\1/p')" title="$(echo "${line}" | sed -n 's/.*\] \[\([^]]*\)\](.*/\1/p')" url="$(echo "${line}" | sed -n 's/.*\](\([^)]*\)).*/\1/p')" [[ -z "${date}" || -z "${title}" || -z "${url}" ]] && continue # RFC 2822 date from YYYY-MM-DD local rfc_date rfc_date="$(date -d "${date}" -R 2>/dev/null || date -jf '%Y-%m-%d' "${date}" '+%a, %d %b %Y 00:00:00 +0000' 2>/dev/null || echo "${date}")" cat < ${title} https://gumx.cc${url} https://gumx.cc${url} ${rfc_date} ITEM done echo " " echo "" } > "${out}" echo "[rss] ${out}" } # generate_sitemap # --------------------------------------------------------------------------- generate_sitemap() { local out="output/sitemap.xml" { echo '' echo '' find output -name 'index.html' | sort | while read -r html; do local path path="${html#output}" # /blog/hello/index.html path="${path%index.html}" # /blog/hello/ echo " https://gumx.cc${path}" done echo '' } > "${out}" echo "[sitemap] ${out}" } # --------------------------------------------------------------------------- # build_includes # --------------------------------------------------------------------------- build_includes() { local inc_md="${TMPDIR_BUILD}/includes.md" printf '# includes\n\n' > "${inc_md}" for inc in $(find includes -type f ! -name '.*' | sort); do printf -- '- [%s](/%s)\n' "$(basename "${inc}")" "${inc}" >> "${inc_md}" done local out_html="output/includes/index.html" local tmpl="templates/base.html" if needs_rebuild "${inc_md}" "${out_html}" "${tmpl}"; then local content content="$(lowdown "${inc_md}")" check_has_code "${content}" export PAGE_TITLE="includes" export CONTENT="${content}" export BREADCRUMBS=" / includes" export HEADER_EXTRA="" export META_DATE="" export META_DESCRIPTION="" export HAS_CODE export LANG="en" export DIR="" export IS_HOME="" export ARCHIVE_TITLE="" render_page "${tmpl}" "${out_html}" echo "[built] ${out_html}" else log "[skip] ${out_html}" fi } # --------------------------------------------------------------------------- # build_license # --------------------------------------------------------------------------- build_license() { local src="license.md" local out_html="output/license/index.html" local tmpl="templates/base.html" if needs_rebuild "${src}" "${out_html}" "${tmpl}"; then local content content="$(lowdown "${src}")" check_has_code "${content}" export PAGE_TITLE="license" export CONTENT="${content}" export BREADCRUMBS=" / license" export HEADER_EXTRA="" export META_DATE="" export META_DESCRIPTION="" export HAS_CODE export LANG="en" export DIR="" export IS_HOME="" export ARCHIVE_TITLE="" render_page "${tmpl}" "${out_html}" echo "[built] ${out_html}" else log "[skip] ${out_html}" fi } # --------------------------------------------------------------------------- # build_home # --------------------------------------------------------------------------- build_home() { local src="index.md" local out_html="output/index.html" local tmpl="templates/home.html" # We always rebuild home (it aggregates counts/lists from other passes) local home_md="${TMPDIR_BUILD}/index.home.md" cp "${src}" "${home_md}" # Inject garden count sed -i "s/\[%garden\]/${garden_count}/g" "${home_md}" # Recent blog entries { printf '\n\n## recent entries:\n\n' local blog_list="${TMPDIR_BUILD}/blog.list.md" if [[ -f "${blog_list}" && -s "${blog_list}" ]]; then sort -r "${blog_list}" | head -n "${LIST_COUNT}" local total total="$(wc -l < "${blog_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/blog)\n' else printf -- '- stuff I wrote should be listed here\n' fi } >> "${home_md}" # Recent talks { printf '\n\n## talks & workshops:\n\n' local talks_list="${TMPDIR_BUILD}/talks.list.md" if [[ -f "${talks_list}" && -s "${talks_list}" ]]; then sort -r "${talks_list}" | head -n "${LIST_COUNT}" local total total="$(wc -l < "${talks_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/talks)\n' else printf -- '- stuff I said should be listed here\n' fi } >> "${home_md}" # Projects { printf '\n\n## projects:\n\n' local proj_list="${TMPDIR_BUILD}/projects.list.md" if [[ -f "${proj_list}" && -s "${proj_list}" ]]; then head -n "${LIST_COUNT}" "${proj_list}" local total total="$(wc -l < "${proj_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/projects)\n' else printf -- '- stuff I made should be listed here\n' fi } >> "${home_md}" # Code snippets { local code_list="${TMPDIR_BUILD}/code.list.md" if [[ -f "${code_list}" && -s "${code_list}" ]]; then printf '\n\n## code snippets:\n\n' head -n "${LIST_COUNT}" "${code_list}" local total total="$(wc -l < "${code_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/code)\n' fi } >> "${home_md}" # Recipes { local recipes_list="${TMPDIR_BUILD}/recipes.list.md" if [[ -f "${recipes_list}" && -s "${recipes_list}" ]]; then printf '\n\n## recipes:\n\n' head -n "${LIST_COUNT}" "${recipes_list}" local total total="$(wc -l < "${recipes_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/recipes)\n' fi } >> "${home_md}" # Slides { local slides_list="${TMPDIR_BUILD}/slides.list.md" if [[ -f "${slides_list}" && -s "${slides_list}" ]]; then printf '\n\n## slides:\n\n' sort -r "${slides_list}" | head -n "${LIST_COUNT}" local total total="$(wc -l < "${slides_list}")" [[ "${total}" -gt "${LIST_COUNT}" ]] && printf -- '- [all ..](/slides)\n' fi } >> "${home_md}" local content content="$(lowdown "${home_md}")" check_has_code "${content}" export PAGE_TITLE="" export CONTENT="${content}" export BREADCRUMBS="" export HEADER_EXTRA="" export META_DATE="" export META_DESCRIPTION="" export HAS_CODE export LANG="en" export DIR="" export IS_HOME="true" export ARCHIVE_TITLE="" render_page "${tmpl}" "${out_html}" echo "[built] ${out_html}" } # --------------------------------------------------------------------------- # Main build # --------------------------------------------------------------------------- mkdir -p output # Copy static assets cp -ar static/* output/ 2>/dev/null || true cp -ar includes output/ 2>/dev/null || true cp -ar images output/ 2>/dev/null || true echo "[static] assets copied" # Content sections process_content "blog" "blog" "blog archive" "true" process_content "talks" "talk" "talks & workshops" "true" process_content "code" "code" "code snippets" "false" process_content "recipes" "recipe" "recipes" "false" # (list files already live in TMPDIR_BUILD with canonical names) process_projects process_slides # Garden (count pages for home page injection) garden_count=0 process_garden garden # Includes + license build_includes build_license # Home (uses all collected list files) build_home # RSS + Sitemap generate_rss_feed generate_sitemap echo "" echo "[done] Build complete. Output in output/" # --------------------------------------------------------------------------- # --serve # --------------------------------------------------------------------------- if [[ "${DO_SERVE}" == true ]]; then if command -v darkhttpd &>/dev/null; then echo "[serve] Starting darkhttpd on http://localhost:8080" darkhttpd output --port 8080 else echo "[error] darkhttpd not found. Install it to use --serve." >&2 exit 1 fi fi