code wiki / (root) / nx_block_density.nx

nx_block_density.nx

buildroot/runtime/nx_block_density.nx

29895 B586 linesdepth 4pulls 5 transitivereach 33 importersview sourcekind librarytopic block
docsdependenciesstructsconstsfunctions

about

nx_block_density.nx -- BOILERPLATE BLOCK SCORING for HTML: the /compare/webscraping R6 contract (symbol bd_fit_text). Kohlschuetter, Fankhauser, Nejdl (WSDM 2010) shallow text features -- per-block TEXT DENSITY and LINK DENSITY -- computed in integer permil over the flat tag stream, so an indexer stores the article and not the nav, footer and sidebar around it. WHY THIS SHAPE (2026-08-24): nx_html_to_text is a tag-stripping renderer with 51 importers and a frozen byte contract; the /compare row that credited it with a "block-aware boilerplate strip" overclaimed -- no density scoring existed anywhere in the tree (link_density / text_density: 0 matches, corpus complete). This lib is the missing ruler. It COMPOSES nx_html_to_text for the final text so the estate still has exactly ONE HTML-to-text renderer: fit = score blocks -> rebuild HTML from the kept spans -> nx_html_to_text. No second entity table, no second whitespace policy, no second tag list. A BLOCK is the span between two block-level tag boundaries (open or close of p, div, li, h1-h6, section, article, aside, nav, header, footer, main, table, tr, td, th, blockquote, pre, ul, ol, dl, dt, dd, figure, figcaption, form, hr, body). script / style / noscript / template / svg bodies are never text and are removed from every block before counting; sup / sub text (citation brackets) is not counted as words. Per block: text bytes (whitespace runs collapsed), markup bytes, words, link words (words that begin inside an <a>). link_density = link_words * 1000 / words, in permil. THE DECISION IS THE PUBLISHED ONE, NOT AN INVENTED SCORE. Calibration on four real pages (2026-08-24, two Wikipedia articles, a Python docs page, an LWN article) refuted a weighted text-density score: Wikipedia prose measures ~7 pct text density and ~60 pct character link density because every link carries ~100 bytes of markup and citation brackets are anchor text -- the score dropped 86 real paragraphs on one page. Kohlschuetter's densitometric classifier uses WORD COUNTS of the previous, current and next block plus word-based link density; the thresholds below are the learned values his reference implementation (boilerpipe NumWordsRulesClassifier) ships, cited on the /compare row: if curr.ld <= ld_curr_max: if prev.ld <= ld_prev_max: if curr.words <= words_curr_short: if next.words <= words_next_short: (prev.words <= words_prev_short ? BOILERPLATE : CONTENT) else CONTENT else CONTENT else: if curr.words <= words_curr_long: (next.words <= words_next_long ? BOILERPLATE : CONTENT) else CONTENT else BOILERPLATE prev/next are the nearest blocks WITH words (empty wrapper and close-tag blocks are structural and kept so a page with no boilerplate rebuilds byte-identical). Blocks inside a semantic element the conf drops (nav / footer / aside by default) are dropped by construction. If nothing survives, or the page has one block, the FULL text is returned and bd_last_fallback_g says so -- a filter that can return an empty page is worse than no filter.

dependencies 3 imports · 6 importers

nx_syscalls.nx nx_lane_conf.nx nx_html_to_text.nx nx_block_density.nx nx_block_density_cli.nx nx_block_density_gate.nx nx_crawl_sufficiency_cli.nx nx_docportal_lib.nx nx_fetchwall_lib.nx nx_web_crawl_step.nx

imports: nx_syscalls.nxnx_lane_conf.nxnx_html_to_text.nx

imported by: nx_block_density_cli.nxnx_block_density_gate.nxnx_crawl_sufficiency_cli.nxnx_docportal_lib.nxnx_fetchwall_lib.nxnx_web_crawl_step.nx

structs

none

consts

50const BD_CONF: *u8 = "knowledge/block_density.conf"
52const BD_DEF_LD_CURR_MAX: i64 = 333 // boilerpipe NumWordsRulesClassifier: curr linkDensity <= 0.333333
53const BD_DEF_LD_PREV_MAX: i64 = 556 // prev linkDensity <= 0.555556
54const BD_DEF_WORDS_CURR_SHORT: i64 = 16 // curr numWords <= 16
55const BD_DEF_WORDS_NEXT_SHORT: i64 = 15 // next numWords <= 15
56const BD_DEF_WORDS_PREV_SHORT: i64 = 4 // prev numWords <= 4
57const BD_DEF_WORDS_CURR_LONG: i64 = 40 // (prev linky) curr numWords <= 40
58const BD_DEF_WORDS_NEXT_LONG: i64 = 17 // (prev linky) next numWords <= 17
59const BD_DEF_DROP_NAV: i64 = 1 // HTML5 semantic elements mean what they say; header stays because article headers hold the title
60const BD_DEF_DROP_FOOTER: i64 = 1
61const BD_DEF_DROP_ASIDE: i64 = 1
62const BD_DEF_DROP_HEADER: i64 = 0
63const BD_DEF_DROP_FORM: i64 = 0 // MEASURED 2026-08-24: LWN wraps its whole comment thread in a <form>; dropping forms
66const BD_PERMIL: i64 = 1000
68const BD_REC: i64 = 10
69const BD_F_START: i64 = 0
70const BD_F_END: i64 = 1
71const BD_F_TEXT: i64 = 2
72const BD_F_MARKUP: i64 = 3
73const BD_F_LINK: i64 = 4
74const BD_F_WORDS: i64 = 5
75const BD_F_SCORE: i64 = 6
76const BD_F_KEPT: i64 = 7
77const BD_F_SEM: i64 = 8
78const BD_F_LWORDS: i64 = 9
79const BD_KEPT_NO: i64 = 0
80const BD_KEPT_YES: i64 = 1
81const BD_KEPT_RESCUED: i64 = 2
82const BD_KEPT_SEMANTIC: i64 = 3 // dropped because inside nav/footer/aside/form
83const BD_KEPT_STRUCT: i64 = 4 // no words: a wrapper or close tag, kept for structure, contributes no text
85const BD_SEM_NAV: i64 = 0
86const BD_SEM_FOOTER: i64 = 1
87const BD_SEM_ASIDE: i64 = 2
88const BD_SEM_HEADER: i64 = 3
89const BD_SEM_FORM: i64 = 4
90const BD_SEM_N: i64 = 5
91const BD_LT: i64 = 60
92const BD_GT: i64 = 62
93const BD_SLASH: i64 = 47
94const BD_BANG: i64 = 33
95const BD_DASH: i64 = 45
96const BD_SPACE: i64 = 32

functions

123func bd_conf_one(key: *u8, dflt: i64) -> i64
called by 1: bd_load_conf calls 1: lc_geti
129func bd_load_conf() -> i64
147func bd_conf_reset() -> i64 { bd_conf_loaded_g = 0; return 0 }
called by 1: main
150func bd_text_density(text: i64, markup: i64) -> i64
called by 1: bd_map
162func bd_classify_t(pw: i64, pld: i64, cw: i64, cld: i64, nw: i64,
182func bd_classify(pw: i64, pld: i64, cw: i64, cld: i64, nw: i64) -> i64
called by 1: main calls 2: bd_load_confbd_classify_t
189func bd_lc(c: i64) -> i64 { if c >= 65 { if c <= 90 { return c + 32 } } return c }
190func bd_is_ws(c: i64) -> i64
called by 1: bd_scan
198func bd_is_name(c: i64) -> i64
called by 1: bd_scan
205func bd_name_eq(src: *u8, off: i64, len: i64, lit: *u8) -> i64
217func bd_is_block(src: *u8, off: i64, len: i64) -> i64
called by 1: bd_scan calls 2: bd_lcbd_name_eq
248func bd_sem_slot(src: *u8, off: i64, len: i64) -> i64
called by 1: bd_scan calls 1: bd_name_eq
256func bd_sem_dropped(slot: i64) -> i64
called by 1: bd_scan
265func bd_is_suppress(src: *u8, off: i64, len: i64) -> i64
called by 1: bd_scan calls 1: bd_name_eq
274func bd_find_close(src: *u8, n: i64, p: i64, nameoff: i64, namelen: i64) -> i64
called by 1: bd_scan calls 1: bd_lc
291func bd_rec(tbl: *i64, i: i64, f: i64) -> i64 { return tbl[i * BD_REC + f] }
292func bd_set(tbl: *i64, i: i64, f: i64, v: i64) -> i64 { tbl[i * BD_REC + f] = v; return 0 }
called by 2: bd_scanbd_decide
296func bd_scan(src: *u8, n: i64, tbl: *i64, depth: *i64) -> i64
418func bd_prev_content(tbl: *i64, j: i64) -> i64
called by 1: bd_decide calls 1: bd_rec
423func bd_next_content(tbl: *i64, nb: i64, j: i64) -> i64
called by 1: bd_decide calls 1: bd_rec
428func bd_decide(tbl: *i64, nb: i64) -> i64
468func bd_count_lt(src: *u8, n: i64) -> i64
476func bd_fit_text(src: *u8, n: i64, out: *u8, cap: i64) -> i64
521func bd_map_num(out: *u8, cap: i64, o: i64, v: i64) -> i64
called by 2: bd_mapbc_num calls 2: sys_mmapsys_munmap
533func bd_map_str(out: *u8, cap: i64, o: i64, s: *u8) -> i64
called by 1: bd_map
540func bd_map(src: *u8, n: i64, out: *u8, cap: i64) -> i64