""" Copyright 2024-2026 ChatterMate Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Main-content selection shared by the knowledge crawler (which extracts text) and the help-center article importer (which needs the actual DOM node to convert to Markdown). One implementation, because the two drifting apart is how the crawler bug below survived in the importer. """ from typing import Iterable, Optional from bs4 import BeautifulSoup, Tag # Tags stripped before any content selection — never useful content. STRIP_TAGS = ("script", "style", "noscript", "iframe", "head") # Chrome that repeats on every page. Excluded when measuring how much content a # container holds, so that a page whose only text is a phone number and a login # link reads as empty rather than full. BOILERPLATE_TAGS = ("nav", "header", "footer", "aside") # A container must hold at least this share of the page's content text to be # considered "the page". Below it we keep looking outward. CONTENT_RETENTION = 0.90 MIN_MAIN_CONTENT_CHARS = 100 # Containers worth considering, cheapest-to-richest. 'div' is here because most # sites wrap their content in one rather than in
. CANDIDATE_TAGS = ("article", "main", "section", "div") def content_chars(node: Tag) -> int: """Length of a node's text, ignoring navigation chrome. solcontrol.ca has 147 characters of body text, all of it a phone number and a login link. Counting that as content made the page look ingestible and suppressed the browser fallback that could actually read it. """ total = len(node.get_text(strip=True)) for boilerplate in node.find_all(BOILERPLATE_TAGS): # Only outermost chrome: a